Go to Hackademy website

Décompresser et parser des fichiers en Go

Théo Delaune

Posté par Théo Delaune dans les catégories back

Aujourd’hui nous allons aborder le parsing de fichiers xml contenus dans un zip avec le langage Go.

Vous pouvez retrouver nos précédents articles sur les débuts en Go sur notre blog.

Notre application

Nous allons prendre le cas d’un concessionnaire automobile qui reçoit de son fournisseur la liste des voitures sous forme de fichiers xml contenus dans un zip. Le but est d’obtenir après traitement par l’application, la liste de chaque voiture avec ses attributs.

<cars>
  <car>
    <manufacturer>Citroën</manufacturer>
    <model>c1</model>
    <year>2015</year>
  </car>
  <car>
    <manufacturer>Citroën</manufacturer>
    <model>c2</model>
    <year>2015</year>
  </car>
  <car>
</cars>

Nous allons pour cet exemple prendre le cas d’un zip cars.zip qui contient deux fichiers xml: - citroen.xml - volvo.xml

Architecture du projet

Nous sommes partis sur une architecture simple, où nous avons séparé les différentes parties dans des packages spécifiques.

parsexml/
  zip/
    unzip.go
  extractor/
    car.go
  main.go
  • unzip.go provient du package unzip et va gérer la décompression d’un fichier .zip
  • car.go provient du package extractor et va gérer la récupération des voitures dans le fichier xml
  • main.go permet de lancer l’application de d’afficher les voitures extraites

package unzip

Nous allons créer le fichier unzip.go au sein du dossier unzip, dans un premier temps nous allons mettre en place la structure de base du fichier.

Nous allons initialiser une constante permettant de spécifier le dossier où seront extrait les fichiers, ainsi que deux fonctions:

  • TmpDirectoryCreate va créer le dossier tmp/ si il n’existe pas à l’endroit où l’exécutable se trouve
  • Unzip va dézipper le fichier passé en argument.
package unzip

import (

)

const (
  TMP_DIRECTORY = "tmp/"
)

func Unzip(src string) error {

  return nil
}

func TmpDirectoryCreate() error {

  return nil
}

Création du dossier temporaire

Nous devons créer le répertoire permettant de récupérer les fichiers extraits.

Le répertoire va être créé avec la fonction MkdirAll, si le répertoire existe déjà rien n’est fait et la fonction nous retourne nil.

Dans le cas où le répertoire n’existe pas, il est créé et la fonction nous retourne également nil car aucune erreur n’est levée.

Si une erreur apparaît elle est retournée pour pouvoir la traiter lorsque l’on va appeler notre fonction TmpDirectoryCreate.

Veillez à inclure le package “os” au niveau de la partie import de votre fichier unzip.go.

func TmpDirectoryCreate() error {
  return os.MkdirAll(TMP_DIRECTORY, 0775)
}

Décompresser une archive zip

La décompression d’une archive zip va être effectuée au sein de notre fonction Unzip qui retourne seulement une erreur.

Dans le cas où la décompression se passe correctement le retour est nil, sinon nous renvoyons l’erreur levée.

Les packages à ajouter dans l’import sont les suivants: - “archive/zip” - “io” - “log” - “path”

func Unzip(src string) error {
  if err := TmpDirectoryCreate(); err != nil {
    log.Println(err)
    return err
  }

  r, err := zip.OpenReader(src)

  if err != nil {
    log.Println(err)
    return err
  }

  defer r.Close()

  for _, file := range r.File {
    rc, err := file.Open()

    if err != nil {
      log.Println(err)
      return err
    }

    defer rc.Close()

    file_path := path.Join(TMP_DIRECTORY, file.Name)

    if file.FileInfo().IsDir() {
      os.MkdirAll(file_path, file.Mode())
    } else {
      newFile, err := os.Create(file_path)

      if err != nil {
        log.Println(err)
        return err
      }

      io.Copy(newFile, rc)
      newFile.Sync()
    }
  }
  return nil
}

