Contar de forma eficiente el número de líneas de un archivo de texto. (200 MB +)


88

Acabo de descubrir que mi script me da un error fatal:

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

Esa línea es esta:

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

Entonces creo que está teniendo dificultades para cargar el archivo en la memoria y contar el número de líneas, ¿hay alguna manera más eficiente de hacer esto sin tener problemas de memoria?

Los archivos de texto que necesito para contar el número de líneas varían de 2 MB a 500 MB. Quizás un concierto a veces.

Gracias a todos por cualquier ayuda.

Respuestas:


161

Esto usará menos memoria, ya que no carga todo el archivo en la memoria:

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

fclose($handle);

echo $linecount;

fgetscarga una sola línea en la memoria (si $lengthse omite el segundo argumento , seguirá leyendo del flujo hasta que llegue al final de la línea, que es lo que queremos). Aún es poco probable que esto sea tan rápido como usar algo que no sea PHP, si le importa el tiempo de pared y el uso de la memoria.

El único peligro con esto es si alguna línea es particularmente larga (¿qué pasa si encuentra un archivo de 2GB sin saltos de línea?). En cuyo caso, es mejor que lo sorba en trozos y cuente los caracteres de final de línea:

$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
no perfecto: podría tener un archivo de estilo Unix ( \n) siendo analizado en una máquina con Windows ( PHP_EOL == '\r\n')
nickf

1
¿Por qué no mejorar un poco limitando la lectura de la línea a 1? Dado que solo queremos contar el número de líneas, ¿por qué no hacer una fgets($handle, 1);?
Cyril N.

1
@CyrilN. Esto depende de su configuración. Si tiene principalmente archivos que contienen solo algunos caracteres por línea, podría ser más rápido porque no necesita usarlos substr_count(), pero si tiene líneas muy largas, debe llamar while()y fgets()mucho más, lo que causa una desventaja. No lo olvides: fgets() no lee línea por línea. Lee solo la cantidad de caracteres que definió $lengthy si contiene un salto de línea, detiene lo que $lengthse haya configurado.
mgutt

3
¿No devolverá 1 más que el número de líneas? while(!feof())hará que lea una línea adicional, porque el indicador EOF no se establece hasta después de que intente leer al final del archivo.
Barmar

1
@DominicRodger en el primer ejemplo, creo que $line = fgets($handle);podría ser fgets($handle);porque $linenunca se usa.
Pocketsand

107

fgets()Sin embargo, usar un bucle de llamadas es una buena solución y la más sencilla de escribir:

  1. aunque internamente el archivo se lee usando un búfer de 8192 bytes, su código aún tiene que llamar a esa función para cada línea.

  2. Es técnicamente posible que una sola línea sea más grande que la memoria disponible si está leyendo un archivo binario.

Este código lee un archivo en fragmentos de 8kB cada uno y luego cuenta el número de líneas nuevas dentro de ese fragmento.

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 longitud promedio de cada línea es como máximo de 4kB, ya comenzará a ahorrar en llamadas a funciones, y esas pueden sumarse cuando procese archivos grandes.

Punto de referencia

Ejecuté una prueba con un archivo de 1GB; aquí están los resultados:

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

El tiempo se mide en segundos en tiempo real, vea aquí lo que significa real


Es curioso cuán rápido (?) Será si amplía el tamaño del búfer a algo así como 64k. PD: si tan solo php tuviera una manera fácil de hacer IO asincrónico en este caso
zerkms

@zerkms Para responder a su pregunta, con búferes de 64kB se vuelve 0.2 segundos más rápido en 1GB :)
Ja͢ck

3
Tenga cuidado con este punto de referencia, ¿cuál ejecutó primero? El segundo tendrá la ventaja de que el archivo ya está en la caché del disco, lo que sesgará enormemente el resultado.
Oliver Charlesworth

6
@OliCharlesworth tienen promedios de más de cinco carreras, saltándose la primera carrera :)
Ja͢ck

1
¡Esta respuesta es genial! Sin embargo, IMO, debe probar cuando hay algún carácter en la última línea para agregar 1 en el recuento de líneas: pastebin.com/yLwZqPR2
caligari

48

Solución de objeto orientado simple

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

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

var_dump($file->key());

Actualizar

Otra forma de hacer esto es con el método PHP_INT_MAXin SplFileObject::seek.

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

echo $file->key() + 1; 

3
La segunda solución es excelente y usa Spl! Gracias.
Daniele Orlando

2
Gracias ! De hecho, esto es genial. Y más rápido que llamar wc -l(debido a la bifurcación, supongo), especialmente en archivos pequeños.
Drasill

¡No pensé que la solución sería tan útil!
Wallace Maxters

2
Esta es la mejor solución con diferencia
Valdrinium

1
¿La "tecla () + 1" es correcta? Lo intenté y parece mal. Para un archivo dado con terminaciones de línea en cada línea, incluida la última, este código me da 3998. Pero si hago "wc" en él, obtengo 3997. Si uso "vim", dice 3997L (y no indica que falta EOL). Entonces creo que la respuesta "Actualizar" es incorrecta.
user9645

37

