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

Posted

技术标签:

【中文标题】有效地计算文本文件的行数。 (200mb+)【英文标题】:Efficiently counting the number of lines of a text file. (200mb+) 【发布时间】:2011-01-10 20:59:03 【问题描述】:

我刚刚发现我的脚本给了我一个致命错误:

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 的行数的文本文件。有时可能是演出。

感谢大家的帮助。

【问题讨论】:

【参考方案1】:

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

【讨论】:

我在windows机器上!如果我是,我认为这将是最好的解决方案! @ghostdog74:为什么,是的,你是对的。它是非便携式的。这就是为什么我通过在它前面加上“如果你在 Linux/Unix 主机上运行它......”这个子句来明确承认我的建议的不可移植性。 不可移植(虽然在某些情况下有用),但 exec(或 shell_exec 或 system)是系统调用,与 PHP 内置函数相比要慢得多。 @Manz:为什么,是的,你是对的。它是非便携式的。这就是为什么我通过在它前面加上“如果你在 Linux/Unix 主机上运行它......”这个子句来明确承认我的建议的不可移植性。 @Manz 在大文件上仍然快 8 倍(或更多)(见 Jack 的回答)。【参考方案2】:

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

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

【讨论】:

不完美:您可以在 Windows 机器 (PHP_EOL == '\r\n') 上解析 unix 样式的文件 (\n) 为什么不通过将行读数限制为 1 来改进一点?既然我们只想计算行数,为什么不做一个fgets($handle, 1); @CyrilN。这取决于您的设置。如果您的大部分文件每行仅包含一些字符,则可能会更快,因为您不需要使用 substr_count(),但如果您的行很长,则需要调用 while()fgets() 很多更造成不利。 不要忘记: fgets() 不会逐行读取。它仅读取您通过 $length 定义的字符数量,并且 如果 它包含一个换行符,它会停止任何已设置的 $length 这不会比行数多返回 1 吗? while(!feof()) 将导致您读取额外的一行,因为直到您尝试在文件末尾读取之后才会设置 EOF 指示符。 @DominicRodger 在第一个示例中我相信$line = fgets($handle); 可能只是fgets($handle);,因为从未使用过$line【参考方案3】:

您有多种选择。第一个是增加允许的可用内存,这可能不是最好的方法,因为您声明文件可能会变得非常大。另一种方法是使用fgets逐行读取文件并增加一个计数器,这根本不会导致任何内存问题,因为任何时候只有当前行在内存中。

【讨论】:

【参考方案4】:
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 :)

玩得开心:)

【讨论】:

【参考方案5】:

我发现有一种更快的方法不需要遍历整个文件

仅在 *nix 系统上,在 windows 上可能有类似的方式...

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

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

【讨论】:

add 2>/dev/null 抑制“没有这样的文件或目录” $total_lines = intval(exec("wc -l '$file'"));将处理带有空格的文件名。 感谢 pgee70 还没有遇到过,但有道理,我更新了我的答案 exec('wc -l '.escapeshellarg($file).' 2>/dev/null') 看起来@DaveSherohman 的答案在此之前 3 年发布【参考方案6】:

如果您使用的是 PHP 5.5,则可以使用 generator。不过,这将在 5.5 之前的任何 PHP 版本中工作。来自 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

【讨论】:

try/finally 不是必须的,PHP 会自动为你关闭文件。您可能还应该提到实际计数可以使用iterator_count(getFiles($file)) :)【参考方案7】:

使用fgets() 调用的循环是很好的解决方案,也是最简单的编写方法,但是:

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

    如果您正在读取二进制文件,单行可能大于可用内存,这在技术上是可能的。

此代码以每个 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   |
+------------+-------------+------------------+---------+

时间以秒为单位实时测量,见here什么是真实的意思

【讨论】:

很好奇如果将缓冲区大小扩展到 64k 之类的速度会有多快(?)。 PS:如果只有 php 有一些 easy 在这种情况下使 IO 异步的方法 @zerkms 要回答您的问题,使用 64kB 缓冲区,在 1GB 上它会快 0.2 秒 :) 小心这个基准,你先运行哪个?第二个将受益于文件已经在磁盘缓存中,从而大大扭曲了结果。 @OliCharlesworth 他们是五次运行的平均值,跳过了第一次运行:) 这个答案很棒!但是,IMO,它必须测试最后一行中是否有某些字符才能在行数中加 1:pastebin.com/yLwZqPR2【参考方案8】:

我认为还有另一个答案可能是对这个列表的一个很好的补充。

如果您安装了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 的访问权限。

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

John Reeve Article

【讨论】:

【参考方案9】:
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>";

【讨论】:

请考虑添加至少一些文字向 OP 解释,并让更多读者回答为什么以及如何回答原始问题。【参考方案10】:

仅计算行数使用:

$handle = fopen("file","r");
static $b = 0;
while($a = fgets($handle)) 
    $b++;

echo $b;

【讨论】:

【参考方案11】:

简单的面向对象解决方案

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

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

var_dump($file->key());

#更新

另一种方法是在SplFileObject::seek 方法中使用PHP_INT_MAX

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

echo $file->key(); 

【讨论】:

第二个解决方案很棒,使用了Spl!谢谢。 谢谢!这确实很棒。而且比调用wc -l 更快(我想是因为分叉),尤其是在小文件上。 优秀的解决方案! 这是迄今为止最好的解决方案 “key() + 1”对吗?我试过了,似乎错了。对于在包括最后一行在内的每一行都有行结尾的给定文件,此代码给我 3998。但如果我在上面执行“wc”,我会得到 3997。如果我使用“vim”,它会显示 3997L(并不表示丢失停产)。所以我认为“更新”的答案是错误的。【参考方案12】:

基于多米尼克罗杰的解决方案, 这是我使用的(如果可用,它使用 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

【讨论】:

【参考方案13】:

这是对Wallace Maxter's 解决方案的补充

计数时也会跳过空行:

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; 

【讨论】:

【参考方案14】:

我使用这种方法来纯粹计算文件中有多少行。与其他答案相比,这样做的缺点是什么。我看到很多行,而不是我的两行解决方案。我猜没有人这样做是有原因的。

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

【讨论】:

原来的解决方案是这样的。但是由于 file() 将整个文件加载到内存中,这也是原始问题(内存耗尽)所以不,这不是问题的解决方案。【参考方案15】:

可以通过以下代码计算行数:

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

【讨论】:

【参考方案16】:

如果你在 linux 下,你可以这样做:

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

如果您使用其他操作系统,您只需找到正确的命令

问候

【讨论】:

【参考方案17】:

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

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

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

【讨论】:

【参考方案18】:

这有点晚了,但是……

这是我使用 \n 分隔每一行的文本日志文件的解决方案。

$data = file_get_contents("myfile.txt"); $numlines = strlen($data) - strlen(str_replace("\n","",$data));

它确实将文件加载到内存中,但不需要循环通过未知数量的行。如果文件大小为 GB,则可能不合适,但对于具有短数据行的较小文件,它对我来说是一种享受。

它只是从文件中删除“\n”,并通过将文件中数据的长度与删除所有换行符后的长度进行比较来比较已删除的数量(“\n”字符在我的情况下) .如果您的分界符是不同的字符,请将“\n”替换为您的分界符。

我知道这不是所有场合的最佳答案,但我发现对于我的目的来说是快速而简单的,因为日志的每一行只有几百个字符,总日志文件不会太大。

【讨论】:

以上是关于有效地计算文本文件的行数。 (200mb+)的主要内容,如果未能解决你的问题,请参考以下文章

如何判断Label中文本的行数

如何有效地读取 LARGE 文本文件中的行数

在 C++ 中计算文本文件中的文本行数时出错

如何有效地计算数据帧的行数? [复制]

读取大文本文件VB6中的行数

Node.js:计算文件中的行数