Nous allons avant tout ouvrir le fichier .zip avec la fonction OpenReader du package zip. Nous vérifions et ajoutons un log si il y a une erreur.

Après la gestion d’erreur nous utilisons la fonction defer.

Cette fonction va permettre d’ajouter la fermeture du fichier r.Close() dans une pile. Elle sera exécutée à la fin des appels de la fonction pour fermer le fichier en toute sécurité, même si une exécution crash dans la suite de la fonction.

Dans r, nous obtenons donc la liste des fichiers inclus dans l’archive. Comme précédemment nous l’ouvrons et le fermons grâce à la fonction defer.

Par la suite nous créons le path du fichier pour obtenir dans la variable file_path, un path de la sorte: “tmp/citroen.xml”

Nous gérons par la suite le cas où la variable rc est un répertoire. Dans ce cas nous le créons.

Dans le cas d’un fichier, nous le créons avec le path obtenu juste au dessus via la méthode Create. Nous pouvons alors copier le contenu du fichier rc qui est le fichier issu de l’archive zip, dans le fichier que nous venons de créer grâce à la fonction Copy.

Il ne faut pas oublier de persister le fichier grâce à la fonction Sync.

package extractor

Nous allons créer le fichier car.go au sein du dossier extractor.

package extractor

import (
  "encoding/xml"
  "io/ioutil"
  "log"
  "os"
)

type Car struct {

}

type Cars struct {

}

func ExtractCarsFromXml(fp string) ([]Car, error) {

}

Les structures

Nous allons calquer nos structures sur la structure du fichier xml vue plus haut.

La première structure est la structure Cars, nous allons lui passer le champ xml auquel il correspond, ainsi qu’un tableau de type Car.

type Cars struct {
  XMLName xml.Name `xml:"cars"`
  Cars    []Car    `xml:"car"`
}

Nous avons besoin également d’une structure Car, qui va elle contenir le nom du champ xml auquel il correspond ainsi que ses attributs.

type Car struct {
  XMLName      xml.Name `xml:"car"`
  Manufacturer string   `xml:"manufacturer"`
  Model        string   `xml:"model"`
  Year         int      `xml:"year"`
}

Nous spécifions pour chaque item d’une structure le champ xml auquel il correspond grâce à xml:"monattribut".

Extraction depuis un fichier xml

func ExtractCarsFromXml(fp string) ([]Car, error) {
  file, err := os.Open(fp)

  if err != nil {
    log.Println(err)
    return nil, err
  }

  defer file.Close()

  content, err := ioutil.ReadAll(file)

  if err != nil {
    log.Println(err)
    return nil, err
  }

  var cars Cars

  xml.Unmarshal(content, &cars)

  return cars.Cars, nil
}

Notre fonction ExtractCarsFromXml prend le path du fichier à extraire en argument et retourne un tableau de Car et une variable de type error.

Comme vu pour la décompression d’un fichier, nous allons ouvrir le fichier, mais cette fois ci, nous récupérons son contenu avec la fonction ReadAll qui prend le fichier ouvert en argument.

Nous créons ensuite une variable de type Cars, c’est cette variable que nous allons utiliser pour récupérer les voitures au sein de notre xml.

Pour parser le fichier xml, nous passons par la méthode UnMarshal auquel nous donnons le contenu du fichier ainsi que l’adresse de la variable cars.

cars contient maintenant toutes les voitures du fichier parsé. Pour plus de simplicité d’utilisation nous ne retournons que le tableau de Car, obtenu par cars.Cars.

Lancement de notre programme

Nous créons le fichier main.go à la racine de notre projet.

package main

import (
  "fmt"
  "github.com/theodelaune/parsexml/extractor"
  "github.com/theodelaune/parsexml/unzip"
  "io/ioutil"
  "log"
  "path"
)

func main() {

}

func ShowEachCar(cars []extractor.Car) {

}

