类似linux提供wc -l 命令一样,统计文本行数.

注意点:

  1. 文件大小
  2. 内存大小

准备工作

php版本:7.4
操作系统: Macos10.15.7
配置: 16GB 3.1 GHz 双核Intel Core i7

生成文本:

// 生成1k随机文本
dd if=/dev/random of=1k.txt bs=1k count=1
// 1m
dd if=/dev/random of=1mb.txt bs=1m count=1
// 100MB
dd if=/dev/random of=100m.txt bs=1m count=100
// 1G的文本
dd if=/dev/random of=1g.txt bs=1m count=1024
// 4G
dd if=/dev/random of=4g.txt bs=1g count=4
// 10G
dd if=/dev/random of=10g.txt bs=1g count=10

如下
-w633

测试代码

<?php declare(strict_types=1);
    ini_set('memory_limit','-1');
    Trace::Start();
    Trace::Log('前'); // 统计内存和时间
    // 运行代码
    $lines=1000;
    var_dump($lines);

    Trace::Log('后');
    Trace::End();
?>

对比调用wc命令:

function wc($file){
    $command = 'wc -l '.realpath($file).' | awk \'{print $1}\' ';
    return intval(shell_exec($command))+1;
}

参考内存和时间:
-w638

ver_dump 内存占用:1.35MB 耗时0.0001秒 ,后面以这个为参考标准.

计算执行时间:
time php wc.php

php wc.php  0.07s user 0.06s system 95% cpu 0.138 total

参考wc命令执行结果. 注意wc -l 统计比实际少1行.

方案1:

使用file函数.
file() :把整个文件按行读入一个数组中.
优点: 代码量少
缺点: 占用内存比较高

function countLine1(string $file):int{
    return count(file($file));
}
  • 1k文件

wc -l data/1k.txt
-w647
2行,耗时: 0.003

time php wc.php
-w554

3行,耗时: 0.136,内存:2MB

  • 1MB

[wc]
-w609
4000行,耗时0.005秒

[php]
-w583
4000行,0.133秒,内存4MB ,1MB文件可以接受范围内.

结论,内存128MB可使用50MB文件.时间在200毫秒左右.
如果大于1大于内存就不适合了.

第二种方法

fget: 逐行读取.
注意: fget的默认最大长度为1024字节,在长度范围内如果遇到换行符就返回,如果超过1024字节没有换行符就会新起一行.

function countLine2(string $file):int{
    $count=0;
    try {
        $fp = fopen($file, 'r');
        while (!feof($fp)){
            $buff=fgets($fp,1024);
            if($buff!==false && $buff[-1]=="\n"){
                $count++;
            }
        }

    }
    finally {
        if(is_resource($fp)){
            fclose($fp);
        }
        return $count;
    }
}

执行效率:
-w598
1MB文件,4000行,耗时0.13秒,内存2MB

-w609
1G文件,耗时1.093秒,内存2MB

-w641
4G文件,与wc相差1秒.

结论: 无论多大的文件都不会占用太多内存,但性能比wc差.
主要是按行切割,寻找换行符.

第三种方案

使用fread分块读取,然后在substr_count统计换行符数量. 根据内容

function countLine3($file)
{
    $i = 0;
    try {
        $fp = fopen($file, "r");

        while (!feof($fp)) {
            if ($data = fread($fp, 1024 * 100)) {
                $num = substr_count($data, "\n");
                $i += $num;
            }
        }
        fclose($fp);
    }finally {
        if(is_resource($fp)){
            fclose($fp);
        }
    }
    return  $i;
}

1K时
-w661

-w900
1G时,内存一样,耗时减半.

-w1070
与wc -l 相比时间减少1/3.

结论: fread 自定义分段读取内容,然后在统计换行符数量.
每行字符数量很少时,可以一次性读取很多行,而不是逐行读取,在内存中计算那换行数量,减少文件io.

第4种,SplFileObject

第二种基础上,有更简单的标准文件处理.
优缺点:
优点有更少的代码量,不需要打开文件和关闭文件.

逐行扫描,注意必须要current.

function countLine4(string $file){
    $fp=new SplFileObject($file);
    $count=0;
    while (!$fp->eof()){
        $fp->current();
        $count++;
        $fp->next();
    }
    return $count;
}

通过foreach 迭代:

function countLine4(string $file){
    $fp=new SplFileObject($file);
    $count=0;
    foreach($fp as $line){
        $count++;
    }
    return $count;
}

通过内置跳转到最后一行,获取key+1.
seek参数是行,由于不知道最后一行是多大,我们取字节数getSize()或php最大数PHP_INT_MAX,超过实际行数就会跳转到最后一行.

function countLine4(string $file){
    $fp=new SplFileObject($file,'r');
      // 跳转到最大行
    $fp->seek(PHP_INT_MAX);
      // 或者跳到字节数
      // $fp->seek($fp->getSize());
    // 返回当前key,即是行数
    return $fp->key()+1;
}

性能对比

内存2MB,单位秒

方案/大小1kb1MB1G10G
wc -l0.0040.0051.00310.490
file()0.00060.004sxx
fgets()0.00020.00230.9945s12.7513
fread()0.00020.0009s0.40396.7676
SplFileObject::foreach0.00020.0015s1.232913.8505
SplFileObject::next0.00020.00120.997611.7744
SplFileObject::seek0.00020.00080.58887.6388

结论:
性能最好的是 fread和 splFile的seek
最简单的使用,file和splFile的foreach

在对比PHP的性能提升: php8性能提升不明显,0.00几秒,内存占用减少.

建议使用spl方式处理文件.

附加php8.1内存使用.
2023-09-26T08:13:58.png

原作者:阿金
本文地址:https://hi-arkin.com/archives/php-wc.html

标签: php wc file fgets SplFileObject

(本篇完)

评论