详解PHP如何读取大文件

衡量成功

唯一能确认我们对代码所做改进是否有效的方式是:衡量一个糟糕的情况,然后对比我们已经应用改进后的衡量情况。换言之,除非我们知道 “解决方案” 能帮我们到什么程度 (如果有的话),否则我们并不知道它是否是一个解决方案。

我们可以关注两个指标。首先是 CPU 使用率。我们要处理的过程运行得有多快或多慢?其次是内存使用率。脚本执行要占用多少内存?这些通常是成反比的 — 这意味着我们能够以 CPU 使用率为代价减少内存的使用率,反之亦可。

在一个异步处理模型 (例如多进程或多线程 PHP 应用程序) 中,CPU 和内存使用率都是重要的考量。在传统 PHP 架构中,任一达到服务器所限时这些通常都会成为一个麻烦。

测量 PHP 内部的 CPU 使用率是难以实现的。如果你确实关注这一块,可用考虑在 Ubuntu 或 macOS 中使用类似于 top 的命令。对于 Windows,则可用考虑使用 Linux 子系统,这样你就能够在 Ubuntu 中使用 top 命令了。

在本教程中,我们将测量内存使用情况。我们将看一下 “传统” 脚本会使用多少内存。我们也会实现一些优化策略并对它们进行度量。最后,我希望你能做一个合理的选择。

以下是我们用于查看内存使用量的方法:

// formatBytes 方法取材于 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];
}

我们将在脚本的结尾处使用这些方法,以便于我们了解哪个脚本一次使用了最多的内存。

我们有什么选择?

我们有许多方法来有效地读取文件。有以下两种场景会使用到他们。我们可能希望同时读取和处理所有数据,对处理后的数据进行输出或者执行其他操作。 我们还可能希望对数据流进行转换而不需要访问到这些数据。

想象以下,对于第一种情况,如果我们希望读取文件并且把每 10,000 行的数据交给单独的队列进行处理。我们则需要至少把 10,000 行的数据加载到内存中,然后把它们交给队列管理器(无论使用哪种)。

对于第二种情况,假设我们想要压缩一个 API 响应的内容,这个 API 响应特别大。虽然这里我们不关心它的内容是什么,但是我们需要确保它被以一种压缩格式备份起来。

这两种情况,我们都需要读取大文件。不同的是,第一种情况我们需要知道数据是什么,而第二种情况我们不关心数据是什么。接下来,让我们来深入讨论一下这两种做法.

逐行读取文件

PHP 处理文件的函数很多,让我们将其中一些函数结合起来实现一个简单的文件阅读器

// 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.5 MB。内存使用峰值为 12.8 MB。现在,让我们使用生成器来读取每一行:

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

文件大小相同,但是内存使用峰值为 393 KB。这个数据意义大不大,因为我们需要加入对文件数据的处理。例如,当出现两个空白行时,将文档拆分为多个块:

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

有人猜测这次使用多少内存吗?即使我们将文本文档分为 126 个块,我们仍然只使用 459 KB 的内存。鉴于生成器的性质,我们将使用的最大内存是在迭代中需要存储最大文本块的内存。在这种情况下,最大的块是 101985 个字符。

生成器还有其他用途,但显然它可以很好的读取大型文件。如果我们需要处理数据,生成器可能是最好的方法。

文件之间的管道

在不需要处理数据的情况下,我们可以将文件数据从一个文件传递到另一个文件。这通常称为管道 (大概是因为除了两端之外,我们看不到管道内的任何东西,当然,只要它是不透明的)。我们可以通过流 (stream) 来实现,首先,我们编写一个脚本实现一个文件到另一个文件的传输,以便我们可以测量内存使用情况:

// from piping-files-1.php
file_put_contents(
    "piping-files-1.txt", file_get_contents("shakespeare.txt")
);
require "memory.php";

结果并没有让人感到意外。该脚本比其复制的文本文件使用更多的内存来运行。这是因为脚本必须在内存中读取整个文件直到将其写入另外一个文件。对于小的文件而言,这种操作是 OK 的。但是将其用于大文件时,就不是那么回事了。

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

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

这段代码有点奇怪。我们打开两个文件的句柄,第一个处于读取模式,第二个处于写入模式。然后,我们从第一个复制到第二个。我们通过再次关闭两个文件来完成。当你知道内存使用为 393 KB 时,可能会感到惊讶。这个数字看起来很熟悉,这不就是利用生成器保存逐行读取内容时所使用的内存吗。这是因为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),它们的实际效果是一样的。

