Wydajne liczenie wierszy pliku tekstowego. (200 MB +)

88

Właśnie się dowiedziałem, że mój skrypt wyświetla fatalny błąd:

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

Ta linia jest taka:

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

Więc myślę, że ma trudności z załadowaniem pliku do pamięci i zliczeniem liczby wierszy, czy jest bardziej wydajny sposób, aby to zrobić bez problemów z pamięcią?

Pliki tekstowe, które potrzebuję, aby policzyć liczbę wierszy, mieszczą się w zakresie od 2 MB do 500 MB. Może czasem koncert.

Dziękuję wszystkim za pomoc.

Abs
źródło

Odpowiedzi:

161

Spowoduje to użycie mniej pamięci, ponieważ nie załaduje całego pliku do pamięci:

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

fclose($handle);

echo $linecount;

fgetsładuje pojedynczą linię do pamięci (jeśli drugi argument $lengthzostanie pominięty, będzie kontynuował czytanie ze strumienia, aż osiągnie koniec linii, co jest tym, czego chcemy). Jest to nadal mało prawdopodobne, aby było to tak szybkie, jak użycie czegoś innego niż PHP, jeśli zależy Ci na czasie korzystania z ekranu i wykorzystaniu pamięci.

Jedynym niebezpieczeństwem jest to, że jakieś linie są szczególnie długie (a co, jeśli napotkasz plik 2 GB bez przerw w wierszach?). W takim przypadku lepiej jest siorbać to kawałkami i liczyć znaki końca linii:

$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;
Dominic Rodger
źródło
5
nie jest doskonały: można mieć plik typu UNIX ( \n) są przetwarzane na komputerze z systemem Windows ( PHP_EOL == '\r\n')
nickf
1
Dlaczego nie poprawić trochę, ograniczając odczyt linii do 1? Ponieważ chcemy policzyć tylko liczbę wierszy, dlaczego nie zrobić fgets($handle, 1);?
Cyril N.
1
@CyrilN. To zależy od Twojej konfiguracji. Jeśli masz głównie pliki, które zawierają tylko kilka znaków w linii, może to być szybsze, ponieważ nie musisz ich używać substr_count(), ale jeśli masz bardzo długie linie, musisz wywołać while()i fgets()wiele więcej, co powoduje wady. Nie zapomnij: fgets() nie czyta linia po linii. Czyta tylko liczbę znaków, które zdefiniowałeś, $lengtha jeśli zawiera podział wiersza, zatrzymuje wszystko, co $lengthzostało ustawione.
mgutt
3
Czy to nie zwróci o 1 więcej niż liczba wierszy? while(!feof())spowoduje, że przeczytasz dodatkową linię, ponieważ wskaźnik EOF nie jest ustawiony, dopóki nie spróbujesz odczytać końca pliku.
Barmar
1
@DominicRodger w pierwszym przykładzie, jak sądzę, $line = fgets($handle);może być po prostu fgets($handle);dlatego, że $linenigdy nie jest używany.
Kieszenie i
107

Używanie pętli fgets()wywołań jest dobrym rozwiązaniem i najłatwiejszym do napisania, jednak:

  1. mimo że wewnętrznie plik jest odczytywany przy użyciu bufora o wielkości 8192 bajtów, kod nadal musi wywoływać tę funkcję dla każdej linii.

  2. jest technicznie możliwe, że pojedyncza linia może być większa niż dostępna pamięć, jeśli czytasz plik binarny.

Ten kod odczytuje plik w fragmentach po 8kB każdy, a następnie zlicza liczbę nowych wierszy w tym fragmencie.

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

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

    fclose($f);

    return $lines;
}

Jeśli średnia długość każdej linii wynosi co najwyżej 4 kB, zaczniesz już oszczędzać na wywołaniach funkcji, a te mogą się sumować podczas przetwarzania dużych plików.

Reper

Przeprowadziłem test z plikiem 1 GB; Oto wyniki:

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

Czas mierzony jest w sekundach w czasie rzeczywistym, zobacz tutaj, co oznacza prawdziwy

