Contando com eficiência o número de linhas de um arquivo de texto. (200 MB +)

88

Acabei de descobrir que meu script apresenta um erro fatal:

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

Essa linha é esta:

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

Então eu acho que está tendo dificuldade para carregar o arquivo na memória e contar o número de linhas, existe uma maneira mais eficiente de fazer isso sem ter problemas de memória?

Os arquivos de texto de que preciso para contar o número de linhas variam de 2 MB a 500 MB. Talvez um show às vezes.

Obrigado a todos por qualquer ajuda.

Abdômen
fonte

Respostas:

158

Isso usará menos memória, pois não carrega o arquivo inteiro na memória:

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

fclose($handle);

echo $linecount;

fgetscarrega uma única linha na memória (se o segundo argumento $lengthfor omitido, ele continuará lendo o fluxo até chegar ao final da linha, que é o que queremos). É improvável que isso seja tão rápido quanto usar algo diferente do PHP, se você se preocupa com o tempo perdido e também com o uso de memória.

O único perigo com isso é se alguma linha for particularmente longa (e se você encontrar um arquivo de 2 GB sem quebras de linha?). Nesse caso, é melhor engolir em pedaços e contar caracteres de fim de linha:

$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
fonte
5
não é perfeito: você poderia ter um arquivo no estilo Unix ( \n) sendo analisado em uma máquina Windows ( PHP_EOL == '\r\n')
nickf
1
Por que não melhorar um pouco, limitando a leitura da linha a 1? Já que queremos apenas contar o número de linhas, por que não fazer um fgets($handle, 1);?
Cyril N.
1
@CyrilN. Isso depende da sua configuração. Se você tiver principalmente arquivos que contenham apenas alguns caracteres por linha, pode ser mais rápido porque você não precisa usar substr_count(), mas se você tiver linhas muito longas, você precisa chamar while()e fgets()muito mais causando uma desvantagem. Não se esqueça: fgets() não lê linha por linha. Ele lê apenas a quantidade de caracteres que você definiu $lengthe se contiver uma quebra de linha, ele interrompe o $lengthque foi definido.
mgutt
3
Isso não retornará 1 a mais do que o número de linhas? while(!feof())fará com que você leia uma linha extra, porque o indicador EOF não é definido até que você tente ler no final do arquivo.
Barmar
1
@DominicRodger no primeiro exemplo, acredito que $line = fgets($handle);poderia ser apenas fgets($handle);porque $linenunca é usado.
Bolsos e
106

Usar um loop de fgets()chamadas é a solução perfeita e a mais direta de escrever, no entanto:

  1. mesmo que internamente o arquivo seja lido usando um buffer de 8.192 bytes, seu código ainda precisa chamar essa função para cada linha.

  2. é tecnicamente possível que uma única linha seja maior do que a memória disponível se você estiver lendo um arquivo binário.

Este código lê um arquivo em blocos de 8kB cada e conta o número de novas linhas dentro desse bloco.

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

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

    fclose($f);

    return $lines;
}

Se o comprimento médio de cada linha for no máximo 4kB, você já começará a economizar nas chamadas de função, que podem aumentar ao processar arquivos grandes.

Benchmark

Fiz um teste com um arquivo de 1 GB; aqui estão os resultados:

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

O tempo é medido em segundos em tempo real, veja aqui o que significa real

Ja͢ck
fonte
É curioso como será mais rápido (?) Se você estender o tamanho do buffer para algo como 64k. PS: se o php tivesse alguma maneira fácil de tornar o IO assíncrono neste caso
zerkms
@zerkms Para responder à sua pergunta, com buffers de 64kB ele se torna 0,2 segundos mais rápido em 1GB :)
Ja͢ck
3
Tenha cuidado com este benchmark, qual você executou primeiro? O segundo terá a vantagem de o arquivo já estar no cache de disco, distorcendo enormemente o resultado.
Oliver Charlesworth
6
@OliCharlesworth são médias de cinco corridas, ignorando a primeira corrida :)
Ja͢ck
1
Essa resposta é ótima! No entanto, IMO, ele deve testar quando há algum caractere na última linha para adicionar 1 na contagem de linha: pastebin.com/yLwZqPR2
caligari
46

Solução de objeto orientado simples

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

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

var_dump($file->key());

Atualizar

Outra maneira de fazer isso é com PHP_INT_MAXno SplFileObject::seekmétodo.

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

echo $file->key() + 1; 
Wallace Maxters
fonte
3
A segunda solução é ótima e usa Spl! Obrigado.
Daniele Orlando
2
Obrigado ! Isso é, de fato, ótimo. E mais rápido do que chamar wc -l(por causa da bifurcação, suponho), especialmente em arquivos pequenos.
Drasill de
Não pensei que a solução fosse tão útil!
Wallace Maxters
2
Esta é de longe a melhor solução
Valdrinium
1
A "tecla () + 1" está certa? Eu tentei e parece errado. Para um determinado arquivo com terminações de linha em cada linha, incluindo a última, este código me dá 3998. Mas se eu fizer "wc" nele, obtenho 3997. Se eu usar "vim", diz 3997L (e não indica falta EOL). Portanto, acho que a resposta "Atualizar" está errada.
user9645
37

Se você estiver executando em um host Linux / Unix, a solução mais fácil seria usar exec()ou semelhante para executar o comando wc -l $path. Apenas certifique-se de limpar $pathprimeiro para ter certeza de que não é algo como "/ caminho / para / arquivo; rm -rf /".

