Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >如何在不会导致服务器宕机的情况下,用 PHP 读取大文件

如何在不会导致服务器宕机的情况下,用 PHP 读取大文件

作者头像
企鹅号小编
发布于 2018-02-08 03:07:15
发布于 2018-02-08 03:07:15
1.3K0
举报
文章被收录于专栏:企鹅号快讯企鹅号快讯

英文:Christopher Pitt ,译文:oschina

www.oschina.net/translate/performant-reading-big-files-php

作为PHP开发人员,我们并不经常需要担心内存管理。PHP 引擎在我们背后做了很好的清理工作,短期执行上下文的 Web 服务器模型意味着即使是最潦草的代码也不会造成持久的影响。

很少情况下我们可能需要走出这个舒适的地方 ——比如当我们试图在一个大型项目上运行 Composer 来创建我们可以创建的最小的 VPS 时,或者当我们需要在一个同样小的服务器上读取大文件时。

后面的问题就是我们将在本教程中深入探讨的。

GitHub上可以找到本教程的源码。

衡量成功的标准

确保我们对代码有改进的唯一方法是测试一个不好的情况,然后将我们修复之后的测量与另一个进行比较。换句话说,除非我们知道“解决方案”对我们有多大的帮助(如果有的话),否则我们不知道它是否真的是一个解决方案。

这里有两个我们可以关系的衡量标准。首先是CPU使用率。我们要处理的进程有多快或多慢?第二是内存使用情况。脚本执行时需要多少内存?这两个通常是成反比的 – 这意味着我们可以以CPU使用率为代价来降低内存使用,反之亦然。

在一个异步执行模型(如多进程或多线程的PHP应用程序)中,CPU和内存的使用率是很重要的考量因素。在传统的PHP架构中,当任何一个值达到服务器的极限时,这些通常都会成为问题。

测量PHP内的CPU使用率是不切实际的。如果这是你要关注的领域,请考虑在UbuntuMacOS上使用类似top的工具。对于Windows,请考虑使用Linux子系统,以便在Ubuntu中使用top。

为了本教程的目的,我们将测量内存使用情况。我们将看看在“传统”的脚本中使用了多少内存。我们将执行一些优化策略并对其进行度量。最后,我希望你能够做出一个有经验的选择。

我们查看内存使用多少的方法是:

// formatBytes is taken from the php.net documentation

memory_get_peak_usage();

