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

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

[php]
$fichier = « file.csv »;
$data = file($fichier);
foreach($data as $row){
//traitement
}
[/php]

ou bien

[php]
$fichier = « file.xml »;
$data = simplexml_load_file($fichier);
foreach($data->node as $node){
//traitement
}
[/php]

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.

 

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.

[php]
abstract class Parser
{
abstract public function setFile();
abstract public function parse(Closure $callback);
}
[/php]

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

[php]
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;
}
}
[/php]

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.

[php]
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;
}
}
[/php]

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.

[php]
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();
}
[/php]

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).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

[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>
[/xml]

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

[php]
$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)
[/php]

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

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

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

[php]
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);
};
}
[/php]

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.

Vartan Torossian

Recent Posts

Communauté Tech et féminine : Interview avec Helvira de Motiv’her

Elles sont passées où les femmes dans la tech ? Entre le manque de représentation…

4 jours ago

Consommer des APIs HTTP en PHP comme un pro avec Nicolas Grekas.

Dans cette vidéo, on interview Nicolas Grekas, contributeur clé de Symfony, pour discuter de sa…

4 jours ago

Trouver son job grâce à WeLoveDevs.

 Comment trouver son job dans la tech ? Marie a la réponse ! Grâce à…

6 jours ago

Adobe, L’empire créatif.

Adobe, l'empire créatif, et pas des moindres ! Belle ascension de la part de ces…

1 semaine ago

La MAO musique ou musique assistée par ordinateur

Est-ce plus simple de créer des morceaux avec les outils de Musique Assistée par Ordinateur…

1 semaine ago