如何使用PHP读取大文件(一)_java

作为PHP开发者,我们并需要经常担心内存管理。PHP引擎在后台为我们做了很好的清理工作,执行完上下文就短期释放的Web服务器模型意味着,就算是烂代码也不会产生太久的影响。


没错,有时候我们需要走出这个舒适区,比如当我们想在一个小的VPS上一个大项目中运行Composer,或者我们需要在同样小的服务器上读取大文件时。


测试成功的标准


确保我们的代码优化的唯一方法是测量出现不好的情况。接下来需要我们的应用修复后与另一个测量标准进行比较。换句话说,除非我们知道“解决方案”对我们有多大的帮助,


有两个度量值需要我们关注。一个是CPU使用率,它表示我们程序处理的过程有多快或者有多慢?第二是内存使用率。PHP脚本执行需要多少内存?这些通常是成反比的,这意味着我们要么将负载加到CPU上来卸掉内存的占用,反之亦然。


在一个异步执行模型(比如多进程或多线程PHP应用程序)中,CPU和内存使用是重要之考虑因素。在传统的PHP技术栈中,其中有一个达到服务器的限制时,这些都会成为较重的问题。


实时测量PHP执行时的CPU使用率是不切合实际的。如果你要关注此领域,在CentOS,Ubuntu或Mac上可以使用类似top式的命令,如果使用了Windows,可以考虑切换到Linux,以便使用top。


本篇文章的写作目的,就是要测量内存占用情况。我们将看看在传统PHP脚本中使用了多少内存,然后将执行一些优化策略来进行度量。然后,我希望你能做出一个有营养的选择。


以下是我们用来查看内存占用的函数:


// formatBytes is taken from the php.net documentation


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

}


后面,我们会在正常执行的PHP脚本后使用这些函数,通过它可以看出哪个脚本占用内存最多。


我们要什么


在PHP中有很多方法能够读取文件。会有两种用法情况,可能想同时读取和处理文件数据,根据读取的内容处理输出数据或执行其它操作,也有可能只转换一下数据流,而不需要真正的访问数据。


试想一下,对于第一种情况,我们希望在读取文件时,每1000行做一个独立进程Job。那么在内存中至少要保留10000行,然后把它们传给队列的管理器。


对于第二种情况,如果我们是处理从API返回的特别大的数据,此时需要确保它以压缩方式存储。


这两种情况,都是在存取大尺寸文件。


首先,我们需要了解数据是什么,第二,我们不在乎数据是什么。让我们来探索这些选项。


逐行读取文件


有不少处理文件的函数,我们结合一些原生的文件读取函数来处理。如下代码:



// from memory.php


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



// from reading-files-line-by-line-1.php


function readTheFile($path) {

    $lines = [];

    $handle = fopen($path, "r");


    while(!feof($handle)) {

        $lines[] = trim(fgets($handle));

    }


    fclose($handle);

    return $lines;

}


readTheFile("shakespeare.txt");


require "memory.php";


举例来说,我们正读取莎士比亚全集的文本文件,该文件大小约5.5M,最高内存占用量为12.8M。现在让我们用一个生成器来逐行阅读。


// from reading-files-line-by-line-2.php


function readTheFile($path) {

    $handle = fopen($path, "r");


    while(!feof($handle)) {

        yield trim(fgets($handle));

    }


    fclose($handle);

}


readTheFile("shakespeare.txt");


require "memory.php";


文本文件大小相同,但内存占用最高的峰值却只有393KB。在读取数据之前,我看到了两个空白行,也许可以把文档分成几段。


// from reading-files-line-by-line-3.php


$iterator = readTheFile("shakespeare.txt");


$buffer = "";


foreach ($iterator as $iteration) {

    preg_match("/\n{3}/", $buffer, $matches);


    if (count($matches)) {

        print ".";

        $buffer = "";

    } else {

        $buffer .= $iteration . PHP_EOL;

    }

}


require "memory.php";


猜猜我们现在用多少内存?即便我们把文本文件分成1216个块,我们仍然只使用459KB内存,这让你感到惊讶?鉴于generator的性质,我们需要迭代存储最大文本块的内存是101985个字符。


我已经写过使用生成器的性能提升,以及Nikita Popov's的Iterator库的性能提升,如果你喜欢可以再搜索这些内容。


生成器还有其它用途,但是这对于读取大文件的性能是非常明显的,如果我们读取这样的数据,生成器是最好的方法。


管道操作


还有一种情况,我们并不需要操作数据,只是想把一个文件的数据传递到另一个文件,这通常被称为管道(Pipe。大概是由于我们没有看到管道内部执行到结束,因为它是不透明的)。我们可以使用Stream的方法来实现。


首先编写一个PHP脚本来从一个文件导入到另一个文件,以便我们可以测试内存使用情况:


// from piping-files-1.php

file_put_contents(

    "piping-files-1.txt", file_get_contents("shakespeare.txt")

);


require "memory.php";


毫无意外,这个脚本要比它要复制的文件尺寸稍大的内存来运行,因为它必须读入文件并保存到内存,直至写入新文件。对于小文件,这个是没有问题的。但是一旦处理的是一个大文件,就没有那么大的内存了.....


让我们尝试从一个文件流到另一个文件(或管道):


// from piping-files-2.php

$handle1 = fopen("shakespeare.txt", "r");

$handle2 = fopen("piping-files-2.txt", "w");


stream_copy_to_stream($handle1, $handle2);


fclose($handle1);

fclose($handle2);


require "memory.php";