DEV Community

loading...
Cover image for Lire un gros fichier ligne par ligne en PHP (Array, Itérateur & Générateur)

Lire un gros fichier ligne par ligne en PHP (Array, Itérateur & Générateur)

Quentin Ferrer
I’m a french web developer. I develop in PHP with the popular Symfony framework.
Updated on ・7 min read

Sorry English readers, this post goes to my French followers. Let me know if you would like me to translate this one. In the meantime, you can Google translate it.

Dans une carrière de développeur, nous sommes souvent amenés à travailler avec des fichiers. Que ce soit pour importer des données dans un système ou exporter des données dans un fichier il faudra ouvrir le fichier et effectuer des opérations sur chaque ligne du fichier. Il existe plusieurs façons de lire un fichier ligne par ligne en PHP mais certaines peuvent très vite devenir problématique lorsqu'il s'agit de lire des gros fichiers.

Array

Il existe plusieurs façons de lire un fichier dans un tableau PHP, mais voici celle que je vois le plus souvent dans des projets :

<?php
require_once('utils.php');

$rows = $this->read('input.txt');
foreach($rows as $row) {
    // traiter les données
}
Enter fullscreen mode Exit fullscreen mode
<?php
// utils.php
public function read(string $filename): array
{
    $handle = fopen($filename, "r");

    $rows = [];
    while(!feof($handle)) {
        $rows[] = fgets($handle);
    }

    fclose($handle);

    return $rows;
}
Enter fullscreen mode Exit fullscreen mode

Problèmes

Le temps d'exécution est trop long

Plus le fichier est volumineux, plus le temps d'exécution sera long. Cela est logique car PHP aura besoin de stocker un grand nombre d'éléments et donc générer un très grand tableau.

La limite de mémoire est atteinte

Lorsque le fichier est léger, il n'y a pas de question à se poser, mais lorsque celui-ci contient un gros volume de données, c'est là que ce message d'erreur apparaît :

Fatal Error: Allowed memory size of XXXXX bytes exhausted. 
Enter fullscreen mode Exit fullscreen mode

En effet, dans l'exemple ci-dessus, PHP stocke toutes les données du tableau en mémoire. La taille de votre tableau sera donc limitée par la quantité de mémoire allouée au script définie par la variable memory_limit de PHP. Il m'arrive de voir la valeur de cette variable égale à memory_limit=-1 en production, ce qui signifie que PHP utilisera toute la mémoire disponible sur le serveur ! Je ne vous recommande donc pas cette pratique mais vous propose plutôt d'utiliser un Itérateur ou un Générateur.

Itérateur

Un Itérateur est un patron de conception comportemental qui permet de parcourir les éléments d'une collection de façon séquentielle. Chaque itérateur doit indiquer la façon dont il doit être traversé, et quelles valeurs seront disponibles à chaque itération.

Depuis la version 5, PHP fournit une interface Iterator qui peut être utilisée pour créer des itérateurs personnalisées :

Iterator extends Traversable {
    /* Méthodes */
    abstract public current () : mixed
    abstract public key () : scalar
    abstract public next () : void
    abstract public rewind () : void
    abstract public valid () : bool
}
Enter fullscreen mode Exit fullscreen mode

L'interface déclare les opérations nécessaires au parcours d'une collection :

  • current() : donne la position actuelle
  • key() : donne la clé de l'élement actuel
  • next() : donne le prochain élément de la collection
  • rewind() : recommence la boucle depuis le début
  • valid() : vérifie si la position actuelle est valide.

Il est donc possible de créer un itérateur qui permet de parcourir un fichier ligne par ligne :

<?php

class FileIterator implements \Iterator
{
    protected $fileHandle;
    protected $row;
    protected $lineNumber;

    public function __construct(string $fileName)
    {
        if (!$this->fileHandle = fopen($fileName, 'r')) {
            throw new \RuntimeException('Unable to open file "' . $fileName . '" ');
        }
    }

    /**
     * Revient au début du fichier.
     */
    public function rewind()
    {
        fseek($this->fileHandle, 0);
        $this->row = fgets($this->fileHandle);
        $this->lineNumber = 0;
    }

    /**
     * Vérifie que la fin du fichier n'est pas atteinte.
     */
    public function valid()
    {
        return !feof($this->fileHandle);
    }

    /**
     * Retourne la ligne actuelle.
     */
    public function current()
    {
        return $this->row;
    }

    /**
     * Retourne le numéro de la ligne actuel.
     */
    public function key()
    {
        return $this->lineNumber;
    }

    /**
     * Déplace le pointeur à la ligne suivante.
     */
    public function next()
    {
        if ($this->valid()) {
            $this->row = fgets($this->fileHandle);
            $this->lineNumber++;
        }
    }

    public function __destruct()
    {
        fclose($this->fileHandle);
    }
}
Enter fullscreen mode Exit fullscreen mode

La fonction read() peut donc être simplifiée avec l'utilisation de cet itérateur :

public function read(string $filename): FileIterator
{
    return new FileIterator($filename);
}
Enter fullscreen mode Exit fullscreen mode

