Compter efficacement le nombre de lignes d'un fichier texte. (200 Mo +)


88

Je viens de découvrir que mon script me donne une erreur fatale:

Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 440 bytes) in C:\process_txt.php on line 109

Cette ligne est la suivante:

$lines = count(file($path)) - 1;

Je pense donc qu'il a du mal à charger le fichier dans la mémoire et à compter le nombre de lignes, y a-t-il un moyen plus efficace de le faire sans avoir de problèmes de mémoire?

Les fichiers texte dont j'ai besoin pour compter le nombre de lignes vont de 2 Mo à 500 Mo. Peut-être un concert parfois.

Merci à tous pour toute aide.

Réponses:


161

Cela utilisera moins de mémoire, car il ne charge pas tout le fichier en mémoire:

$file="largefile.txt";
$linecount = 0;
$handle = fopen($file, "r");
while(!feof($handle)){
  $line = fgets($handle);
  $linecount++;
}

fclose($handle);

echo $linecount;

fgetscharge une seule ligne en mémoire (si le deuxième argument $lengthest omis, il continuera à lire dans le flux jusqu'à ce qu'il atteigne la fin de la ligne, ce que nous voulons). Il est peu probable que cela soit aussi rapide que d'utiliser autre chose que PHP, si vous vous souciez de l'heure du mur ainsi que de l'utilisation de la mémoire.

Le seul danger avec ceci est si des lignes sont particulièrement longues (que faire si vous rencontrez un fichier de 2 Go sans saut de ligne?). Dans ce cas, vous feriez mieux de le faire glisser par blocs et de compter les caractères de fin de ligne:

$file="largefile.txt";
$linecount = 0;
$handle = fopen($file, "r");
while(!feof($handle)){
  $line = fgets($handle, 4096);
  $linecount = $linecount + substr_count($line, PHP_EOL);
}

fclose($handle);

echo $linecount;

5
pas parfait: vous pourriez avoir un fichier de style Unix ( \n) en cours d'analyse sur une machine Windows ( PHP_EOL == '\r\n')
nickf

1
Pourquoi ne pas améliorer un peu en limitant la lecture de ligne à 1? Puisque nous voulons seulement compter le nombre de lignes, pourquoi ne pas faire un fgets($handle, 1);?
Cyril N.17

1
@CyrilN. Cela dépend de votre configuration. Si vous avez principalement des fichiers qui ne contiennent que quelques caractères par ligne, cela pourrait être plus rapide car vous n'avez pas besoin de les utiliser substr_count(), mais si vous avez de très longues lignes, vous devez appeler while()et fgets()bien plus encore causant un désavantage. N'oubliez pas: fgets() ne lit pas ligne par ligne. Il ne lit que le nombre de caractères que vous avez définis $lengthet s'il contient un saut de ligne, il arrête tout ce qui $lengtha été défini.
mgutt

3
Est-ce que cela ne renvoie pas 1 de plus que le nombre de lignes? while(!feof())vous obligera à lire une ligne supplémentaire, car l'indicateur EOF n'est défini qu'après avoir essayé de lire à la fin du fichier.
Barmar le

1
@DominicRodger dans le premier exemple, je crois, $line = fgets($handle);pourrait être simplement fgets($handle);parce qu'il $linen'est jamais utilisé.
Pocketsand

107

Utiliser une boucle d' fgets()appels est la bonne solution et la plus simple à écrire, cependant:

  1. même si le fichier est lu en interne à l'aide d'un tampon de 8192 octets, votre code doit toujours appeler cette fonction pour chaque ligne.

  2. il est techniquement possible qu'une seule ligne soit plus grande que la mémoire disponible si vous lisez un fichier binaire.

Ce code lit un fichier par blocs de 8 Ko chacun, puis compte le nombre de sauts de ligne dans ce bloc.

function getLines($file)
{
    $f = fopen($file, 'rb');
    $lines = 0;

    while (!feof($f)) {
        $lines += substr_count(fread($f, 8192), "\n");
    }

    fclose($f);

    return $lines;
}