fonction ShowEachCar

Cette fonction a pour but de parcourir la liste des voitures et de nous les afficher dans la console.

func ShowEachCar(cars []extractor.Car) {
  for _, car := range cars {
    fmt.Printf("Voiture de marque: %s", car.Manufacturer)
    fmt.Printf(" de modèle: %s de l'année %d", car.Model, car.Year)
    fmt.Println()
  }
}

fonction main

Lors du lancement de notre programme, celui-ci va débuter par cette fonction main.

func main() {
  log.Println("unzip file ...")

  err := zip.Unzip("cars.zip")

  if err != nil {
    return
  }

  log.Println("begin parsing...")

  files, _ := ioutil.ReadDir(unzip.TMP_DIRECTORY)

  for _, f := range files {
    log.Printf("extract from %s....", f.Name())

    path := path.Join(unzip.TMP_DIRECTORY, f.Name())

    if cars, err := extractor.ExtractCarsFromXml(path); err == nil {
      log.Printf("show cars from %s", f.Name())
      ShowEachCar(cars)
    }

  }
}

Dans un premier temps, nous allons lancer la décompression de notre archive grâce à notre fonction “Unzip”.

Puis nous récupérons tous les fichiers contenus dans notre répertoire temporaire.

Nous parcourons chaque fichier, et récupérons son path. Nous le parsons grâce à notre fonction ExtractCarsFromXml, pour plus de clarté nous affichons chaque voiture que nous avons extrait par la fonction ShowEachCar.

Au lancement de notre programme, la sortie doit ressembler à quelque chose comme cela:

2015/04/09 12:28:03 unzip file ...
2015/04/09 12:28:04 begin parsing...
2015/04/09 12:28:04 extract from citroen.xml....
2015/04/09 12:28:04 show cars from citroen.xml
Voiture de marque: Citroën de modèle: c1 de l'année 2015
Voiture de marque: Citroën de modèle: c2 de l'année 2015
Voiture de marque: Citroën de modèle: c3 de l'année 2015
Voiture de marque: Citroën de modèle: c4 de l'année 2015
2015/04/09 12:28:04 extract from volvo.xml....
2015/04/09 12:28:04 show cars from volvo.xml
Voiture de marque: Volvo de modèle: xc60 de l'année 2015
Voiture de marque: Volvo de modèle: xc90 de l'année 2014
Voiture de marque: Volvo de modèle: v40 de l'année 2015

Conclusion

Cet article nous a permis de mettre en pratique le langage Go sur la décompression d’une archive de type zip, ainsi que le parsage de fichiers xml.

Dans un prochain article, nous continuerons sur cette partie en ajoutant l’écriture en base de donnée de chaque voiture extraite.

En espérant que cette approche au langage Go vous a plu !

Les sources de cet article sont disponibles sur GitHub.


L’équipe Synbioz. Libres d’être ensemble.

Articles connexes

Une brève histoire d'Elixir et Erlang/OTP

31/01/2019

Je développe depuis plusieurs années en Ruby. Depuis mon arrivée chez Synbioz, j’expérimente en plus avec Elixir de façon assez naturelle. En quoi Elixir est-il différent, me demanderez-vous ? Pour...

Écrire une thread pool en Ruby

10/01/2019

Pouvoir exécuter plusieurs tâches en parallèle, que ce soit dans un script ou une application, peut être vraiment très utile, surtout dans le cas où le traitement de ces tâches peut être très long....

Translation temporelle

31/05/2018

Cette semaine, je me suis essayé à un nouveau format d’article qui se présente sous la forme d’une nouvelle de Science-Fiction. Je tiens en passant à remercier Valentin pour ses illustrations....

Authentifier l'accès à vos ressources avec Dragonfly

11/05/2017

Pour ceux qui ne connaissent pas Dragonfly, c’est une application Rack qui peut être utilisée seule ou via un middleware. Le fait que ce soit une application Rack la rend compatible avec toutes les...