Jacek
źródło
Ciekawe, jak szybciej (?) Będzie, jeśli zwiększysz rozmiar bufora do około 64k. PS: gdyby tylko php miał jakiś łatwy sposób na uczynienie IO asynchronicznego w tym przypadku
zerkms
@zerkms Odpowiadając na twoje pytanie, z buforami 64kB przyspiesza o 0,2 sekundy przy 1 GB :)
Ja͢ck
3
Uważaj na ten test porównawczy, który sprawdziłeś jako pierwszy? Drugi będzie miał tę zaletę, że plik już znajduje się w pamięci podręcznej dysku, co spowoduje znaczne wypaczenie wyniku.
Oliver Charlesworth
6
@OliCharlesworth to średnie z pięciu przejazdów, pomijając pierwszy bieg :)
Ja͢ck
1
Ta odpowiedź jest świetna! Jednak IMO musi sprawdzić, czy w ostatnim wierszu znajduje się jakiś znak, aby dodać 1 do liczby wierszy: pastebin.com/yLwZqPR2
caligari
48

Rozwiązanie Simple Oriented Object

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

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

var_dump($file->key());

Aktualizacja

Innym sposobem, aby to się PHP_INT_MAXw SplFileObject::seekmetodzie.

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

echo $file->key() + 1; 
Wallace Maxters
źródło
3
Drugie rozwiązanie jest świetne i wykorzystuje Spl! Dzięki.
Daniele Orlando
2
Dziękuję Ci ! To jest rzeczywiście świetne. I szybciej niż dzwonienie wc -l(z powodu rozwidlenia, jak przypuszczam), szczególnie w przypadku małych plików.
Drasill
Nie sądziłem, że rozwiązanie będzie tak pomocne!
Wallace Maxters
2
To zdecydowanie najlepsze rozwiązanie
Valdrinium,
1
Czy „klucz () + 1” jest prawidłowy? Spróbowałem i wydaje się źle. Dla danego pliku z końcówkami linii w każdym wierszu, łącznie z ostatnim, ten kod daje mi 3998. Ale jeśli zrobię na nim "wc", otrzymam 3997. Jeśli użyję "vim", to mówi 3997L (i nie wskazuje brakującego EOL). Myślę więc, że odpowiedź „Aktualizuj” jest błędna.
user9645
37

Jeśli uruchamiasz to na hoście Linux / Unix, najłatwiejszym rozwiązaniem byłoby użycie exec()lub podobnego do uruchomienia polecenia wc -l $path. Po prostu upewnij się, że $pathnajpierw zostałeś oczyszczony, aby upewnić się, że nie jest to coś w rodzaju „/ ścieżka / do / pliku; rm -rf /”.

Dave Sherohman
źródło
Jestem na komputerze z systemem Windows! Gdybym był, myślę, że byłoby to najlepsze rozwiązanie!
Abs
24
@ ghostdog74: Cóż, tak, masz rację. Nie jest przenośny. Dlatego wyraźnie potwierdziłem, że moja sugestia nie jest przenośna, poprzedzając ją klauzulą ​​„Jeśli uruchamiasz to na hoście z systemem Linux / Unix ...”.
Dave Sherohman
1
Nie przenośne (choć przydatne w niektórych sytuacjach), ale exec (lub shell_exec lub system) są wywołaniami systemowymi, które są znacznie wolniejsze w porównaniu do wbudowanych funkcji PHP.
Manz
11
@Manz: Dlaczego, tak, masz rację. Nie jest przenośny. Dlatego wyraźnie potwierdziłem, że moja sugestia nie jest przenośna, poprzedzając ją klauzulą ​​„Jeśli uruchamiasz to na hoście z systemem Linux / Unix ...”.
Dave Sherohman
@DaveSherohman Tak, masz rację, przepraszam. IMHO, myślę, że najważniejszą kwestią jest czasochłonność wywołania systemowego (zwłaszcza jeśli musisz często używać)
Manz
32

Znalazłem szybszy sposób, który nie wymaga przeglądania całego pliku

tylko na systemach * nix , podobnie może być na Windowsie ...

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

//Get number of lines
$totalLines = intval(exec("wc -l '$file'"));
Andy Braham
źródło
dodaj 2> / dev / null, aby ukryć „Brak takiego pliku lub katalogu”
Tegan Snyder
$ total_lines = intval (exec ("wc -l '$ file'")); obsłuży nazwy plików ze spacjami.
pgee70 11.11.13
Dzięki pgee70 jeszcze tego nie spotkałem, ale ma to sens, zaktualizowałem moją odpowiedź
Andy Braham
6
exec('wc -l '.escapeshellarg($file).' 2>/dev/null')
Zheng Kai
Wygląda na to, że odpowiedź od @DaveSherohman powyżej opublikowana 3 lata przed tą
e2-e4
8

Jeśli używasz PHP 5.5, możesz użyć generatora . Będzie to nie działa w każdej wersji PHP przed 5,5 chociaż. Z php.net:

