Blog

Tuto : comment optimiser le parsage des fichiers grâce aux closures

Capture d’écran 2016-01-15 à 10.17.47
Share Button

Tvart, membre de la communauté de blogueurs JobProd.

Le transfert des flux et la syndication est au coeur de la plupart des sociétés. Les formats les plus courants utilisés sont les csv et xml. Alors quand nous devons traiter des dizaines ou des centaines de fichiers entre 10MB et 10GB il n’est plus question de charger tout le fichier en mémoire. Nous verrons donc comment il est possible en php de réussir ce défi.

La voie du débutant

Quand au quotidien on ne traite qu’un seul fichier on peut se permettre des fantaisies du type

 $fichier = "file.csv";
 $data = file($fichier);
 foreach($data as $row){
  //traitement
 }

ou bien

 $fichier = "file.xml";
 $data = simplexml_load_file($fichier);
 foreach($data->node as $node){
  //traitement
 }

Mais quand le volume des fichiers et la quantité augmentent, il est impératif d’améliorer notre code afin de ne pas charger en mémoire tout le contenu du fichier que nous traitons. La solution c’est de faire un traitement ligne par ligne pour les fichier csv et noeud par noeud pour les fichiers xmls.

 
Je prouve mon niveau tech !
 

La voie du milieu

Pour commencer factorisons le code. Nous savons que quelque soit le format (xml, csv ..) nous aurons besoin au moins de deux méthodes : setFile et parse.

La première permet de spécifier le fichier qui va être parsé, c’est ce que nous allons voir tout de suite. La deuxième méthode on l’abordera plus tard.

abstract class Parser
{
  abstract public function setFile();
  abstract public function parse(Closure $callback);
}

Maintenant mettons en place les classes qui vont s’occuper de chacun des formats en particulier

class ParseCsv extends Parser{
 protected $handle;
 public function setFile($file){
  $this->hanlde = fopen($file, 'r');
  if($this->hanlde == false) {
   throw new Exception(sprintf("Unable to handle file %s", $file));
  }
  return $this;
 }
}

Pour le format XML nous allons introduire trois méthodes supplémentaires.
Les deux premières s’occupent à instancier un objet XMLReader qui permet de
lire un fichier xml en itérant sur les noeuds sans charger en mémoire le contenu
de l’ensemble du fichier.

La troisième méthode spécifie le noeud sur lequel nous allons itérer.

class ParseXml extends Parser{
 protected $xmlr;
 protected $node;
 public function __construct()
 {
  $this->xmlr = new XMLReader();
 }
 public function __destruct(){
  if($this->xmlr){
   $this->xmlr->close();
  }
 }
 public function setNode($node){
  $this->node = $node;
  return $this;
 }
 public function setFile($file){
  libxml_use_internal_errors(true);
  $this->xmlr->open($file);
  $errors = libxml_get_errors();
  if(!empty($errors)) {
    $last_error = libxml_get_last_error();
    throw new \RuntimeException(sprintf("%s %s", $last_error->message,$file));
  }
  libxml_clear_errors();
  return $this;
 }
}

La fin du tunnel

La dernière méthode que nous allons étudier est la méthode parse.
Sa spécificité c’est qu’elle attend en paramètre un objet de type callable qu’on appelle généralement une Closure.

Contrairement à une variable qui retourne la valeur qu’elle contient, une closure retourne une fonction.

    public function parse(Closure $callback)
    {
        while($this->xmlr->read() && $this->xmlr->localName !== $this->node);
        while($this->xmlr->localName == $this->node) {
            //Utilisation du callback ici
            $callback(new SimpleXMLElement($this->xmlr->readOuterXml()));
            if($this->debug){break;}
            $this->xmlr->next($this->node);
        }
        $this->xmlr->close();
    }

Dans ce contexte l’optimisation du traitement du fichier se fait grâce à l’utilisation de XMLReader.
Les avantages qu’apportent l’utilisation de la closure se trouve dans la réutilisation du code.

En effet en injectant une méthode dynamiquement, au lieu d’avoir une méthode définie dans la class XmlParser, nous sommes capable de traiter les données quelque soit l’arbre ou la structure de notre fichier XML (ou csv).

 
Je prouve mon niveau tech !
 

Etude de cas

Supposons le cas d’un portail de vente de livre dont la base de données est alimentée par plusieurs fournisseurs, chacun ayant sont format d’export particulier.

Grace à la possibilité qu’offre les closures à passer des fonctions en paramètres, au lieu de refaire un parseur pour chaque fournisseur, nous allons réutiliser nos parsers génériques en codant seulement le callback particulier pour chaque format.

Exemple:
export.xml

<livres>
<livre id="1"><auteur>Balzac</auteur><titre>La peau de chagrin</titre><annee_pub>1990</annee_pub></livre>
<livre id="2"><auteur>Rimbaud</auteur><titre>Saison en enfer</titre><annee_pub>2006</annee_pub></livre>
<livre id="3"><auteur>Baudelaire</auteur><titre>Les fleurs du mal</titre><annee_pub>2012</annee_pub></livre>
</livres>

Le script d’import ne tient plus qu’en 5 lignes :

$fichier = "export.xml";
$opt = ['un','tableau','avec','quelques','options'];
$parser = (new ParseXml())
              ->setFile($fichier)
              ->node('livre')
              ->parse(//fonction de callback appelée dans la boucle while)

Dans la méthode parse nous avons deux choix. Soit nous écrivons une fonction anonyme directement en paramètre,

$parser->parse(function($data) use($opts){
//Ici s'effectue le traitement sur chaque noeud que nous parcourrons par itération
//en utilisant XMLReader
});

soit nous écrivons une fonction qui à son tour retourne une fonction, et c’est cette première méthode que nous passons en paramètre

public function run(){
 $fichier = "export.xml";
 $opt = ['un','tableau','avec','quelques','options'];
 $parser = (new ParseXml())
               ->setFile($fichier)
               ->node('livre')
               ->parse($this->callback($opts))
}

protected function callback($opts){
 return function($data) use($opts){
  echo sprintf("Id : %d\nAuteur : %s\nTitre : %s\n",$data['id'],$data->auteur,$data->titre);
 };
}

Les sources pour les parseurs (xml et csv) sont disponibles sur le dépôt git suivant

Conclusion

En conclusion, ce qu’il faudrait retenir c’est qu’il est conseillé d’utiliser des méthodes de parsage qui lisent les fichiers lignes par lignes (SplFileObject, fgets, fgetcsv) ou bloc par bloc (XMLReader) au lieu des méthodes comme file, file_get_contents ou simplexml_load_* qui chargent tout le contenu du fichier en mémoire tout au long du traitement.

L’avantage des méthodes de lecture par bloc est la possibilité d’arriver à une optimisation qu’offre les
génératosr ( disponible seulement qu’à partir de la version 5.5 de PHP).

En itérant sur un collback qu’on passe en paramètre, on obtient un traitement dynamique et automatique des flux sans avoir à modifier le moteur, mais
simplement en implémentant une méthode pour le format spécifique.

 
Je prouve mon niveau tech !
 

Share Button

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Vous pouvez utiliser ces balises et attributs HTML : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>