Conteggio efficiente del numero di righe di un file di testo. (200 MB +)


88

Ho appena scoperto che il mio script mi ​​dà un errore fatale:

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

Quella linea è questa:

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

Quindi penso che stia avendo difficoltà a caricare il file in memeory e contare il numero di righe, c'è un modo più efficiente per farlo senza problemi di memoria?

I file di testo di cui ho bisogno per contare il numero di righe vanno da 2 MB a 500 MB. Forse a volte un concerto.

Grazie a tutti per qualsiasi aiuto.

Risposte:


161

Ciò utilizzerà meno memoria, poiché non carica l'intero file in memoria:

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

fclose($handle);

echo $linecount;

fgetscarica una singola riga in memoria (se il secondo argomento $lengthviene omesso, continuerà a leggere dal flusso fino a raggiungere la fine della riga, che è quello che vogliamo). È ancora improbabile che sia veloce come usare qualcosa di diverso da PHP, se ti interessa il wall time e l'utilizzo della memoria.

L'unico pericolo con questo è se le righe sono particolarmente lunghe (cosa succede se si incontra un file da 2 GB senza interruzioni di riga?). In tal caso, faresti meglio a mangiarlo in pezzi e contare i caratteri di fine riga:

$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
non perfetto: potresti avere un file in stile unix ( \n) analizzato su una macchina Windows ( PHP_EOL == '\r\n')
nickf

1
Perché non migliorare un po 'limitando la lettura della riga a 1? Dato che vogliamo solo contare il numero di righe, perché non fare un fgets($handle, 1);?
Cyril N.

1
@CyrilN. Dipende dalla tua configurazione. Se hai principalmente file che contengono solo alcuni caratteri per riga, potrebbe essere più veloce perché non è necessario utilizzarli substr_count(), ma se hai linee molto lunghe devi chiamare while()e fgets()molto di più causando uno svantaggio. Non dimenticare: fgets() non legge riga per riga. Legge solo la quantità di caratteri che hai definito $lengthe se contiene un'interruzione di riga interrompe qualsiasi cosa $lengthsia stata impostata.
mgutt

3
Questo non restituirà 1 in più rispetto al numero di righe? while(!feof())ti farà leggere una riga in più, perché l'indicatore EOF non è impostato fino a quando non provi a leggere alla fine del file.
Barmar

1
@DominicRodger nel primo esempio credo $line = fgets($handle);potrebbe essere solo fgets($handle);perché $linenon viene mai utilizzato.
Tasche e

107

L'uso di un ciclo di fgets()chiamate è una soluzione eccellente e la più semplice da scrivere, tuttavia:

  1. anche se internamente il file viene letto utilizzando un buffer di 8192 byte, il codice deve comunque chiamare quella funzione per ogni riga.

  2. è tecnicamente possibile che una singola riga possa essere più grande della memoria disponibile se stai leggendo un file binario.

Questo codice legge un file in blocchi di 8 kB ciascuno e quindi conta il numero di nuove righe all'interno di quel blocco.

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

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

    fclose($f);

    return $lines;
}

Se la lunghezza media di ogni riga è al massimo 4kB, inizierai già a risparmiare sulle chiamate di funzione e queste possono sommarsi quando elabori file di grandi dimensioni.

Prova delle prestazioni

Ho eseguito un test con un file da 1 GB; ecco i risultati:

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

Il tempo è misurato in secondi in tempo reale, guarda qui cosa significa reale


Curioso quanto sarà più veloce (?) Se estendi la dimensione del buffer a qualcosa come 64k. PS: se solo php avesse un modo semplice per rendere IO asincrono in questo caso
zerkms

@zerkms Per rispondere alla tua domanda, con 64 kB di buffer diventa 0,2 secondi più veloce su 1 GB :)
Ja͢ck

3
Fai attenzione a questo benchmark, quale hai eseguito per primo? Il secondo avrà il vantaggio che il file si trova già nella cache del disco, distorcendo notevolmente il risultato.
Oliver Charlesworth

6
@OliCharlesworth sono in media su cinque run, saltando la prima :)
Ja͢ck

1
Questa risposta è fantastica! Tuttavia, IMO, deve testare quando c'è qualche carattere nell'ultima riga per aggiungere 1 nel conteggio delle righe: pastebin.com/yLwZqPR2
caligari

48

Soluzione a oggetti orientati semplice

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

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

var_dump($file->key());

Aggiornare

Un altro modo per fare questo è con PHP_INT_MAXin SplFileObject::seekmetodo.

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

echo $file->key() + 1; 

3
La seconda soluzione è ottima e utilizza Spl! Grazie.
Daniele Orlando

2
Grazie ! Questo è davvero fantastico. E più veloce della chiamata wc -l(suppongo a causa del fork), specialmente su file piccoli.
Drasill

Non pensavo che la soluzione sarebbe stata così utile!
Wallace Maxters

2
Questa è di gran lunga la soluzione migliore
Valdrinium

1
Il "tasto () + 1" è corretto? Ho provato e sembra sbagliato. Per un dato file con terminazioni di riga su ogni riga inclusa l'ultima, questo codice mi dà 3998. Ma se faccio "wc" su di esso, ottengo 3997. Se uso "vim", dice 3997L (e non indica che manca EOL). Quindi penso che la risposta "Aggiorna" sia sbagliata.
user9645

37

