Эффективный подсчет количества строк текстового файла. (200 МБ +)

88

Я только что узнал, что мой сценарий выдает фатальную ошибку:

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

Эта строка такая:

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

Итак, я думаю, что у него проблемы с загрузкой файла в память и подсчетом количества строк, есть ли более эффективный способ сделать это без проблем с памятью?

Текстовые файлы, в которых мне нужно подсчитать количество строк, варьируются от 2 МБ до 500 МБ. Может быть, иногда концерт.

Спасибо всем за любую помощь.

Abs
источник

Ответы:

161

Это будет использовать меньше памяти, так как не загружает весь файл в память:

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

fclose($handle);

echo $linecount;

fgetsзагружает одну строку в память (если второй аргумент $lengthопущен, он будет продолжать чтение из потока до тех пор, пока не достигнет конца строки, что мы и хотим). Это вряд ли будет так же быстро, как использование чего-то другого, кроме PHP, если вы заботитесь о времени стены, а также об использовании памяти.

Единственная опасность заключается в том, что какие-либо строки особенно длинные (что, если вы встретите файл размером 2 ГБ без разрывов строк?). В этом случае вам лучше проглотить его по частям и считать символы конца строки:

$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
не идеально: у вас может быть файл в стиле unix ( \n), анализируемый на машине с Windows ( PHP_EOL == '\r\n')
nickf
1
Почему бы не улучшить немного, ограничив чтение строки до 1? Поскольку мы хотим только подсчитать количество строк, почему бы не сделать fgets($handle, 1);?
Кирилл Н.
1
@CyrilN. Это зависит от ваших настроек. Если у вас в основном файлы, содержащие только несколько символов в строке, это может быть быстрее, потому что вам не нужно использовать substr_count(), но если у вас очень длинные строки, вам нужно позвонить while()и fgets()многое другое, что вызывает недостаток. Не забывайте: fgets() не читает построчно. Он читает только количество символов, которые вы определили, $lengthи если он содержит разрыв строки, он останавливает все, $lengthчто было установлено.
mgutt
3
Разве это не вернет на 1 больше, чем количество строк? while(!feof())заставит вас прочитать дополнительную строку, потому что индикатор EOF не устанавливается до тех пор, пока вы не попытаетесь прочитать в конце файла.
Barmar
1
@DominicRodger в первом примере, я считаю, $line = fgets($handle);может быть просто fgets($handle);потому, что $lineникогда не используется.
Pocketsand
107

Использование цикла fgets()вызовов - прекрасное решение, и его проще всего написать:

  1. даже если внутренне файл читается с использованием буфера размером 8192 байта, ваш код все равно должен вызывать эту функцию для каждой строки.

  2. технически возможно, что одна строка может быть больше, чем доступная память, если вы читаете двоичный файл.

Этот код считывает файл фрагментами по 8 КБ каждый, а затем подсчитывает количество новых строк в этом фрагменте.

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

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

    fclose($f);

    return $lines;
}

Если средняя длина каждой строки не превышает 4 КБ, вы уже начнете экономить на вызовах функций, и они могут накапливаться при обработке больших файлов.

Контрольный показатель

Я провел тест с файлом размером 1 ГБ; вот результаты:

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

Время измеряется в секундах в реальном времени, посмотрите здесь, что означает реальное

Джек
источник
Любопытно, насколько быстрее (?) Это будет, если вы увеличите размер буфера примерно до 64 КБ. PS: если бы только у php был какой-то простой способ сделать IO асинхронным в этом случае
zerkms
@zerkms Чтобы ответить на ваш вопрос, с буфером 64 КБ он становится на 0,2 секунды быстрее на 1 ГБ :)
Ja͢ck
3
Будьте осторожны с этим тестом, который вы запускали первым? У второго будет преимущество, заключающееся в том, что файл уже находится в дисковом кеше, что значительно искажает результат.
Оливер Чарльзуорт,
6
@OliCharlesworth, они в среднем набирают пять пробежек, пропуская первую пробежку :)
Як,
1
Отличный ответ! Тем не менее, IMO, он должен проверить, есть ли какой-либо символ в последней строке, чтобы добавить 1 в счетчик строк: pastebin.com/yLwZqPR2
Caligari
48

Простое ориентированное объектное решение

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

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

var_dump($file->key());

Обновить

Другой способ сделать это - использовать метод PHP_INT_MAXin SplFileObject::seek.

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

echo $file->key() + 1; 
Уоллес Макстерс
источник
3
Второе решение отличное и использует Spl! Спасибо.
Даниэле Орландо
2
Спасибо ! Это действительно здорово. И быстрее, чем вызов wc -l(я полагаю, из-за разветвления), особенно для небольших файлов.
Drasill
Не думал, что решение окажется таким полезным!
Wallace Maxters,
2
Это лучшее решение на сегодняшний день
Вальдриний
1
Правильно ли "ключ () + 1"? Я пробовал и кажется ошибся. Для заданного файла с окончанием строки в каждой строке, включая последнюю, этот код дает мне 3998. Но если я использую «wc», я получаю 3997. Если я использую «vim», он говорит 3997L (и не указывает на отсутствие EOL). Поэтому я считаю, что ответ «Обновить» неверен.
user9645,
37

Если вы запускаете это на хосте Linux / Unix, самым простым решением будет использование exec()или аналогичный запуск команды wc -l $path. Просто убедитесь, что вы продезинфицировали $pathсначала, чтобы убедиться, что это не что-то вроде "/ path / to / file; rm -rf /".

