PHP 读取大文件

3,173 阅读4分钟

前言

在日常业务开发过程中,我们可能有遇到一些文件相关操作,比如读取大文件啦,比如读取文件最后几行啦,这些操作在 shell 中都比较容易实现,那么在 PHP 中我们如何实现呢,下面就让我们来看一下。

方法一 使用 file 函数

直接使用 file 函数将整个文件读入一个数组内,还可以通过 file_get_contents 函数以字符串形式获取文件的内容。

我们来看一下 PHP 官方给的 file 函数的示例:

<?php
// 将一个文件读入数组。本例中通过 HTTP 从 URL 中取得 HTML 源文件。

$lines = file('http://www.example.com/');

// 在数组中循环,显示 HTML 的源文件并加上行号。

foreach ($lines as $line_num => $line) {
    echo "Line #<b>{$line_num}</b> : " . htmlspecialchars($line) . "<br />\n";
}

// 另一个例子将 web 页面读入字符串。参见 file_get_contents()。

$html = implode('', file('http://www.example.com/'));

// 从 PHP 5 开始可以使用可选标记参数
$trimmed = file('somefile.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
?>

我们来看一下这样做有什么问题。file 函数会一次性将文件内容读取到内存中,而 PHP 为了防止一些脚本占用了太多的内存而对每个线程的最大可用内存进行了限制,该限制在 php.ini 中配置,参数为 memory_limit

memory_limit 参数作用是设置了一个脚本允许分配的最大内存量,以字节(bytes)为单位。这有助于防止写得不好的脚本吃掉服务器上所有可用的内存。如果不需要内存限制,请将此指令设置为 -1。默认值为 128M

日常业务开发中,建议不要将该值设置为 -1,否则一旦有哪个进程耗尽了内存,影响了正常业务,那后果就非常严重了。

fgets 逐行读取文件

考虑到一次性读取整个文件会耗费大量的内存,而且假设程序有部分功能是从文件中读取数据,然后去数据库中查找数据,数据库也不可能一次性查找所有数据,因此我们可以分批查询,对应到文件中可以是一行一行读取。

下面一个方法是计算分配给程序内存的峰值的函数;

<?php

    memory_get_peak_usage();

    function formatBytes($bytes, $precision = 2) {
        $units = array("b", "kb", "mb", "gb", "tb");

        $bytes = max($bytes, 0);
        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
        $pow = min($pow, count($units) - 1);

        $bytes /= (1 << (10 * $pow));

        return round($bytes, $precision) . " " . $units[$pow];
    }

    print formatBytes(memory_get_peak_usage());
?>

下面一个函数我们来逐行读取文件,注意最后需要执行 fclose 函数

<?php


    function readTheFile($fileName) {

        $line = [];
        $handle = fopen($fileName, "r");

        while (!feof($handle)) {
            $line[] = ltrim(fgets($handle));
        }

        fclose($handle);

        return $line;
    }

    readTheFile("name.txt");

    require "memory.php";
?>

函数功能

  • memory_get_peak_usage — 返回分配给 PHP 内存的峰值
  • fopen — 打开文件或者 URL
  • feof — 测试文件指针是否到了文件结束的位置
  • fgets — 从文件指针中读取一行
  • fclose — 关闭一个已打开的文件指针

使用生成器 yield

生成器介绍

一个生成器函数看起来像一个普通的函数,不同的是普通函数返回一个值,而一个生成器可以 yield 生成许多它所需要的值。

当一个生成器被调用的时候,它返回一个可以被遍历的对象。当你遍历这个对象的时候(例如通过一个 foreach 循环),PHP 将会在每次需要值的时候调用生成器函数,并在产生一个值之后保存生成器的状态,这样它就可以在需要产生下一个值的时候恢复调用状态。

一旦不再需要产生更多的值,生成器函数可以简单退出,而调用生成器的代码还可以继续执行,就像一个数组已经被遍历完了。

yield 关键字

生成器函数的核心是 yield 关键字。它最简单的调用形式看起来像一个 return 申明,不同之处在于普通 return 会返回值并终止函数的执行,而 yield 会返回一个值给循环调用此生成器的代码并且只是暂停执行生成器函数。

测试代码

<?php


    function yieldCommand($fileName) {

        $line = [];
        $handle = fopen($fileName, "r");

        while (!feof($handle)) {
            yield ltrim(fgets($handle));
        }

        fclose($handle);

    }

    foreach (yieldCommand("name.txt") as $key => $value) {
        echo $value;
    }

    require "memory.php";
?>

优点

  • 可以优化 php 的性能

  • 节省大量的内存

  • 适合大量数据计算

使用 shell 命令读取文件最后 n 行

当我们想获取文件中最后几行数据的时候,我们知道在 shell 中直接使用 tail 命令非常容易获得,那么我们能不能在 PHP 中执行 shell 命令呢?答案是可以的。

在 PHP 中,我们有三个函数可以执行 shell 命令,这三个函数分别为 systemexecpassthru。这三个函数的区别如下:

  • system — 执行外部程序,并且显示输出
  • exec — 执行一个外部程序,不输出
  • passthru — 执行外部程序并且显示原始输出

我们来测试一下:

<?php

    function tailCommand($fileName) {

        $cmd = "tail -n 10 $fileName";

        $result = system($cmd);

        return $result;

    }

    tailCommand("name.txt");

    require "memory.php";
?>

escapeshellarg 函数的功能是把字符串转码为可以在 shell 命令里使用的参数。

参考文档