Si la longueur moyenne de chaque ligne est au maximum de 4 Ko, vous commencerez déjà à économiser sur les appels de fonction, et ceux-ci peuvent s'additionner lorsque vous traitez de gros fichiers.

Référence

J'ai effectué un test avec un fichier de 1 Go; Voici les résultats:

             +-------------+------------------+---------+
             | This answer | Dominic's answer | wc -l   |
+------------+-------------+------------------+---------+
| Lines      | 3550388     | 3550389          | 3550388 |
+------------+-------------+------------------+---------+
| Runtime    | 1.055       | 4.297            | 0.587   |
+------------+-------------+------------------+---------+

Le temps est mesuré en secondes en temps réel, voyez ici ce que signifie réel


Curieux de savoir à quel point ce sera plus rapide (?) Si vous étendez la taille de la mémoire tampon à quelque chose comme 64k. PS: si seulement php avait un moyen facile de rendre IO asynchrone dans ce cas
zerkms

@zerkms Pour répondre à votre question, avec des tampons de 64 Ko, cela devient 0,2 seconde plus rapide sur 1 Go :)
Ja͢ck

3
Faites attention avec ce benchmark, lequel avez-vous exécuté en premier? Le second aura l'avantage que le fichier se trouve déjà dans le cache disque, faussant massivement le résultat.
Oliver Charlesworth

6
@OliCharlesworth, ce sont des moyennes sur cinq points, sautant la première manche :)
Ja͢ck

1
Cette réponse est géniale! Cependant, l'OMI, il doit tester quand il y a un caractère dans la dernière ligne pour ajouter 1 dans le nombre de lignes: pastebin.com/yLwZqPR2
caligari

48

Solution d'objet orienté simple

$file = new \SplFileObject('file.extension');

while($file->valid()) $file->fgets();

var_dump($file->key());

Mise à jour

Une autre façon de faire est avec PHP_INT_MAXen SplFileObject::seekméthode.

$file = new \SplFileObject('file.extension', 'r');
$file->seek(PHP_INT_MAX);

echo $file->key() + 1; 

3
La deuxième solution est excellente et utilise Spl! Merci.
Daniele Orlando

2
Merci ! C'est vraiment génial. Et plus rapide que d'appeler wc -l(à cause de la fourche je suppose), surtout sur les petits fichiers.
Drasill

Je ne pensais pas que la solution serait si utile!
Wallace Maxters

2
C'est de loin la meilleure solution
Valdrinium

1
La "touche () + 1" est-elle correcte? Je l'ai essayé et semble mal. Pour un fichier donné avec des fins de ligne sur chaque ligne, y compris la dernière, ce code me donne 3998. Mais si je fais "wc" dessus, j'obtiens 3997. Si j'utilise "vim", il dit 3997L (et n'indique pas manquant EOL). Je pense donc que la réponse "Update" est fausse.
user9645

37

Si vous l'exécutez sur un hôte Linux / Unix, la solution la plus simple serait d'utiliser exec()ou similaire pour exécuter la commande wc -l $path. Assurez-vous simplement que vous avez nettoyé d' $pathabord pour être sûr que ce n'est pas quelque chose comme "/ chemin / vers / fichier; rm -rf /".


Je suis sur une machine Windows! Si c'était le cas, je pense que ce serait la meilleure solution!
Abs

24
@ ghostdog74: Pourquoi, oui, vous avez raison. Il n'est pas portable. C'est pourquoi j'ai explicitement reconnu la non-portabilité de ma suggestion en la faisant précéder de la clause "Si vous l'exécutez sur un hôte Linux / Unix ...".
Dave Sherohman