Дэйв Шерохман
источник
Я на машине windows! Если бы я был, думаю, это было бы лучшим решением!
Abs
24
@ ghostdog74: Да, ты прав. Он не переносится. Вот почему я недвусмысленно признал непереносимость своего предложения, поставив перед ним пункт «Если вы запускаете это на хосте Linux / Unix ...».
Дэйв Шерохман
1
Непереносимый (хотя и полезный в некоторых ситуациях), но exec (или shell_exec, или system) - это системный вызов, который значительно медленнее по сравнению со встроенными функциями PHP.
Manz
11
@Manz: Да, ты прав. Он не переносится. Вот почему я недвусмысленно признал непереносимость своего предложения, поставив перед ним пункт «Если вы запускаете это на хосте Linux / Unix ...».
Дэйв Шерохман,
@DaveSherohman Да, ты прав, извини. ИМХО, я думаю, что самая важная проблема - это время, затрачиваемое на системный вызов (особенно, если вам нужно часто использовать)
Манц
32

Я обнаружил, что есть более быстрый способ, который не требует перебора всего файла.

только в системах * nix , может быть аналогичный способ в Windows ...

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

//Get number of lines
$totalLines = intval(exec("wc -l '$file'"));
Энди Брэм
источник
добавьте 2> / dev / null, чтобы подавить сообщение «Нет такого файла или каталога»
Теган Снайдер
$ total_lines = intval (exec ("wc -l '$ file'")); будет обрабатывать имена файлов с пробелами.
pgee70
Спасибо, pgee70 еще не сталкивался с этим, но имеет смысл, я обновил свой ответ
Энди Брэм
6
exec('wc -l '.escapeshellarg($file).' 2>/dev/null')
Чжэн Кай
Похоже, ответ @DaveSherohman выше опубликован за 3 года до этого
e2-e4
8

Если вы используете PHP 5.5, вы можете использовать генератор . Однако это НЕ будет работать ни в одной из версий PHP до 5.5. С php.net:

«Генераторы предоставляют простой способ реализации простых итераторов без накладных расходов или сложности, связанных с реализацией класса, реализующего интерфейс 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
try/ finallyНе является строго необходимым, PHP будет автоматически закрывать файл для вас. Вы, вероятно, также должны упомянуть, что фактический подсчет можно сделать с помощью iterator_count(getFiles($file)):)
NikiC
7

Это дополнение к решению Уоллеса де Соуза

Он также пропускает пустые строки при подсчете:

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

Если вы используете Linux, вы можете просто сделать:

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

Вам просто нужно найти правильную команду, если вы используете другую ОС

С уважением

Элколотфи
источник
1
private static function lineCount($file) {
    $linecount = 0;
    $handle = fopen($file, "r");
    while(!feof($handle)){
        if (fgets($handle) !== false) {
                $linecount++;
        }
    }
    fclose($handle);
    return  $linecount;     
}

Я хотел добавить небольшое исправление в функцию выше ...

в конкретном примере, где у меня был файл, содержащий слово «тестирование», функция в результате вернула 2. поэтому мне нужно было добавить проверку, вернул ли fgets false или нет :)

радоваться, веселиться :)

ufk
источник
1

Основываясь на решении Доминика Роджера, вот что я использую (он использует wc, если он доступен, в противном случае - откат к решению Доминика Роджера).

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

Подсчет количества строк можно производить по следующим кодам:

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

У вас есть несколько вариантов. Первый - увеличить разрешенную доступную память, что, вероятно, не лучший способ делать что-то, учитывая, что вы заявляете, что файл может стать очень большим. Другой способ - использовать fgets для чтения файла построчно и увеличения счетчика, что не должно вызывать никаких проблем с памятью, поскольку в любой момент времени в памяти находится только текущая строка.

Якоби
источник
0

Есть еще один ответ, который, по моему мнению, может стать хорошим дополнением к этому списку.

Если вы perlустановили и можете запускать что-то из оболочки на PHP:

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

Это должно обрабатывать большинство разрывов строк, будь то файлы, созданные в Unix или Windows.

ДВА минуса (как минимум):

1) Не рекомендуется иметь ваш скрипт настолько зависимым от системы, в которой он работает (может быть небезопасно предполагать, что Perl и wc доступны)

2) Просто небольшая ошибка при побеге, и вы передали доступ к оболочке на своей машине.

Как и большинство вещей, которые я знаю (или думаю, что знаю) о кодировании, я получил эту информацию откуда-то еще:

Статья Джона Рива

Дуглас Сезар
источник
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
Пожалуйста, подумайте о том, чтобы добавить хотя бы несколько слов, объясняющих OP, и чтобы ваши читатели ответили, почему и как он отвечает на исходный вопрос.
β.εηοιτ.βε 01
0

Я использую этот метод только для подсчета количества строк в файле. Какова обратная сторона этого стиха, другие ответы. Я вижу много строк в отличие от моего двухстрочного решения. Я предполагаю, что есть причина, по которой никто этого не делает.

$lines = count(file('your.file'));
echo $lines;
kaspirtk1
источник
Первоначальное решение было таким. Но поскольку file () загружает весь файл в память, это также была исходная проблема (исчерпание памяти), поэтому нет, это не решение вопроса.
Туим
0

Наиболее емкое кроссплатформенное решение, которое буферизует только одну строку за раз.

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

К сожалению, мы должны установить этот READ_AHEADфлаг, иначе iterator_countблокировка будет бесконечной. В противном случае это было бы однострочным.

Quolonel вопросы
источник
-1

Для подсчета строк используйте:

$handle = fopen("file","r");
static $b = 0;
while($a = fgets($handle)) {
    $b++;
}
echo $b;
Адил Ахмад
источник