Dave Sherohman
fonte
Estou em uma máquina Windows! Se fosse, acho que seria a melhor solução!
Abs de
23
@ ghostdog74: Sim, você está certo. Não é portátil. É por isso que reconheci explicitamente a não portabilidade da minha sugestão, precedendo-a com a cláusula "Se você estiver executando em um host Linux / Unix ...".
Dave Sherohman
1
Não portátil (embora útil em algumas situações), mas exec (ou shell_exec ou sistema) são uma chamada de sistema, que são consideravelmente mais lentas em comparação com as funções integradas do PHP.
Manz
10
@Manz: Sim, você está certo. Não é portátil. É por isso que reconheci explicitamente a não portabilidade da minha sugestão, precedendo-a com a cláusula "Se você estiver executando em um host Linux / Unix ...".
Dave Sherohman
@DaveSherohman Sim, você está certo, desculpe. IMHO, acho que a questão mais importante é o tempo gasto em uma chamada de sistema (especialmente se você precisar usar com frequência)
Manz
31

Há uma maneira mais rápida que descobri que não requer o loop por todo o arquivo

apenas em sistemas * nix , pode haver uma maneira semelhante no Windows ...

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

//Get number of lines
$totalLines = intval(exec("wc -l '$file'"));
Andy Braham
fonte
adicione 2> / dev / null para suprimir o "arquivo ou diretório
inexistente
$ total_lines = intval (exec ("wc -l '$ arquivo'")); tratará nomes de arquivos com espaços.
pgee70
Obrigado, pgee70 não descobriu isso ainda, mas faz sentido. Eu atualizei minha resposta
Andy Braham
6
exec('wc -l '.escapeshellarg($file).' 2>/dev/null')
Zheng Kai
Parece que a resposta de @DaveSherohman postada 3 anos antes desta
e2-e4
8

Se estiver usando PHP 5.5, você pode usar um gerador . Porém, isso NÃO funcionará em nenhuma versão do PHP anterior à 5.5. De php.net:

"Os geradores fornecem uma maneira fácil de implementar iteradores simples sem a sobrecarga ou complexidade de implementar uma classe que implemente a interface do 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
Ben Harold
fonte
5
O try/ finallynão é estritamente necessário, o PHP fechará automaticamente o arquivo para você. Você provavelmente também deve mencionar que a contagem real pode ser feita usando iterator_count(getFiles($file)):)
NikiC
7

Esta é uma adição à solução de Wallace de Souza

Ele também pula linhas vazias durante a contagem:

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
fonte
6

Se você estiver no Linux, pode simplesmente fazer:

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

Você só precisa encontrar o comando certo se estiver usando outro sistema operacional

Saudações

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

Eu queria adicionar um pequeno conserto à função acima ...

em um exemplo específico onde eu tinha um arquivo contendo a palavra 'teste', a função retornou 2 como resultado. então eu precisava adicionar uma verificação se fgets retornou falso ou não :)

diverta-se :)

ufk
fonte
1

A contagem do número de linhas pode ser feita pelos seguintes códigos:

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

Você tem várias opções. A primeira é aumentar a memória disponível permitida, o que provavelmente não é a melhor maneira de fazer as coisas, visto que você afirma que o arquivo pode ficar muito grande. A outra maneira é usar fgets para ler o arquivo linha por linha e incrementar um contador, o que não deve causar nenhum problema de memória, já que apenas a linha atual está na memória a qualquer momento.

Yacoby
fonte
0

Há outra resposta que achei que poderia ser uma boa adição a esta lista.

Se você perlinstalou e é capaz de executar coisas do shell em PHP:

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

Isso deve lidar com a maioria das quebras de linha, sejam de arquivos criados pelo Unix ou pelo Windows.

DOIS desvantagens (pelo menos):

1) Não é uma boa ideia ter seu script tão dependente do sistema em que está sendo executado (pode não ser seguro assumir que Perl e wc estão disponíveis)

2) Apenas um pequeno erro ao escapar e você entregou o acesso a um shell em sua máquina.

Como acontece com a maioria das coisas que sei (ou acho que sei) sobre codificação, obtive essas informações de outro lugar:

Artigo de John Reeve

Douglas.Sesar
fonte
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
fonte
5
Considere adicionar pelo menos algumas palavras explicando o OP e para que outros leitores respondam por que e como ele responde à pergunta original.
β.εηοιτ.βε 01 de
0

Com base na solução de Dominic Rodger, aqui está o que eu uso (ele usa wc se disponível, caso contrário, retorna para a solução 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

ling
fonte
0

Eu uso este método para simplesmente contar quantas linhas em um arquivo. Qual é a desvantagem de fazer estes versículos as outras respostas. Estou vendo muitas linhas em oposição à minha solução de duas linhas. Acho que há um motivo pelo qual ninguém faz isso.

$lines = count(file('your.file'));
echo $lines;
kaspirtk1
fonte
A solução original era esta. Mas, como file () carrega todo o arquivo na memória, esse também era o problema original (esgotamento da memória), então não, essa não é uma solução para a questão.
Tuim
0

A solução de plataforma cruzada mais sucinta que armazena apenas uma linha de cada vez.

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

Infelizmente, temos que definir o READ_AHEADsinalizador, caso contrário, iterator_countbloqueia indefinidamente. Caso contrário, isso seria uma linha única.

Quolonel Questions
fonte
-1

Para apenas contar as linhas, use:

$handle = fopen("file","r");
static $b = 0;
while($a = fgets($handle)) {
    $b++;
}
echo $b;
Adeel Ahmad
fonte