functionformatBytes($bytes,$precision=2){

$units=array("b","kb","mb","gb","tb");

$bytes=max($bytes,);

$pow=floor(($bytes?log($bytes):)/log(1024));

$pow=min($pow,count($units)-1);

$bytes/=(1

returnround($bytes,$precision)." ".$units[$pow];

}

我们将在脚本的最后使用这些函数,以便我们能够看到哪个脚本一次使用最大的内存。

我们的选择是什么?

这里有很多方法可以有效地读取文件。但是也有两种我们可能使用它们的情况。我们想要同时读取和处理所有数据,输出处理过的数据或根据我们所读取的内容执行其他操作。我们也可能想要转换一个数据流,而不需要真正访问的数据。

让我们设想一下,对于第一种情况,我们希望读取一个文件,并且每10,000行创建一个独立排队的处理作业。我们需要在内存中保留至少10000行,并将它们传递给排队的工作管理器(无论采取何种形式)。

对于第二种情况,我们假设我们想要压缩一个特别大的API响应的内容。我们不在乎它的内容是什么,但我们需要确保它是以压缩形式备份的。

在这两种情况下,如果我们需要读取大文件,首先,我们需要知道数据是什么。第二,我们并不在乎数据是什么。让我们来探索这些选择吧…

逐行读取文件

有许多操作文件的函数,我们把部分结合到一个简单的文件阅读器中(封装为一个方法):

// from memory.php

functionformatBytes($bytes,$precision=2){

$units=array("b","kb","mb","gb","tb");

$bytes=max($bytes,);

$pow=floor(($bytes?log($bytes):)/log(1024));

$pow=min($pow,count($units)-1);

$bytes/=(1

returnround($bytes,$precision)." ".$units[$pow];

}

print formatBytes(memory_get_peak_usage());

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

functionreadTheFile($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。现在让我们用一个生成器来读取每一行:

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

functionreadTheFile($path){

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

while(!feof($handle)){

yieldtrim(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($iteratoras$iteration){

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

if(count($matches)){

print".";

$buffer="";

}else{

$buffer.=$iteration.PHP_EOL;

}

}

require"memory.php";

猜到我们使用了多少内存吗?我们把文档分割为1216块,仍然只使用了459KB的内存,这是否让你惊讶?考虑到生成器的性质,我们使用的最多内存是使用在迭代中我们需要存储的最大文本块。在本例中,最大的块为101985字符。

我已经撰写了使用生成器提示性能和Nikita Popov的迭代器库,如果你感兴趣就去看看吧!

生成器还有其它用途,但是最明显的好处就是高性能读取大文件。如果我们需要处理这些数据,生成器可能是最好的方法。

管道间的文件

在我们不需要处理数据的情况下,我们可以把文件数据传递到另一个文件。通常被称为管道(大概是因为我们看不到除了两端的管子里面,当然,它也是不透明的),我们可以通过使用流方法实现。让我们先写一个脚本从一个文件传到另一个文件。这样我们可以测量内存的占用情况:

// 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";

这段代码稍微有点陌生。我们打开了两文件的句柄,第一个是只读模式,第二个是只写模式,然后我们从第一个复制到第二个中。最后我们关闭了它,也许使你惊讶,内存只占用了393KB

这似乎很熟悉。像代码生成器在存储它读到的每一行代码?那是因为第二个参数fgets规定了每行读多少个字节(默认值是-1或者直到下一行为止)。

第三个参数stream_copy_to_stream和第二个参数是同一类参数(默认值相同),stream_copy_to_stream一次从一个数据流里读一行,同时写到另一个数据流里。它跳过生成器只有一个值的部分(因为我们不需要这个值)。

这篇文章对于我们来说可能是没用的,所以让我们想一些我们可能会用到的例子。假设我们想从我们的CDN中输出一张图片,作为一种重定向的路由应用程序。我们可以参照下边的代码来实现它:

// 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获取一个文件,而不是从本地的文件系统获取。我们可以用一些其他的东西来更好的替换file_get_contents(就像Guzzle),即使在引擎内部它们几乎是一样的。

图片的内存大概有581K。现在,让我们来试试这个

// 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";

内存使用明显变少(大概400K),但是结果是一样的。如果我们不关注内存信息,我们依旧可以用标准模式输出。实际上,PHP提供了一个简单的方式来完成:

$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 将一直持存储在内存中直到资源耗尽。

过滤器

还有一个我们可以在stream上使用的技巧,称为过滤器。它们是一种中间的步骤,提供对stream数据的一些控制,但不把他们暴露给我们。想象一下,我们会使用Zip扩展名来压缩我们的shakespeare.txt文件。

// 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左右。使用过滤器的话,我们可以减少内存:

// 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.deflate的过滤器,它读取并压缩资源的内容。我们可以在之后将压缩数据导出到另一个文件中。这仅使用了896KB.

我知道这是不一样的格式,或者制作zip存档是有好处的。你不得不怀疑:如果你可以选择不同的格式并节省约12倍的内存,为什么不选呢?

为了解压此数据,我们可以通过执行另一个zlib filter将压缩后的数据还原:

// from filters-2.php

file_get_contents(

"php://filter/zlib.inflate/resource=filters-2.deflated"

);

Streams have been extensively covered in Stream在“理解PHP中的流”和“U高效使用PHP中的流”中已经被全面介绍了。如果你喜欢一个完全不同的视角,可以阅读一下。

定制流

fopen和file_get_contents有它们自己的一套默认选项,但是这些都是完全可定制的。为了定义它们,我们需要创建一个新的流上下文:

// 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=[

"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和https)。我们设置一些消息头参数,并打开一个文件句柄到API。由于上下文处理写操作,我们可以将句柄打开为只读。

查看文档了解更多。

制定自定义协议和过滤器

在我们结束之前,让我们谈谈制定自定义协议。 如果你查看文档,你可以找到一个示例类来实现:

Protocol{

publicresource$context;

public__construct(void)

public__destruct(void)

publicbooldir_closedir(void)

publicbooldir_opendir(string$path,int$options)

publicstringdir_readdir(void)

publicbooldir_rewinddir(void)

publicboolmkdir(string$path,int$mode,int$options)

publicboolrename(string$path_from,string$path_to)

publicboolrmdir(string$path,int$options)

publicresourcestream_cast(int$cast_as)

publicvoidstream_close(void)

publicboolstream_eof(void)

publicboolstream_flush(void)

publicboolstream_lock(int$operation)

publicboolstream_metadata(string$path,int$option,mixed$value)

publicboolstream_open(string$path,string$mode,int$options,

string&$opened_path)

publicstringstream_read(int$count)

publicboolstream_seek(int$offset,int$whence=SEEK_SET)

publicboolstream_set_option(int$option,int$arg1,int$arg2)

publicarraystream_stat(void)

publicintstream_tell(void)

publicboolstream_truncate(int$new_size)

publicintstream_write(string$data)

publicboolunlink(string$path)

publicarrayurl_stat(string$path,int$flags)

}

我们不打算实现其中的一个,因为我认为它应该有自己的教程。这里有很多工作需要完成。但是一旦这个工作完成,我们可以很容易地注册我们的流包装:

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

同样,也可以创建自定义流过滤器。该文档有一个示例过滤器类:

Filter{

public$filtername;

public$params

publicintfilter(resource$in,resource$out,int&$consumed,

bool$closing)

publicvoidonClose(void)

publicboolonCreate(void)

}

这可以很容易地注册:

$handle=fopen("story.txt","w+");

stream_filter_append($handle,"highlight-names",STREAM_FILTER_READ);

突出显示名称需要匹配新的筛选器类的filtername属性。也可以在php://filter/highligh-names/resource=story.txt字符串中使用自定义过滤器。定义过滤器比定义协议要容易得多。因为协议需要处理目录操作,而过滤器只需处理每个数据块。

如果你有这个想法,我强烈建议你尝试创建自定义协议和过滤器。如果你可以将过滤器应用于stream_copy_to_streamoperations,那么即使在使用大容量文件时,你的应用程序也可以在没有内存的情况下使用。想象一下,编写一个调整大小的图像过滤器或加密的应用程序过滤器。

总结

虽然这不是我们经常遇到的问题,但在处理大文件时很容易搞砸。在异步应用程序中,当我们不注意小心使用内存的话,很容易导致整个服务器宕机。

本教程希望向你介绍一些新的想法(或者让你重新认识他们),以便你可以更多地考虑如何高效地读取和写入大型文件。当我们开始熟悉流程和生成器,并停止使用像file_get_contents这样的函数时,我们的应用程序中就会减少错误的类别,这看起来是很好。

看完本文有收获?请分享给更多人

关注「PHP开发者」,提升PHP技能

本文来自企鹅号 - PHP开发者媒体

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文来自企鹅号 - PHP开发者媒体

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
《PHP技术大全·第1卷》Generator 通俗解读
下面是 Iterator 接口的伪代码(伪代码是指非真实代码,通常用于表达基本的用途)。
猿哥
2019/07/25
5590
关于PHP流不得不说的那些事
相信不少PHP开发者或多或少都见过类似于 "php://input" 或者 "php://output" 这样的内容,很多人都知道这两个的作用一个是接收的 POST 请求中的原始 body 内容,另一个其实和 echo 之类的输出一样是进行输出的。当然,我们的文章内容不会如此的简单,其实类似这样的 php:// 开头的协议还有好几种,它们共同称为 PHPIO流协议(PHP输入/输出流协议) 。
硬核项目经理
2020/05/18
6030
关于PHP流不得不说的那些事
PHP常用函数总结
$x = 5.7; $y = 1.3; // 两个浮点数,x>y 浮点余数 $r = fmod($x, $y); // $r equals 0.5, because 4 * 1.3 + 0.5 = 5.7
V站CEO-西顾
2018/06/12
3.3K1
了解PHP中Stream(流)的概念与用法
Stream是PHP开发里最容易被忽视的函数系列(SPL系列,Stream系列,pack函数,封装协议)之一,但其是个很有用也很重要的函数。Stream可以翻译为“流”,在Java里,流是一个很重要的概念。 流(stream)的概念源于UNIX中管道(pipe)的概念。在UNIX中,管道是一条不间断的字节流,用来实现程序或进程间的通信,或读写外围设备、外部文件等。根据流的方向又可以分为输入流和输出流,同时可以在其外围再套上其它流,比如缓冲流,这样就可以得到更多流处理方法。 PHP里的流和Java里的流实际上
wangxl
2018/03/07
1.9K0
php的各种 I/O流 以及用法
php://协议 首先,我们来说一下一个php提供的协议:"php://" 或许有人看到这个会懵逼,这是什么东东?这有啥用?这咋用?我是谁?我在哪?我要去往何处? 恩,大家可以翻开php手册,搜索一下
仙士可
2019/12/19
1.4K0
php的各种 I/O流 以及用法
[Web安全]PHP伪协议
[Web安全]PHP伪协议 最近php伪协议的各种神奇妙用好像突然又常常提到了,php中支持的伪协议有下面这么多 复制代码 file:// — 访问本地文件系统 http:// — 访问 HTTP(s) 网址 ftp:// — 访问 FTP(s) URLs php:// — 访问各个输入/输出流(I/O streams) zlib:// — 压缩流 data:// — 数据(RFC 2397) glob:// — 查找匹配的文件路径模式 phar:// — PHP 归档 ssh2:// — Secure
安恒网络空间安全讲武堂
2018/02/06
2.5K0
php使用yield解决Fatal error: Allowed memory size of 134217728 bytes exhausted
yield生成器允许你 在 foreach 代码块中写代码来迭代一组数据而不需要在内存中创建一个数组。
崔哥
2022/09/23
4540
3分钟短文 | 大神救我!18G的大文件,PHP咋按行读取?
想逐行读取文件,完全避免把这个文件加载到内存中。如果文件太大(比如 18G),无法在内存中打开,还是要硬来的话,会抛出异常。
程序员小助手
2020/06/19
1.3K0
编程日记:PHP实用函数记录
ignore_user_abort() ,可以实现当客户端关闭后仍然可以执行PHP代码,可保持PHP进程一直在执行,可实现所谓的计划任务功能与持续进程,只需要开启执行脚本,除非 apache等服务器重启或有脚本有输出,该PHP脚本将一直处于执行的状态;
房东的狗丶
2023/02/17
2.7K0
PHP大文件读取操作
简单的文件读取,一般我们会使用 file_get_contents() 这类方式来直接获取文件的内容。不过这种函数有个严重的问题是它会把文件一次性地加载到内存中,也就是说,它会受到内存的限制。因此,加载大文件的时候是绝对不能使用这种方式的。我们还是先看看这种方式加载的例子。
硬核项目经理
2020/06/16
2.7K0
PHP-文件操作
1、文本流有明确的结束符,二进制流没有明确的结束符,通过文件大小判断文件是否读取完毕
cwl_java
2020/03/26
6450
PHP 多任务协程处理
上周 有幸和同事一起在 SilverStripe 分享最近的工作事宜。今天我计划分享 PHP 异步编程,不过由于上周我聊过 ReactPHP;我决定讨论一些不一样的内容。所以本文将探讨多任务协程这方面的内容。
柳公子
2018/09/17
1.3K0
PHP 多任务协程处理
PHP 字符串与文件操作
字符串比较(字节序): 字节序比较可以使用strcmp/strcasecmp两个函数,只需传入两个字符串即可.
王瑞MVP
2022/12/28
8160
PHP对大文件进行读取切割拆分
近期在对项目日志进行分析时,发现日志文件较大,里面的文件行数也较多,使用编辑器进行打开或使用分析工具打开时较慢,于是将其拆分成多个小文件,便于对其进行分析、查看。
申霖
2019/12/27
2K0
PHP-协议流学习
可以访问请求的原始数据的只读流。 POST 请求的情况下,最好使用 php://input 来代替 $HTTP_RAW_POST_DATA,因为它不依赖于特定的 php.ini 指令。
偏有宸机
2020/11/04
1K0
PHP读取大文件【php】
php操作文件一般是file、file_get_contents等此类函数。但是如果处理大文件,这些函数受限于性能和内存,可能就不是那么理想了!
sinnoo
2021/07/27
6.1K0
php面试笔记(7)-php基础知识-文件及目录处理考点
在面试中,考官往往喜欢基础扎实的面试者,而文件及目录处理相关的考点,往往是大家容易忽视的一个点,今天冷月就来帮各位小伙伴们梳理一下,在面试中文件及目录处理相关的注意点。
学长冷月
2020/08/02
7160
基于PHP常用文件函数和目录函数整理
string basename ( string $path [, string $suffix ] ) //给出一个包含有指向一个文件的全路径的字符串,本函数返回基本的文件名。
用户2323866
2021/06/30
7530
PHP 中的文件处理(读取、写入、上传)
文件处理是 Web 开发中常见的需求,几乎所有的 Web 应用都需要与文件进行交互。不论是读取文件、写入文件,还是文件上传,都需要用到 PHP 中的文件操作函数。掌握 PHP 的文件操作功能,可以帮助开发者高效地处理服务器上的文件,实现数据存储、日志记录、文件上传等多种功能。本篇博客将详细介绍 PHP 中的文件处理,包括文件的读取、写入、上传等常用操作,并通过实例帮助你深入理解。通过学习这些基本的文件操作,您将能够在开发 Web 应用时轻松处理与文件相关的各种任务。
繁依Fanyi
2025/01/17
3270
【php学习笔记】文件系统---制作备忘录和修改配置文件
我们会点鼠标右键删除文件、会control+c(或右键)复制、粘贴文件,会新建一些文件,检测这个文件是不是只读文件。
20岁爱吃必胜客
2022/11/13
1.2K0
【php学习笔记】文件系统---制作备忘录和修改配置文件
相关推荐
《PHP技术大全·第1卷》Generator 通俗解读
更多 >
领券
💥开发者 MCP广场重磅上线!
精选全网热门MCP server,让你的AI更好用 🚀
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档