web-dev-qa-db-fra.com

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

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 est difficile de charger le fichier dans la mémoire et de compter le nombre de lignes. Existe-t-il un moyen plus efficace de le faire sans problèmes de mémoire?

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

Merci à tous pour toute aide.

80
Abs

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;

fgets charge une seule ligne en mémoire (si le deuxième argument $length est omis, il continuera à lire 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 quelque chose d’autre que PHP, si vous vous souciez du temps passé sur le mur et de l’utilisation de la mémoire.

Le seul danger est que certaines lignes sont particulièrement longues (et si vous rencontriez un fichier de 2 Go sans saut de ligne?). Dans ce cas, vous feriez mieux de le slurper en morceaux 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;
150
Dominic Rodger

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

  1. même si, en interne, le fichier est lu à l'aide d'un tampon de 8 192 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 en morceaux de 8 Ko chacun, puis compte le nombre de nouvelles lignes dans ce morceau.

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 des fichiers volumineux.

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, voir ici ce que signifie réel

101
Ja͢ck

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 cela est avec PHP_INT_MAX dans SplFileObject::seek méthode.

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

echo $file->key() + 1; 
41
Wallace Maxters

Si vous utilisez ceci 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 désinfecté $path _ d'abord pour vous assurer qu'il ne s'agit pas de quelque chose comme "/ path/to/file; rm -rf /".

34
Dave Sherohman

J'ai trouvé un moyen plus rapide qui n'exige pas de boucle dans tout le fichier

niquement sur les systèmes * nix, il pourrait exister une méthode similaire sous Windows ...

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

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

Si vous utilisez PHP 5.5), vous pouvez utiliser un générateur . Ce sera [~ # ~] pas [~ # ~ ] fonctionne dans n’importe quelle version de PHP avant la version 5.5). Depuis php.net:

"Les générateurs offrent un moyen simple d'implémenter des itérateurs simples sans la charge ou la complexité d'implémenter une classe implémentant 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
8
Ben Harold

Ceci est un ajout à la solution Wallace de Souza

Il ignore également les lignes vides lors du décompte:

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; 
}
5
Jani

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

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

Vous devez juste trouver la bonne commande si vous utilisez un autre système d'exploitation

Cordialement

4
elkolotfi

Il est possible de compter le nombre de lignes en utilisant les 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);
?>
1
Santosh Kumar
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 renvoyait 2 à la suite. donc j'avais besoin d'ajouter un chèque si les fgets retournaient faux ou pas :)

s'amuser :)

1
ufk

Il y a une autre réponse que je pensais être un bon ajout à cette liste.

Si vous avez Perl installé et êtes capable d'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 créés par Unix ou Windows.

DEUX inconvénients (au moins):

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

2) Juste une petite erreur d’évasion et vous avez donné l’accès à un Shell sur votre machine.

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

article de John Reeve

0
Douglas.Sesar

Vous avez plusieurs options. La première consiste à augmenter la mémoire disponible, ce qui n'est probablement pas la meilleure façon de faire les choses, sachant que le fichier peut devenir très volumineux. L’autre méthode 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 en cours est en mémoire à un moment donné.

0
Yacoby
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>";
}
0
Yogi Sadhwani

Basé sur la solution de dominic Rodger, voici ce que j’utilise (il utilise wc si disponible, sinon c’est 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

0
ling