Se lo stai eseguendo su un host Linux / Unix, la soluzione più semplice sarebbe usare exec()o simile per eseguire il comando wc -l $path. Assicurati solo di aver disinfettato $pathprima per essere sicuro che non sia qualcosa come "/ path / to / file; rm -rf /".


Sono su una macchina Windows! Se lo fossi, penso che sarebbe la soluzione migliore!
Abs

24
@ ghostdog74: Perché, sì, hai ragione. Non è portatile. Ecco perché ho riconosciuto esplicitamente la non portabilità del mio suggerimento anteponendolo alla clausola "Se stai eseguendo questo su un host Linux / Unix ...".
Dave Sherohman

1
Non portabile (sebbene utile in alcune situazioni), ma exec (o shell_exec o system) sono una chiamata di sistema, che sono considerevolmente più lente rispetto alle funzioni integrate in PHP.
Manz

11
@ Manz: Perché, sì, hai ragione. Non è portatile. Ecco perché ho riconosciuto esplicitamente la non portabilità del mio suggerimento anteponendolo alla clausola "Se stai eseguendo questo su un host Linux / Unix ...".
Dave Sherohman

@ DaveSherohman Sì, hai ragione, scusa. IMHO, penso che il problema più importante sia il tempo impiegato in una chiamata di sistema (soprattutto se è necessario utilizzarlo frequentemente)
Manz

32

C'è un modo più veloce che ho scoperto che non richiede il ciclo attraverso l'intero file

solo su sistemi * nix , potrebbe esserci un modo simile su Windows ...

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

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

aggiungi 2> / dev / null per sopprimere "No such file or directory"
Tegan Snyder

$ total_lines = intval (exec ("wc -l '$ file'")); gestirà i nomi dei file con spazi.
pgee70

Grazie pgee70 non l'ho ancora scoperto ma ha senso, ho aggiornato la mia risposta
Andy Braham

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

Sembra che la risposta di @DaveSherohman sopra sia stata pubblicata 3 anni prima di questa
e2-e4

8

Se stai usando PHP 5.5 puoi usare un generatore . Tuttavia , NON funzionerà in nessuna versione di PHP precedente alla 5.5. Da php.net:

"I generatori forniscono un modo semplice per implementare iteratori semplici senza l'overhead o la complessità dell'implementazione di una classe che implementa l'interfaccia 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
La try/ finallynon è strettamente necessaria, PHP chiuderà automaticamente il file per te. Probabilmente dovresti anche menzionare che il conteggio effettivo può essere fatto usando iterator_count(getFiles($file)):)
NikiC

7

Questa è un'aggiunta a Wallace de Souza's soluzione

Salta anche le righe vuote durante il conteggio:

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

Se sei sotto Linux puoi semplicemente fare:

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

Devi solo trovare il comando giusto se stai usando un altro sistema operativo

Saluti


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

Volevo aggiungere una piccola correzione alla funzione sopra ...

in un esempio specifico in cui avevo un file contenente la parola "test", la funzione ha restituito 2 come risultato. quindi avevo bisogno di aggiungere un controllo se fgets restituiva falso o no :)

divertiti :)


1

Basato sulla soluzione di Dominic Rodger, ecco cosa uso (usa wc se disponibile, altrimenti fallback alla soluzione di 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

Il conteggio del numero di righe può essere effettuato tramite i seguenti codici:

<?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

Hai diverse opzioni. Il primo è aumentare la memoria disponibile consentita, che probabilmente non è il modo migliore per fare le cose dato che si afferma che il file può diventare molto grande. L'altro modo è usare fgets per leggere il file riga per riga e incrementare un contatore, il che non dovrebbe causare alcun problema di memoria poiché solo la riga corrente è in memoria in qualsiasi momento.


0

C'è un'altra risposta che ho pensato potrebbe essere una buona aggiunta a questa lista.

Se hai perlinstallato e sei in grado di eseguire cose dalla shell in PHP:

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

Questo dovrebbe gestire la maggior parte delle interruzioni di riga sia da file Unix che da file creati da Windows.

DUE aspetti negativi (almeno):

1) Non è una buona idea avere lo script così dipendente dal sistema su cui è in esecuzione (potrebbe non essere sicuro presumere che Perl e wc siano disponibili)

2) Solo un piccolo errore durante la fuga e hai consegnato l'accesso a un guscio sulla tua macchina.

Come con la maggior parte delle cose che so (o penso di sapere) sulla programmazione, ho ottenuto queste informazioni da qualche altra parte:

Articolo di 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
Si prega di considerare l'aggiunta di almeno alcune parole per spiegare al PO e ad ulteriori lettori di voi che rispondete perché e come risponde alla domanda originale.
β.εηοιτ.βε

0

Uso questo metodo per contare semplicemente quante righe in un file. Qual è lo svantaggio di fare questo versi le altre risposte. Vedo molte righe rispetto alla mia soluzione a due righe. Immagino che ci sia una ragione per cui nessuno lo fa.

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

La soluzione originale era questa. Ma poiché file () carica l'intero file in memoria, questo era anche il problema originale (esaurimento della memoria) quindi no, questa non è una soluzione per la domanda.
Tuim

0

La soluzione multipiattaforma più succinta che memorizza solo una riga alla volta.

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

Sfortunatamente, dobbiamo impostare il READ_AHEADflag altrimenti si iterator_countblocca a tempo indeterminato. Altrimenti, questa sarebbe una battuta.


-1

Solo per contare le linee usa:

$handle = fopen("file","r");
static $b = 0;
while($a = fgets($handle)) {
    $b++;
}
echo $b;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.