Vous pouvez ensuite parcourir l'itérateur avec l'utilisation de foreach :

$rows = $this->read('input.txt');
foreach($rows as $row) {
    // traiter les données
}
Enter fullscreen mode Exit fullscreen mode

Voici l'ordre des opérations exécutées au moment où vous bouclez sur l'itérateur :

  1. Avant la première itération de la boucle, la méthod rewind() est appelée.
  2. Avant chaque itération de la boucle, la méthode valid() est appelée :
    • Si la méthode valid() retourne false, la boucle est terminée.
    • Si la méthode valid() retourne true, les méthodes current() et key() sont appelées.
  3. Le corps de la boucle est évalué.
  4. Après chaque itération de la boucle, la méthode next() est appelée et nous répétons les opérations à partir de l'étape 2 ci-dessus.

À la différence d'un tableau, l'itérateur consommera très peu de mémoire (<1 Mo) car PHP aura besoin de stocker uniquement la position du pointeur et la ligne actuelle du fichier. Les précédentes lignes ne seront pas gardées en mémoire.

Problème

Les itérateurs peuvent être complexes à créer en fonction du type de fichier. Bien qu'il s'agit d'une optimisation au niveau de la mémoire utilisée, le code est plus conséquent pour une simple opération de lecture d'un fichier. Voyons une façon plus simple de créer des itérateurs à l'aide des générateurs PHP.

Genérateur

Depuis la version PHP 5.5, il est aussi possible d'économiser de la mémoire en utilisant des générateurs PHP :

Un générateur permet de parcourir les éléments d'une collection sans avoir à construire un tableau en mémoire pouvant conduire à dépasser la limite de la mémoire ou nécessiter un temps important pour sa génération.

En réalité, un générateur est un objet PHP qui implémente l'interface Iterator. Il permet de simplifier la création des itérateurs en écrivant une seule fonction chargée de construire le tableau, sans se soucier de toutes les autres fonctions de l'itérateur permettant d’obtenir l’entrée courante du tableau ou de savoir si le tableau contient une autre entrée pour continuer son parcours par exemple. Il sera capable de le gérer tout seul.

Pour créer un générateur, il suffit de créer une fonction qui à la place de retourner un tableau, utilisera autant de fois que nécessaire le mot-clé yield pour indiquer à la foncction générateur, les éléments à parcourir au moment où vous bouclerez dessus :

<?php
public function read(string $filename): \Generator
{
    $handle = fopen($filename, "r");

    // $rows = [];
    while(!feof($handle)) {
        // $rows[] = fgets($handle);
        yield fgets($handle);
    }

    fclose($handle);

    // return $rows;
}
Enter fullscreen mode Exit fullscreen mode

Comme vous pouvez le voir, on parcourt toujours les lignes du fichier mais plutôt que de renvoyer le tableau avec toutes les lignes, on utilise le mot-clé yield pour indiquer à PHP les lignes du fichier que le générateur devra parcourir.

Lorsque la fonction read est appelée, elle retourne un objet Generator que l'on peut parcourir avec foreach :

$rows = $this->read('input.txt');
foreach($rows as $row) {
    // traiter les données
}
Enter fullscreen mode Exit fullscreen mode

Lorsque vous parcourez cet objet, PHP appellera les méthodes d'itération de l'objet (rewind(), valid(), key(), current()) chaque fois qu'il a besoin d'une valeur. Ensuite, il sauvegardera le statut du générateur en appelant la méthode next() de l'itérateur au moment où il génère une valeur. Lorsqu'il n'y a plus de valeur à fournir (valid() retourne false), la fonction générateur peut simplement sortir, et le code appelant continuera comme si un tableau n'avait plus de valeur.

Le générateur consommera autant de mémoire qu'un itérateur, c'est à dire très peu.

Problème

À la différence des itérateurs, le générateur est capable d'aller uniquement vers l'avant. Il ne peut donc pas être ré-initialisé une fois que le parcours a commencé. Cela signifie que le même générateur ne peut pas être utilisé à plusieurs reprises : il devra être reconstruit en appelant une nouvelle fois la fonction générateur.

$rows = $this->read('input.txt');
foreach($rows as $row) {
    // traiter les données
}

foreach($rows as $row) {
    // vous ne passerez jamais ici car le générateur a déjà été consommé.
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

La lecture d'un fichier ligne par ligne avec un tableau PHP est à bannir si vous travailler avec des gros fichiers.

Le générateur ne vous permettra pas de lire plusieurs fois le fichier, vous devrez appeler une nouvelle fois la fonction générateur à chaque fois que vous voulez revenir au début du fichier. Si vous devez parcourir une seule fois le fichier, je vous recommande d'utiliser les générateurs qui sont plus simple à créer par rapport à des itérateurs.

L'itérateur est la solution à retenir si vous avez besoin de traiter plusieurs fois les données du fichier. Vous serez capable de revenir au début du fichier à n'importe quel moment.

Ressources

Credits

Image de Jordi Koalitic

Discussion (0)