1
Non portable (bien qu'utile dans certaines situations), mais exec (ou shell_exec ou system) est un appel système, qui est considérablement plus lent que les fonctions intégrées de PHP.
Manz

11
@Manz: Eh bien, oui, vous avez raison. Il n'est pas portable. C'est pourquoi j'ai explicitement reconnu la non-portabilité de ma suggestion en la faisant précéder de la clause "Si vous l'exécutez sur un hôte Linux / Unix ...".
Dave Sherohman

@DaveSherohman Oui, vous avez raison, désolé. À mon humble avis, je pense que le problème le plus important est la prise de temps dans un appel système (surtout si vous devez l'utiliser fréquemment)
Manz

32

Il existe un moyen plus rapide que j'ai trouvé qui ne nécessite pas de boucle sur tout le fichier

uniquement sur les systèmes * nix , il pourrait y avoir une manière similaire sur Windows ...

$file = '/path/to/your.file';

//Get number of lines
$totalLines = intval(exec("wc -l '$file'"));

ajouter 2> / dev / null pour supprimer le "No such file or directory"
Tegan Snyder

$ total_lines = intval (exec ("wc -l '$ fichier'")); gérera les noms de fichiers avec des espaces.
pgee70 du

Merci pgee70 n'a pas encore rencontré cela mais a du sens, j'ai mis à jour ma réponse
Andy Braham

6
exec('wc -l '.escapeshellarg($file).' 2>/dev/null')
Zheng Kai

On dirait la réponse de @DaveSherohman ci-dessus postée 3 ans avant celle-ci
e2-e4

8

Si vous utilisez PHP 5.5, vous pouvez utiliser un générateur . Cela ne fonctionnera dans aucune version de PHP avant la version 5.5. Depuis php.net:

"Les générateurs fournissent un moyen simple d'implémenter des itérateurs simples sans la surcharge ou la complexité de l'implémentation d'une classe qui implémente l'interface Iterator."

// This function implements a generator to load individual lines of a large file
function getLines($file) {
    $f = fopen($file, 'r');

    // read each line of the file without loading the whole file to memory
    while ($line = fgets($f)) {
        yield $line;
    }
}

// Since generators implement simple iterators, I can quickly count the number
// of lines using the iterator_count() function.
$file = '/path/to/file.txt';
$lineCount = iterator_count(getLines($file)); // the number of lines in the file

5
Le try/ finallyn'est pas strictement nécessaire, PHP fermera automatiquement le fichier pour vous. Vous devriez probablement également mentionner que le comptage réel peut être fait en utilisant iterator_count(getFiles($file)):)
NikiC

7

Ceci est un complément à la solution de Wallace de Souza

Il saute également les lignes vides lors du comptage:

function getLines($file)
{
    $file = new \SplFileObject($file, 'r');
    $file->setFlags(SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY | 
SplFileObject::DROP_NEW_LINE);
    $file->seek(PHP_INT_MAX);

    return $file->key() + 1; 
}

6

Si vous êtes sous Linux, vous pouvez simplement faire:

number_of_lines = intval(trim(shell_exec("wc -l ".$file_name." | awk '{print $1}'")));

Il vous suffit de trouver la bonne commande si vous utilisez un autre OS

Cordialement


1
private static function lineCount($file) {
    $linecount = 0;
    $handle = fopen($file, "r");
    while(!feof($handle)){
        if (fgets($handle) !== false) {
                $linecount++;
        }
    }
    fclose($handle);
    return  $linecount;     
}

Je voulais ajouter un petit correctif à la fonction ci-dessus ...

dans un exemple spécifique où j'avais un fichier contenant le mot «test», la fonction a renvoyé 2 comme résultat. donc j'avais besoin d'ajouter une vérification si fgets retournait faux ou non :)

s'amuser :)


1

Basé sur la solution de dominic Rodger, voici ce que j'utilise (il utilise wc si disponible, sinon des replis vers la solution de dominic Rodger).

class FileTool
{

    public static function getNbLines($file)
    {
        $linecount = 0;

        $m = exec('which wc');
        if ('' !== $m) {
            $cmd = 'wc -l < "' . str_replace('"', '\\"', $file) . '"';
            $n = exec($cmd);
            return (int)$n + 1;
        }


        $handle = fopen($file, "r");
        while (!feof($handle)) {
            $line = fgets($handle);
            $linecount++;
        }
        fclose($handle);
        return $linecount;
    }
}

https://github.com/lingtalfi/Bat/blob/master/FileTool.php


1

Le comptage du nombre de lignes peut se faire à l'aide des codes suivants:

<?php
$fp= fopen("myfile.txt", "r");
$count=0;
while($line = fgetss($fp)) // fgetss() is used to get a line from a file ignoring html tags
$count++;
echo "Total number of lines  are ".$count;
fclose($fp);
?>

0

Vous avez plusieurs options. La première consiste à augmenter la mémoire disponible autorisée, ce qui n'est probablement pas la meilleure façon de faire les choses étant donné que vous déclarez que le fichier peut devenir très volumineux. L'autre façon consiste à utiliser fgets pour lire le fichier ligne par ligne et incrémenter un compteur, ce qui ne devrait causer aucun problème de mémoire car seule la ligne actuelle est en mémoire à la fois.


0

Il y a une autre réponse qui, à mon avis, pourrait être un bon ajout à cette liste.

Si vous avez perlinstallé et pouvez exécuter des choses à partir du shell en PHP:

$lines = exec('perl -pe \'s/\r\n|\n|\r/\n/g\' ' . escapeshellarg('largetextfile.txt') . ' | wc -l');

Cela devrait gérer la plupart des sauts de ligne que ce soit à partir de fichiers Unix ou Windows créés.

DEUX inconvénients (au moins):

1) Ce n'est pas une bonne idée d'avoir votre script tellement dépendant du système sur lequel il s'exécute (il n'est peut-être pas prudent de supposer que Perl et wc sont disponibles)