内存使用情况为 581KB,现在,我们如何尝试进行流传输呢?

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

内存使用比刚才略少 (400 KB),但是结果是相同的。如果我们不需要内存信息,也可以打印至标准输出。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将继续存储在内存中直到耗尽。

过滤器

我们可以对流使用另一个技巧,称为过滤器。它介于两者之间,对数据进行了适当的控制使其不暴露给外接。假设我们要压缩shakespeare.txt文件。我们可以使用 Zip 扩展

// 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.75 MB 的内存。我们可以使用过滤器来进行优化

// 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 过滤器:

// from filters-2.php
file_get_contents(
    "php://filter/zlib.inflate/resource=filters-2.deflated"
);

自定义流

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

本例中,我们尝试发送一个 POST 请求给 API。API 端点是安全的,不过我们仍然使用了 http 上下文属性(可用于 http 或者 https)。我们设置了一些头部,并打开了 API 的文件句柄。我们可以将句柄以只读方式打开,上下文负责编写。

创建自定义协议和过滤器

在总结之前,我们先谈谈创建自定义协议。

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 )
}

我们并不打算实现其中一个,因为我认为它值得拥有自己的教程。有很多工作要做。但是一旦完成工作,我们就可以很容易地注册流包装器:

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
    public int filter ( resource $in , resource $out , int &$consumed ,
        bool $closing )
    public void onClose ( void )
    public bool onCreate ( void )
}

可被轻松注册

$handle = fopen("story.txt", "w+");
stream_filter_append($handle, "highlight-names", STREAM_FILTER_READ);

highlight-names 需要与新过滤器类的 filtername 属性匹配。还可以在 php:///filter/highligh-names/resource=story.txt 字符串中使用自定义过滤器。定义过滤器比定义协议要容易得多。原因之一是协议需要处理目录操作,而过滤器仅需要处理每个数据块。

如果您愿意,我强烈建议您尝试创建自定义协议和过滤器。如果您可以将过滤器应用于 stream_copy_to_stream 操作,则即使处理令人讨厌的大文件,您的应用程序也将几乎不使用任何内存。想象一下编写调整大小图像过滤器或加密应用程序过滤器。

如果你愿意,我强烈建议你尝试创建自定义协议和过滤器。如果你可以将过滤器应用于 stream_copy_to_stream 操作,即使处理烦人的大文件,你的应用程序也几乎不使用任何内存。想象下编写 resize-image 过滤器和 encrypt-for-application 过滤器吧。

总结

虽然这不是我们经常遇到的问题,但是在处理大文件时的确很容易搞砸。在异步应用中,如果我们不注意内存的使用情况,很容易导致服务器的崩溃。

本教程希望能带给你一些新的想法(或者更新你的对这方面的固有记忆),以便你能够更多的考虑如何有效地读取和写入大文件。当我们开始熟悉和使用流和生成器并停止使用诸如 file_get_contents 这样的函数时,这方面的错误将全部从应用程序中消失,这不失为一件好事。

以上就是详解PHP如何读取大文件的详细内容,更多关于PHP如何读取大文件的资料请关注我们其它相关文章!

(0)

