17611538698
webmaster@21cto.com

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

资讯 0 3761 2017-11-20 12:02:24

bigview.jpeg

作为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";
 
(to be continue)
 


译者:养乐多
作者:Christopher Pitt
来源:https://www.sitepoint.com/perf ... -php/
 


评论