„Generatory zapewniają łatwy sposób implementacji prostych iteratorów bez narzutu i złożoności implementacji klasy, która implementuje interfejs Iteratora”.

// 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
Ben Harold
źródło
5
try/ finallyNie jest to bezwzględnie konieczne, PHP automatycznie zamknie plik dla ciebie. Powinieneś też chyba wspomnieć, że faktyczne liczenie można zrobić za pomocą iterator_count(getFiles($file)):)
NikiC
7

Jest to dodatek do Wallace de Souza rozwiązania

Pomija również puste wiersze podczas liczenia:

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; 
}
Jani
źródło
6

Jeśli korzystasz z Linuksa, możesz po prostu zrobić:

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

Musisz tylko znaleźć odpowiednie polecenie, jeśli używasz innego systemu operacyjnego

pozdrowienia

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

Chciałem trochę poprawić powyższą funkcję ...

w konkretnym przykładzie, w którym miałem plik zawierający słowo „testowanie”, funkcja zwróciła jako wynik 2. więc musiałem dodać sprawdzenie, czy fgets zwróciły fałsz, czy nie :)

baw się dobrze :)

ufk
źródło
1

Liczenie linii można wykonać za pomocą następujących kodów:

<?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);
?>
Santosh Kumar
źródło
0

Masz kilka opcji. Pierwszym jest zwiększenie dostępnej dostępnej pamięci, co prawdopodobnie nie jest najlepszym sposobem robienia rzeczy, biorąc pod uwagę, że plik może być bardzo duży. Innym sposobem jest użycie fgets do odczytywania pliku wiersz po wierszu i zwiększania licznika, co nie powinno powodować żadnych problemów z pamięcią, ponieważ w danym momencie w pamięci znajduje się tylko bieżąca linia.

Yacoby
źródło
0

Jest jeszcze jedna odpowiedź, która moim zdaniem może być dobrym dodatkiem do tej listy.

Jeśli perlzainstalowałeś i możesz uruchamiać rzeczy z powłoki w PHP:

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

Powinno to obsłużyć większość znaków końca wierszy, czy to z plików utworzonych w systemie Unix, czy w systemie Windows.

DWA wady (przynajmniej):

1) Nie jest dobrym pomysłem uzależnienie twojego skryptu od systemu, na którym działa (założenie, że Perl i wc są dostępne, może nie być bezpieczne)

2) Tylko mały błąd w ucieczce i przekazałeś dostęp do powłoki na swoim komputerze.

Podobnie jak w przypadku większości rzeczy, które wiem (lub myślę, że wiem) na temat kodowania, otrzymałem te informacje z innego miejsca:

Artykuł Johna Reeve'a

Douglas.Sesar
źródło
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>";
}
Yogi Sadhwani
źródło
5
Prosimy o rozważenie dodania przynajmniej kilku słów wyjaśniających do PO i aby inni czytelnicy odpowiedzieli, dlaczego i jak odpowiada na pierwotne pytanie.
β.εηοιτ.βε
0

W oparciu o rozwiązanie Dominica Rodgera, oto czego używam (używa wc, jeśli jest dostępne, w przeciwnym razie jest to rozwiązanie zastępcze do rozwiązania Dominica Rodgera).

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

molwa
źródło
0

Używam tej metody do liczenia liczby wierszy w pliku. Jaka jest wada robienia tego w przypadku innych odpowiedzi. Widzę wiele linii w przeciwieństwie do mojego rozwiązania dwuwierszowego. Domyślam się, że jest powód, dla którego nikt tego nie robi.

$lines = count(file('your.file'));
echo $lines;
kaspirtk1
źródło
Oryginalne rozwiązanie było takie. Ale ponieważ file () ładuje cały plik do pamięci, był to również pierwotny problem (wyczerpanie pamięci), więc nie, to nie jest rozwiązanie tego pytania.
Tuim
0

Najbardziej zwięzłe rozwiązanie wieloplatformowe, które buforuje tylko jedną linię naraz.

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

Niestety, musimy ustawić READ_AHEADflagę, w przeciwnym razie iterator_countblokuje się na czas nieokreślony. W przeciwnym razie byłby to jednolinijkowy.

Pytania Quolonel
źródło
-1

Do liczenia wierszy użyj:

$handle = fopen("file","r");
static $b = 0;
while($a = fgets($handle)) {
    $b++;
}
echo $b;
Adeel Ahmad
źródło