有效地计算文本文件的行数。(200mb +)


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;

因此,我认为将文件加载到内存中并计算行数有困难,有没有一种更有效的方法可以在没有内存问题的情况下执行此操作?

我需要计算的行数从2MB到500MB的文本文件。有时候也许是演出。

谢谢大家的帮助。

Answers:


161

这将使用较少的内存,因为它不会将整个文件加载到内存中:

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

fclose($handle);

echo $linecount;

fgets将一行加载到内存中(如果$length省略第二个参数,它将继续从流中读取数据,直到到达行尾为止,这正是我们想要的)。如果您关心墙壁时间和内存使用情况,这仍然不太可能像使用PHP以外的其他工具一样快。

唯一的危险是,如果有任何行特别长(如果遇到2GB的文件而没有换行符该怎么办?)。在这种情况下,最好将其分成几大块,然后计算行尾字符:

$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在Windows机器上解析了Unix风格的文件()(PHP_EOL == '\r\n'
nickf,2010年

1
为什么不通过将行读数限制为1来改善一点?由于我们只想计算行数,所以为什么不fgets($handle, 1);呢?
西里尔N.14年

1
@CyrilN。这取决于您的设置。如果大多数文件每行只包含一些字符,则可能会更快,因为您不需要使用substr_count(),但是如果您的行很长,则需要调用while()fgets()这会带来很多不利因素。不要忘记: fgets()不逐行阅读。它只读取字符的您通过定义的数量$length是否包含断行停止一切$length已设置。
mgutt

3
这样返回的行数是否不会比行数多1?while(!feof())会导致您多读一行,因为直到尝试在文件末尾读取后才设置EOF指示器。
Barmar 2015年

1
我相信第一个示例中的@DominicRodger$line = fgets($handle);可能是fgets($handle);因为$line从未使用过。
Pocketsand '16

107

使用fgets()调用循环是一个很好的解决方案,并且最容易编写,但是:

  1. 即使在内部使用8192字节的缓冲区读取文件,您的代码仍然必须为每一行调用该函数。

  2. 从技术上讲,如果您正在读取二进制文件,则单行可能大于可用内存。

这段代码读取每个8kB块的文件,然后计算该块中换行的数量。

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

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

    fclose($f);

    return $lines;
}

如果每行的平均长度最大为4kB,则您将已经开始保存函数调用,并且在处理大文件时这些函数可能会累加起来。

基准测试

我对一个1GB的文件进行了测试;结果如下:

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

时间以秒为单位实时测量,请参阅此处的实际含义


奇怪的是,如果将缓冲区大小扩展到64k,将会有多快(?)。PS:在这种情况下,如果只有php有一些简单的方法可以使IO异步
zerkms 2013年

@zerkms要回答你的问题,具有64kB的缓冲区它为0.2秒1GB :)更快
杰克

3
小心使用此基准测试,您首先运行了哪个?第二个优点是文件已经在磁盘缓存中,从而大大扭曲了结果。
奥利弗·查尔斯沃思

6
@OliCharlesworth他们平均在5次运行,跳过第一次运行:)
杰克

1
这个答案很棒!但是,对于IMO,它必须测试最后一行中是否有某些字符以在行数中添加1:pastebin.com/yLwZqPR2
caligari

48

简单面向对象的解决方案

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

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

var_dump($file->key());

更新资料

另一种方法是使用PHP_INT_MAXinSplFileObject::seek方法。

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

echo $file->key() + 1; 

3
第二个解决方案很棒,并且使用了Spl!谢谢。
Daniele Orlando

2
谢谢 !确实,这很棒。而且比调用wc -l(由于我想的是分叉)要快,尤其是在小文件上。
Drasill '16

我没想到该解决方案会这么有用!
华莱士·麦克特斯

2
这是迄今为止最好的解决方案
Valdrinium '17

1
“ key()+ 1”对吗?我尝试过,似乎不对。对于给定文件,该文件在每行(包括最后一行)都以行尾结尾,那么该代码将为我提供3998。但是,如果在其上执行“ wc”,则会得到3997。如果我使用“ vim”,它将显示为3997L(并不表示缺少) EOL)。因此,我认为“更新”答案是错误的。
user9645

37

如果您是在Linux / Unix主机exec()上运行此命令,则最简单的解决方案是使用或类似命令来运行command wc -l $path。只要确保已$path先进行消毒,以确保它不是“ / path / to / file; rm -rf /”之类的东西。


我在Windows机器上!如果是的话,我认为那将是最好的解决方案!
Abs 2010年

23
@ ghostdog74:为什么,是的,你是对的。它是不可携带的。这就是为什么我通过在子句“如果您正在Linux / Unix主机上运行它……”开头明确表示我的建议不可移植的原因。
Dave Sherohman 2010年

1
非可移植(尽管在某些情况下很有用),但是exec(或shell_exec或system)是系统调用,与PHP内置函数相比,运行起来要慢得多。
Manz 2012年

10
@Manz:为什么,是的,你是对的。它是不可携带的。这就是为什么我通过在子句“如果您正在Linux / Unix主机上运行它……”开头明确表示我的建议不可移植的原因。
Dave Sherohman 2012年

@DaveSherohman是的,你是对的,对不起。恕我直言,我认为最重要的问题是系统调用所花费的时间(尤其是如果您需要经常使用时)
Manz 2012年

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尚未出现,但是很有意义,我更新了答案
Andy Braham

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

看起来像@DaveSherohman的答案在上面的答案发布前3年
e2-e4

8

如果您使用的是PHP 5.5,则可以使用生成器。这不是在PHP的任何版本5.5,虽然之前的工作。从php.net:

“生成器提供了一种简单的方法来实现简单的迭代器,而不会产生实现迭代器接口的类的开销或复杂性。”

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

这是Wallace de Souza解决方案的补充

在计数时,它还会跳过空行:

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;     
}

我想对上面的功能添加一点修复...

在一个特定的示例中,我有一个包含单词“ testing”的文件,结果该函数返回2。所以我需要添加一个检查,如果fgets返回false或不是:)

玩得开心 :)


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的Shell中运行内容:

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

这应该处理大多数换行符,无论是从Unix还是Windows创建的文件。

两个缺点(至少):

1)让脚本如此依赖于其运行的系统不是一个好主意(假设Perl和wc可用可能并不安全)

2)转义中只有一个小错误,您已经将访问权移交给了计算机上的Shell。

就像我了解(或认为知道)有关编码的大多数事情一样,我从其他地方获得了此信息:

约翰·里夫Article


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

基于dominic Rodger的解决方案,这是我使用的(如果可用,它将使用wc,否则会降级到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


0

我仅使用这种方法来计算文件中的行数。这样做的弊端在于其他答案。与两行解决方案相比,我看到了很多行。我猜这是没有人这样做的原因。

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

最初的解决方案是这样。但是由于file()将整个文件加载到内存中,所以这也是原始问题(内存耗尽),所以不是,这不是解决该问题的方法。
Tuim

0

最简洁的跨平台解决方案,一次只能缓冲一行。

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

不幸的是,我们必须设置该READ_AHEAD标志,否则将iterator_count无限期阻塞。否则,这将是单线的。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.