Ефективний підрахунок кількості рядків текстового файлу. (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 МБ. Можливо, іноді концерт.

Дякую всім за будь-яку допомогу.

Відповіді:


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
не ідеально: ви можете \nпроаналізувати файл у стилі unix ( ) на машині Windows ( PHP_EOL == '\r\n')
nickf

1
Чому б не трохи покращитися, обмеживши зчитування рядків до 1? Оскільки ми хочемо підрахувати лише кількість рядків, чому б не зробити a fgets($handle, 1);?
Кирило Н.

1
@CyrilN. Це залежить від вашого налаштування. Якщо у вас є в основному файли, які містять лише деякі символи на рядок, це може бути швидше, тому що вам не потрібно використовувати substr_count(), але якщо у вас дуже довгі рядки, вам потрібно зателефонувати while()і fgets()набагато більше, що створює недолік. Не забувайте: fgets() не читає рядок за рядком. Він читає лише кількість символів, яку ви визначили, $lengthі якщо він містить розрив рядків, він зупиняє все, $lengthщо було встановлено.
mgutt

3
Це не поверне на 1 більше, ніж кількість рядків? while(!feof())змусить вас прочитати додатковий рядок, оскільки індикатор EOF встановлюється лише після спроби прочитати в кінці файлу.
Бармар,

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
КБ

3
Будьте обережні з цим еталоном, який ви запустили першим? Другий отримає перевагу від того, що файл уже знаходиться в кеш-пам’яті диска, що значно скасує результат.
Олівер Чарльзворт

6
@OliCharlesworth вони в середньому за п’ять прогонів, пропускаючи перший запуск :)
Ja͢ck

1
Ця відповідь чудова! Однак, IMO, він повинен перевірити, коли в останньому рядку є якийсь символ, щоб додати 1 до числа рядків: pastebin.com/yLwZqPR2
caligari

48

Рішення простого орієнтованого об’єкта

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

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

var_dump($file->key());

Оновлення

Інший спосіб зробити це з PHP_INT_MAXв SplFileObject::seekметоді.

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

echo $file->key() + 1; 

3
Друге рішення - чудове і використовує Spl! Дякую.
Даніеле Орландо

2
Дякую ! Це, справді, чудово. І швидше, ніж дзвінок wc -l(я вважаю, через розгалуження), особливо для невеликих файлів.
Drasill

Я не думав, що рішення буде настільки корисним!
Уоллес Макстерс

2
Це найкраще рішення на сьогодні
Вальдрініум,

1
Чи правильно "key () + 1"? Я спробував, і здається неправильним. Для даного файлу з закінченнями рядків у кожному рядку, включаючи останній, цей код дає мені 3998. Але якщо я на ньому роблю "wc", я отримую 3997. Якщо я використовую "vim", там пишеться 3997L (і не вказує на відсутність EOL). Тому я вважаю, що відповідь "Оновити" неправильна.
user9645

37

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


Я на машині Windows! Якби я був, я думаю, що це було б найкращим рішенням!
Abs

23
@ ghostdog74: Так, так, ти маєш рацію. Він не є портативним. Ось чому я чітко визнав непереносимість моєї пропозиції, поставивши перед нею пропозицію "Якщо ви запускаєте це на хості Linux / Unix ...".
Dave Sherohman

1
Не портативний (хоч і корисний у деяких ситуаціях), але exec (або shell_exec або system) - це системний виклик, який значно повільніший порівняно з вбудованими функціями PHP.
Манц

10
@Manz: Чому, так, ти маєш рацію. Він не є портативним. Ось чому я чітко визнав непереносимість моєї пропозиції, поставивши перед нею пропозицію "Якщо ви запускаєте це на хості Linux / Unix ...".
Дейв Шерохман

@DaveSherohman Так, ти маєш рацію, вибач. ІМХО, я думаю, що найважливішим питанням є трудомісткість системного дзвінка (особливо якщо вам потрібно часто користуватися)
Манц,

32

Я знайшов більш швидкий спосіб, який не вимагає перегляду всього файлу

лише у системах * nix подібний спосіб може бути і у вікнах ...

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

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

додати 2> / dev / null для придушення "Немає такого файлу або каталогу"
Теган Снайдер

$ total_lines = intval (exec ("wc -l '$ файл'")); буде обробляти імена файлів із пробілами.
pgee70

Дякую pgee70 ще не натрапив на це, але має сенс, я оновив свою відповідь
Andy Braham

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 помилковий чи ні :)

весело :)


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, а також для подальших читачів, які відповідають на питання, чому і як він відповідає на вихідне запитання.
β.εηοιτ.βε

0

Заснований на рішенні Домініка Роджера, ось що я використовую (він використовує туалет, якщо такий є, в іншому випадку - повернення до рішення Домініка Роджера).

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


0

Я використовую цей метод для чистого підрахунку, скільки рядків у файлі. У чому мінус виконання цього вірша інші відповіді. Я бачу багато рядків, на відміну від мого дворядкового рішення. Я здогадуюсь, що є причина, чому ніхто цього не робить.

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

Початкове рішення було таким. Але оскільки файл () завантажує весь файл у пам'ять, це також було первісною проблемою (вичерпання пам'яті), так що ні, це не вирішення питання.
Туїм,

0

Найкоротше міжплатформене рішення, яке буферує лише один рядок за раз.

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

На жаль, ми повинні встановлювати READ_AHEADпрапор, інакше iterator_countблоки нескінченно. Інакше це був би однокласний вкладиш.


-1

Для простого підрахунку рядків використовуйте:

$handle = fopen("file","r");
static $b = 0;
while($a = fgets($handle)) {
    $b++;
}
echo $b;
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.