相关推荐

  • php实现断点续传大文件示例代码

    一.断点续传原理 所谓断点续传,也就是要从文件已经下载的地方开始继续下载.在以前版本的 HTTP 协议是不支持断点的,HTTP/1.1 开始就支持了.一般断点下载时才用到 Range 和 Content-Range 实体头. 不使用断点续传 get /down.zip http/1.1 accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms- excel, application/msword

  • PHP如何通过表单直接提交大文件详解

    前言 我想通过表单直接提交大文件,django 那边我就是这么干的.而对于 php 来说,我认为尽管可以设置最大上传的大小,但最大也无法超过内存大小,因为它无法把文件内容都放到 php://input 里面.直到我试了一下. 下面话不多说了,来一起看看详细的介绍吧 试验 我创建内存 256M 的虚拟机,通过表单直接上传 2.4G 的文件,发现居然可以,挺惊讶的: 后端是 nginx + php 的方式.反正有关 php.ini 里面需要设置的给它足够大: # pip.ini post_max_s

  • PHP大文件分割分片上传实现代码

    服务端为什么不能直接传大文件?跟php.ini里面的几个配置有关 upload_max_filesize = 2M //PHP最大能接受的文件大小 post_max_size = 8M //PHP能收到的最大POST值' memory_limit = 128M //内存上限 max_execution_time = 30 //最大执行时间 当然不能简单粗暴的把上面几个值调大,否则服务器内存资源吃光是迟早的问题. 解决思路 好在HTML5开放了新的FILE API,也可以直接操作二进制对象,我们可

  • PHP大文件及断点续传下载实现代码

    一般来说浏览器要同时下载几个文件,比如pdf文件,会在服务器端把几个文件压缩成一个文件.但是导致的问题就是会消耗服务器的cpu和io资源. 那有没有办法,用户点了几个文件,在客户端同时下载呢? 支持html5的浏览器是可以的,html的a标签有一个属性download <a download="下载的1.pdf" href="1.pdf" rel="external nofollow" rel="external nofollow

  • 详解PHP多个进程配合redis的有序集合实现大文件去重

    1.对一个大文件比如我的文件为 -rw-r--r-- 1 ubuntu ubuntu 9.1G Mar 1 17:53 2018-12-awk-uniq.txt 2.使用split命令切割成10个小文件 split -b 1000m 2018-12-awk-uniq.txt -b 按照字节切割 , 支持单位m和k 3.使用10个php进程读取文件 , 插入redis的有序集合结构中 , 重复的是插不进去的 ,因此可以起到去重的作用 <?php $file=$argv[1]; //守护进程 uma

  • PHP超低内存遍历目录文件和读取超大文件的方法

    这不是一篇教程,这是一篇笔记,所以我不会很系统地论述原理和实现,只简单说明和举例. 前言 我写这篇笔记的原因是现在网络上关于 PHP 遍历目录文件和 PHP 读取文本文件的教程和示例代码都是极其低效的,低效就算了,有的甚至好意思说是高效,实在辣眼睛. 这篇笔记主要解决这么几个问题: PHP 如何使用超低内存快速遍历数以万计的目录文件? PHP 如何使用超低内存快速读取几百MB甚至是GB级文件? 顺便解决哪天我忘了可以通过搜索引擎搜到我自己写的笔记来看看.(因为需要 PHP 写这两个功能的情况真的

  • PHP大文件分片上传的实现方法

    一.前言 在网站开发中,经常会有上传文件的需求,有的文件size太大直接上传,经常会导致上传过程中耗时太久,大量占用带宽资源,因此有了分片上传. 分片上传主要是前端将一个较大的文件分成等分的几片,标识当前分片是第几片和总共几片,待所有的分片均上传成功的时候,在后台进行合成文件即可. 二.开发过程中遇到的问题 分片的时候每片该分多大size?太大会出现"413 request entity too large" 分片上传的时候并不是严格按照分片的序号顺序上传,如何判断所有的分片均上传成功

  • php下载远程大文件(获取远程文件大小)的实例

    废话不多说,直接上代码 <?php // 暂不支持断点续传 // $url = 'http://www.mytest.com/debian.iso'; 不知道为何获取本地文件大小为0 $url = 'http://192.168.8.93/download/vm-672/18/0.vmdk'; $file = basename($url); $header = get_headers($url, 1); $size = $header['Content-Length']; $fp = fopen

  • PHP下载大文件失败并限制下载速度的实例代码

    1.问题: PHP在使用readfile函数定义下载文件时候,文件不可以过大,否则会下载失败,文件损坏且不报错: 2.原因: 这个是因为readfile读取文件的时候会把文件放入缓存,导致内存溢出: 3.解决:分段下载,并限制下载速度: <?php //设置文件最长执行时间 set_time_limit(0); if (isset($_GET['filename']) && !empty($_GET['filename'])) { $file_name = $_GET['filena

  • 详解PHP如何读取大文件

    衡量成功 唯一能确认我们对代码所做改进是否有效的方式是:衡量一个糟糕的情况,然后对比我们已经应用改进后的衡量情况.换言之,除非我们知道 "解决方案" 能帮我们到什么程度 (如果有的话),否则我们并不知道它是否是一个解决方案. 我们可以关注两个指标.首先是 CPU 使用率.我们要处理的过程运行得有多快或多慢?其次是内存使用率.脚本执行要占用多少内存?这些通常是成反比的 - 这意味着我们能够以 CPU 使用率为代价减少内存的使用率,反之亦可. 在一个异步处理模型 (例如多进程或多线程 PH

  • 详解C语言读取文件求某一列的平均值

    目录 第一部分:比较读取文件的效率 第二部分:比较求取列平均值的效率 第一部分:比较读取文件的效率 在之前的文章<生信(五)awk求取某一列的平均值>中,笔者曾经给出过C语言求取某列平均值的代码,但是最近回顾时发现,这段代码至少有几点不足: 1. 利用 fgetc 函数来读取文件,现在看来效率不高. 2. 如果文件最后没有一个空白行的话,会陷入无限循环.也就是对 EOF 的处理不完善. 大家都知道,C语言读取文件的常用函数有 fgetc.fgets.fread 以及 fscanf 等.笔者曾经

  • 详解Java无需解压直接读取Zip文件和文件内容

    整理文档,搜刮出一个Java无需解压直接读取Zip文件和文件内容的代码,稍微整理精简一下做下分享. package test; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.util.zip.ZipE

  • 详解C#编程获取资源文件中图片的方法

    详解C#编程获取资源文件中图片的方法 本文主要介绍C#编程获取资源文件中图片的方法,涉及C#针对项目中资源文件操作的相关技巧,以供借鉴参考.具体内容如下: 例子: using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Reflection; using System.Drawing; namespace CL { public class RES { /

  • 用十张图详解TensorFlow数据读取机制(附代码)

    在学习TensorFlow的过程中,有很多小伙伴反映读取数据这一块很难理解.确实这一块官方的教程比较简略,网上也找不到什么合适的学习材料.今天这篇文章就以图片的形式,用最简单的语言,为大家详细解释一下TensorFlow的数据读取机制,文章的最后还会给出实战代码以供参考. TensorFlow读取机制图解 首先需要思考的一个问题是,什么是数据读取?以图像数据为例,读取数据的过程可以用下图来表示: 假设我们的硬盘中有一个图片数据集0001.jpg,0002.jpg,0003.jpg--我们只需要把

  • 使用Python读取大文件的方法

    背景 最近处理文本文档时(文件约2GB大小),出现memoryError错误和文件读取太慢的问题,后来找到了两种比较快Large File Reading 的方法,本文将介绍这两种读取方法. 准备工作 我们谈到"文本处理"时,我们通常是指处理的内容.Python 将文本文件的内容读入可以操作的字符串变量非常容易.文件对象提供了三个"读"方法: .read()..readline() 和 .readlines().每种方法可以接受一个变量以限制每次读取的数据量,但它们

  • 详解Django自定义图片和文件上传路径(upload_to)的2种方式

    最近在做一个仿知乎网站的项目了,里面涉及很多图片和文件上传.趁此机会我给大家总结下Django自定义图片和文件上传路径的2种方式吧. 方法1: 在Django模型中定义upload_to选项. Django模型中的ImageField和FileField的upload_to选项是必填项,其存储路径是相对于MEIDA_ROOT而来的. 我们来看一个简单案例(如下所示).如果你的MEDIA_ROOT是/media/文件夹,而你的上传文件夹upload_to="avatar", 那么你上传的

  • C语言fgetc和fputc函数用法详解(以字符形式读写文件)

    在C语言中,读写文件比较灵活,既可以每次读写一个字符,也可以读写一个字符串,甚至是任意字节的数据(数据块).本节介绍以字符形式读写文件. 以字符形式读写文件时,每次可以从文件中读取一个字符,或者向文件中写入一个字符.主要使用两个函数,分别是 fgetc() 和 fputc(). 字符读取函数 fgetc fgetc 是 file get char 的缩写,意思是从指定的文件中读取一个字符.fgetc() 的用法为: int fgetc (FILE *fp); fp 为文件指针.fgetc() 读

  • 详解SpringBoot注解读取配置文件的方式

    一.@Value读取application.properties配置文件中的值 application.properties配置文件 fileName=configName PropertiesConfig类文件 import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class PropertiesC

  • 详解Java目录操作与文件操作教程

    目录 目录操作 创建目录 判断这个文件或目录是否存在 判断是否是目录 读取目录 删除目录 文件操作 创建文件 删除文件 File对象常用函数 目录操作 创建目录 File类中有两个方法可以用来创建文件夹: mkdir( )方法创建一个文件夹,成功则返回true,失败则返回false.失败表明File对象指定的路径已经存在,或者由于整个路径还不存在,该文件夹不能被创建. mkdirs()方法创建一个文件夹和它的所有父文件夹. 创建目录AAA路径为D:AAA public class Mk { pu

随机推荐