本文转载自https://www.luyuqiang.com/how-php-read-a-large-file
原由
这是偶然间看到的一篇文章,感受收获颇丰,故转载。转载自芦雨强的网络日志php
干货分割线
做为一个PHP开发者,咱们常常须要关注内存管理。PHP引擎在咱们运行脚本以后作了很好的清理工做,短周期执行的web服务器模型意味着即便是烂代码也不会长时间影响。linux
咱们不多须要走出温馨的边界–好比咱们尝试在一个小的VPS上为建立一个大项目运行Composer,或者当咱们在小服务器上读取一个大文件。git
这是后续将在本教程中呈现的问题。github
教程代码能够在github找到web
衡量成功
肯定咱们完善代码的惟一方式是把烂代码和修正过的代码进行比较。换句话说,咱们不知道它是不是解决办法,除非咱们知道它帮了多少。安全
有两个咱们须要关心的指标。第一个是CPU的使用。咱们想要过程快或者慢?第二个是内存的使用。脚本运行使用了多少内存?这些一般是成反比的-意味着咱们能够在看CPU的使用时候,不看内存的使用,反之亦然。服务器
在一个异步程序模型中(好比多进程或者多线程的PHP应用),CPU和内存使用都须要谨慎考虑的。在传统PHP架构中,当它们中的哪一个达到服务器极限的时候一般就会有问题。网络
在PHP中测量CPU使用不切实际。若是你关注,能够考虑在Ubuntu或者MacOs中使用top命令。Windows能够考虑安装一个linux子系统,你就能够在Ubuntu上使用top。多线程
这个教程的目的是测量内存使用。咱们将看到「传统」脚本中内存的使用状况,以后将会优化而且测量,最后我但愿你能够作一个学习后的选择。架构
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
//php.net文档中格式化字节的方法 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]; }
|
咱们将会在脚本的最后使用这个函数,所以能够在第一时间看到哪一个脚本使用了更多的内存。
选项
咱们能够采起不少高效读取文件方法。可是有两种经常使用的场景。咱们能够先读取后处理数据,而后输出处理后的数据或者执行其余操做。咱们可能也想要转换一个数据流而不用获取数据。
对于第一种状况,咱们读取一个文件,而后每一万行建立一个独立的队列进程。咱们须要至少把一万行放到在内存中,而后把他们发送到队列管理器。
对于第二种状况,咱们压缩一个特别大的API响应。咱们不在意它说什么,但咱们须要确保它是以压缩形式备份的。
两种状况下,咱们都须要读取大文件,只不过一个关注数据一个不关注。让咱们探索这些选项吧。。。
一行一行读文件
有不少处理文件的函数。让咱们使用一个简单明了的文件读取:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
// 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());
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
// 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.5MB,消耗了12.8MB的内存。如今,让咱们使用生成器来读取每一行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
// 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的内存。这也说明不了什么,除非咱们使用读取的数据作一些事。假设咱们把文档以每两个空行分红小片断。就像:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
// 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个片断,咱们却只用了458KB的内存,意外吗?鉴于生成器的性质,咱们内存消耗最大的是须要在循环中存储最大文本块的内存。在这种状况下,最大的块是101,985个字符。
我已经写了使用生成器的性能提高和Nikita Popov的生成器库,因此你想要了解更多就去看吧。
生成器也有其余用法,但对读取大文件有很明显的性能提高。若是咱们须要去处理数据,生成器也是最好的方式。
文件间的管道输送
在某些状况下,咱们不须要处理数据,而是把一个文件的数据传递到另外一个文件。这一般被叫作管道输送(大概由于咱们只看到了两头,没看到管道内。。。固然它不是透明的)。咱们能够经过使用流方法获取它们。写了个从一个文件传递到另外一个的脚本,方便咱们能够测量内存使用:
1 2 3 4 5 6 7
|
// from piping-files-1.php
file_put_contents( 'piping-files-1.txt', file_get_contents('shakespeare.txt') );
require 'memory.php';
|
不出意外地,这个脚本使用比文件的拷贝更多的内存。这是由于它不得不读取、把文本内容放到内存中,而后写入到一个新文件。对于小文件还好。可是当咱们处理一个大文件,就不妙了。。。
让咱们使用流的方式从一个文件传递到另外一个(或者叫管道输送)
1 2 3 4 5 6 7 8 9 10 11
|
// 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';
|
这段代码很奇怪。咱们打开两个文件的句柄,第一个使用读模式,第二使用写模式。而后咱们从第一个复制到第二个。而后关闭两个文件的句柄。是否是惊喜到你了,内存只使用了393KB。
这看起来是否是很熟悉。不就是咱们使用生成器的代码一行一行读取而后存储吗?这是由于第二个变量使用fgets指定每行读取多少字节(默认-1或者直到一个新行)
stream_copy_to_stream的第三个参数是彻底相同的参数(具备彻底相同的默认值)。stream_copy_to_stream正在读取一个流,一次一行,并将其写入另外一个流。 它跳过了生成器产生值的部分,由于咱们不须要使用该值。
管道输送这些文本对咱们来讲没用,因此让咱们仔细思考一下其余可能的例子。假设咱们想要从CDN输出一个图像,重定向应用的路由。代码以下:
1 2 3 4 5 6 7 8 9 10 11
|
// from piping-files-3.php
file_put_contents( 'piping-files-3.jpeg', file_get_contents( 'https://github.com/assertchris/uploads/raw/master/rick.jpg' ) );
// ...or write this straight to stdout, if we don't need the memory info
require 'memory.php';
|
咱们可使用以上代码解决一个应用的路由问题。但咱们想从CDN获取而不是把文件存储在本地文件系统中。咱们可能使用更优雅的(像Guzzle)替代file_get_contents,可是效果同样。
图片的内存使用大约581KB。如今,咱们试着使用流替代?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
// from piping-files-4.php
$handle1 = fopen( 'https://github.com/assertchris/uploads/raw/master/rick.jpg', 'r' );
$handle2 = fopen( 'piping-files-4.jpeg', 'w' );
// ...or write this straight to stdout, if we don't need the memory info
stream_copy_to_stream($handle1, $handle2);
fclose($handle1); fclose($handle2);
require 'memory.php';
|
内存使用会略少(400KB),可是结果却同样。若是咱们须要更多的内存信息,咱们能够打印到standard output。事实上,PHP为实现这个提供了简单的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
$handle1 = fopen( 'https://github.com/assertchris/uploads/raw/master/rick.jpg', 'r' );
$handle2 = fopen( 'php://stdout', 'w' );
stream_copy_to_stream($handle1, $handle2);
fclose($handle1); fclose($handle2);
// require 'memory.php';
|
其余流
有一些其余流咱们能够管道传递、读、或者写:
- php://stdin (只读)
- php://stderr (只写, 像 php://stdout)
- php://input (只读) 获取原请求体
- php://output (只写) 能够写到缓冲区
- php://memory 和 php://temp (读写)存储临时数据的地方。php://temp不一样的是以文件存储,php://memory存储在内存
过滤器
还有一个使用流的技巧叫过滤器。它们是中间步骤,提供管理流而不暴露给咱们的功能。设想一下咱们想要压缩莎士比亚.txt。可能会使用Zip扩展:
1 2 3 4 5 6 7 8 9 10
|
// from filters-1.php
$zip = new ZipArchive(); $filename = 'filters-1.zip';
$zip->open($filename, ZipArchive::CREATE); $zip->addFromString('shakespeare.txt', file_get_contents('shakespeare.txt')); $zip->close();
require 'memory.php';
|
整洁的代码,可是却消耗了10.75MB。咱们使用过滤器改进:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
// from filters-2.php
$handle1 = fopen( 'php://filter/zlib.deflate/resource=shakespeare.txt', 'r' );
$handle2 = fopen( 'filters-2.deflated', 'w' );
stream_copy_to_stream($handle1, $handle2);
fclose($handle1); fclose($handle2);
require 'memory.php';
|
能够看到使用php://filter/zlib.defalte的过滤器来压缩资源。咱们能够把一个压缩后的数据管道传递到另外一个文件。内存消耗896KB。
我知道这不是同一个格式,或者使用zip压缩更好。可是你不得不怀疑:若是你选择不一样的格式能够节省掉12倍的内存,何乐而不为呢?
能够经过另外一个zlib的解压缩过滤器解压文件:
1 2 3 4 5
|
// from filters-2.php
file_get_contents( 'php://filter/zlib.inflate/resource=filters-2.deflated' );
|
流已经在理解PHP中的流 和 PHP流与效率中大量说起。若是你想要了解更多,点开看看。
自定义流
fopen和file_get_contents有他们本身的默认设置,可是能够彻底的自定义。为了方便理解,本身建立一个新的流:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
// from creating-contexts-1.php
$data = join('&', [ 'twitter=assertchris', ]);
$headers = join('\r\n', [ 'Content-type: application/x-www-form-urlencoded', 'Content-length: ' . strlen($data), ]);
$options = [ 'http' => [ 'method' => 'POST', 'header'=> $headers, 'content' => $data, ], ];
$context = stream_content_create($options);
$handle = fopen('https://example.com/register', 'r', false, $context); $response = stream_get_contents($handle);
fclose($handle);
|
在这个例子中,咱们尝试向API发出POST请求。API端是安全的,可是仍须要使用http上下文属性(用于http和http)。咱们设置一些头而且打开API文件句柄。考虑到安全,咱们以只读方式打开。
能够自定义不少东西,因此若是你想了解更多,最好查看文档。
自定义协议的过滤器
在本文结束以前,来谈谈自定义协议。 若是你看文档,你能够找到一个示例类来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
Protocol { public resource $context; public construct ( void ) public destruct ( void ) public bool dir_closedir ( void ) public bool dir_opendir ( string $path , int $options ) public string dir_readdir ( void ) public bool dir_rewinddir ( void ) public bool mkdir ( string $path , int $mode , int $options ) public bool rename ( string $path_from , string $path_to ) public bool rmdir ( string $path , int $options ) public resource stream_cast ( int $cast_as ) public void stream_close ( void ) public bool stream_eof ( void ) public bool stream_flush ( void ) public bool stream_lock ( int $operation ) public bool stream_metadata ( string $path , int $option , mixed $value ) public bool stream_open ( string $path , string $mode , int $options , string &$opened_path ) public string stream_read ( int $count ) public bool stream_seek ( int $offset , int $whence = SEEK_SET ) public bool stream_set_option ( int $option , int $arg1 , int $arg2 ) public array stream_stat ( void ) public int stream_tell ( void ) public bool stream_truncate ( int $new_size ) public int stream_write ( string $data ) public bool unlink ( string $path ) public array url_stat ( string $path , int $flags ) }
|
咱们不打算实如今教程中,由于我认为这是值得的本身完成过程。须要作不少工做,可是一旦这个工做完成,能够很容易地注册的流包装:
1 2 3 4 5 6 7
|
if (in_array('highlight-names', stream_get_wrappers())) { stream_wrapper_unregister('highlight-names'); }
stream_wrapper_register('highlight-names', 'HighlightNamesProtocol');
$highlighted = file_get_contents('highlight-names://story.txt');
|
相似地,能够本身建立一个自定义流过滤器。文档有一个过滤器类的例子:
1 2 3 4 5 6 7 8
|
Filter { public $filtername; public $params public int filter ( resource $in , resource $out , int &$consumed , bool $closing ) public void onClose ( void ) public bool onCreate ( void ) }
|
很容易注册:
1 2
|
$handle = fopen('story.txt', 'w+'); stream_filter_append($handle, 'highlight-names', STREAM_FILTER_READ);
|
高亮名字过滤器须要去匹配新的过滤器类的过滤器名属性。也能够在php://filter/highligh-names/resource=story.txt字符串中使用自定义过滤器。定义过滤器比定义协议要容易得多。 其中一个缘由是协议须要处理目录操做,而过滤器只需处理每一个数据块。
若是你有强烈的进取心,鼓励你编写协议的过滤器。若是你能够将过滤器应用于stream_copy_to_stream操做,那么即便处理大容量的大文件,你的应用程序内存也不会超阈值。 试着编写一个调整图像大小的过滤器或加密应用程序的过滤器。
总结
尽管这不是咱们常常处理的问题,在读取大文件时也很容易陷入困境。在异步应用中,当咱们不注意内存使用时,很容易就把整个服务搞挂。
这个教程但愿给你讲解一些新想法(或者唤醒你的记忆),以便你能在读、写大文件时想得更多。当开始熟练掌握流和生成器后,中止使用像file_get_contents函数:一些莫名其妙问题就在程序中消失了。这就是意义所在!