2) Juste une petite erreur en vous échappant et vous avez remis l'accès à un shell sur votre machine.

Comme pour la plupart des choses que je sais (ou pense savoir) sur le codage, j'ai obtenu cette information ailleurs:

Article de John Reeve


0
public function quickAndDirtyLineCounter()
{
    echo "<table>";
    $folders = ['C:\wamp\www\qa\abcfolder\',
    ];
    foreach ($folders as $folder) {
        $files = scandir($folder);
        foreach ($files as $file) {
            if($file == '.' || $file == '..' || !file_exists($folder.'\\'.$file)){
                continue;
            }
                $handle = fopen($folder.'/'.$file, "r");
                $linecount = 0;
                while(!feof($handle)){
                    if(is_bool($handle)){break;}
                    $line = fgets($handle);
                    $linecount++;
                  }
                fclose($handle);
                echo "<tr><td>" . $folder . "</td><td>" . $file . "</td><td>" . $linecount . "</td></tr>";
            }
        }
        echo "</table>";
}

5
Veuillez envisager d'ajouter au moins quelques mots expliquant au PO et à d'autres lecteurs de vous répondre pourquoi et comment il répond à la question initiale.
β.εηοιτ.βε

0

J'utilise cette méthode pour compter uniquement le nombre de lignes dans un fichier. Quel est l'inconvénient de faire ce versets les autres réponses. Je vois beaucoup de lignes par opposition à ma solution à deux lignes. Je suppose qu'il y a une raison pour laquelle personne ne fait ça.

$lines = count(file('your.file'));
echo $lines;

La solution originale était la suivante. Mais comme file () charge le fichier entier en mémoire, c'était aussi le problème d'origine (épuisement de la mémoire), donc non, ce n'est pas une solution à la question.
Tuim

0

La solution multiplateforme la plus succincte qui ne met en mémoire tampon qu'une seule ligne à la fois.

$file = new \SplFileObject(__FILE__);
$file->setFlags($file::READ_AHEAD);
$lines = iterator_count($file);

Malheureusement, nous devons définir le READ_AHEADdrapeau sinon iterator_countbloque indéfiniment. Sinon, ce serait un one-liner.


-1

Pour ne compter que les lignes, utilisez:

$handle = fopen("file","r");
static $b = 0;
while($a = fgets($handle)) {
    $b++;
}
echo $b;
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.