Si está ejecutando esto en un host Linux / Unix, la solución más fácil sería usar exec()o similar para ejecutar el comando wc -l $path. Solo asegúrese de haber desinfectado $pathprimero para asegurarse de que no sea algo como "/ ruta / a / archivo; rm -rf /".


¡Estoy en una máquina de Windows! Si lo fuera, creo que sería la mejor solución.
Abs

24
@ ghostdog74: Sí, tienes razón. No es portátil. Es por eso que reconocí explícitamente la no portabilidad de mi sugerencia anteponiéndola con la cláusula "Si está ejecutando esto en un host Linux / Unix ...".
Dave Sherohman

1
No portátil (aunque útil en algunas situaciones), pero exec (o shell_exec o system) son llamadas al sistema, que son considerablemente más lentas en comparación con las funciones integradas de PHP.
Manz

11
@Manz: Sí, tienes razón. No es portátil. Es por eso que reconocí explícitamente la no portabilidad de mi sugerencia anteponiéndola con la cláusula "Si está ejecutando esto en un host Linux / Unix ...".
Dave Sherohman

@DaveSherohman Sí, tienes razón, lo siento. En mi humilde opinión, creo que el problema más importante es el tiempo que consume una llamada al sistema (especialmente si necesita usarla con frecuencia)
Manz

32

Hay una forma más rápida que encontré que no requiere recorrer todo el archivo

solo en sistemas * nix , puede haber una forma similar en Windows ...

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

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

agregue 2> / dev / null para suprimir "No existe tal archivo o directorio"
Tegan Snyder

$ total_lines = intval (exec ("wc -l '$ archivo'")); manejará nombres de archivos con espacios.
pgee70

Gracias, pgee70 no se encontró con eso todavía, pero tiene sentido, actualicé mi respuesta
Andy Braham

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

Parece que la respuesta de @DaveSherohman anterior se publicó 3 años antes de esta
e2-e4

8

Si está usando PHP 5.5, puede usar un generador . Sin embargo, esto NO funcionará en ninguna versión de PHP anterior a la 5.5. Desde php.net:

"Los generadores proporcionan una manera fácil de implementar iteradores simples sin la sobrecarga o la complejidad de implementar una clase que implementa la interfaz 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
El try/ finallyno es estrictamente necesario, PHP cerrará automáticamente el archivo por usted. Probablemente también debería mencionar que el conteo real se puede hacer usando iterator_count(getFiles($file)):)
NikiC

7

Esta es una adición a la solución de Wallace de Souza

También omite líneas vacías mientras cuenta:

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 está bajo Linux, simplemente puede hacer:

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

Solo tiene que encontrar el comando correcto si está usando otro sistema operativo

Saludos


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

Quería agregar una pequeña corrección a la función anterior ...

en un ejemplo específico en el que tenía un archivo que contenía la palabra 'prueba', la función devolvió 2 como resultado. así que necesitaba agregar un cheque si fgets devuelve falso o no :)

que te diviertas :)


1

Basado en la solución de dominic Rodger, esto es lo que uso (usa wc si está disponible, de lo contrario, es una alternativa a la solución 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

Se puede contar el número de líneas mediante los siguientes 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);
?>

0

Tienes varias opciones. La primera es aumentar la memoria disponible permitida, que probablemente no sea la mejor manera de hacer las cosas, dado que indica que el archivo puede ser muy grande. La otra forma es usar fgets para leer el archivo línea por línea e incrementar un contador, lo que no debería causar ningún problema de memoria ya que solo la línea actual está en la memoria en un momento dado.


0

Hay otra respuesta que pensé que podría ser una buena adición a esta lista.

Si ha perlinstalado y puede ejecutar cosas desde el shell en PHP:

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

Esto debería manejar la mayoría de los saltos de línea, ya sea de archivos creados por Unix o Windows.

DOS desventajas (al menos):

1) No es una buena idea que su script dependa tanto del sistema en el que se está ejecutando (puede que no sea seguro asumir que Perl y wc están disponibles)

2) Solo un pequeño error al escapar y ha entregado el acceso a un caparazón en su máquina.

Como con la mayoría de las cosas que sé (o creo que sé) sobre codificación, obtuve esta información de otro lugar:

Artículo 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
Considere agregar al menos algunas palabras que expliquen el OP y para que otros lectores respondan por qué y cómo responde a la pregunta original.
β.εηοιτ.βε

0

Utilizo este método para simplemente contar cuántas líneas en un archivo. ¿Cuál es la desventaja de hacer esto frente a las otras respuestas? Veo muchas líneas en comparación con mi solución de dos líneas. Supongo que hay una razón por la que nadie hace esto.

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

La solución original fue esta. Pero como file () carga todo el archivo en la memoria, este también fue el problema original (agotamiento de la memoria), así que no, esta no es una solución para la pregunta.
Tuim

0

La solución multiplataforma más sucinta que solo almacena una línea a la vez.

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

Desafortunadamente, tenemos que establecer la READ_AHEADbandera de lo contrario se iterator_countbloquea indefinidamente. De lo contrario, esto sería una sola línea.


-1

Para solo contar las líneas, use:

$handle = fopen("file","r");
static $b = 0;
while($a = fgets($handle)) {
    $b++;
}
echo $b;
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.