加入收藏 | 设为首页 | 会员中心 | 我要投稿 航空爱好网 (https://www.ikongjun.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 站长学院 > PHP教程 > 正文

PHP过滤器 逐行读取文件

发布时间:2022-10-26 14:56:03 所属栏目:PHP教程 来源:
导读:  作为 php 开发人员,我们不需要担心内存管理。 php 引擎在我们背后进行了出色的清理工作,短暂执行上下文的 web server 模型意味着即使是最草率的代码也没有持久的影响。

  在极少数情况下,我们可能需要

  作为 php 开发人员,我们不需要担心内存管理。 php 引擎在我们背后进行了出色的清理工作,短暂执行上下文的 web server 模型意味着即使是最草率的代码也没有持久的影响。
 
  在极少数情况下,我们可能需要走出舒适的界限 — 例如,当我们尝试在可以创建的最小 vps 上为大型项目运行 composer 时,或者需要在同样小的服务器上读取大文件时。
 
  这是我们将在本教程中讨论的一个问题。
 
  本教程的代码可以在这里找到github.
 
  衡量成功
 
  唯一能确认我们对代码所做改进是否有效的方式是:衡量一个糟糕的情况,然后对比我们已经应用改进后的衡量情况。换言之,除非我们知道 “解决方案” 能帮我们到什么程度 (如果有的话),否则我们并不知道它是否是一个解决方案。
 
  我们可以关注两个指标。首先是 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";
  其他流
 
  还存在一些流可以通过管道来读写。
 
  过滤器
 
  我们可以对流使用另一个技巧,称为过滤器。它介于两者之间,对数据进行了适当的控制使其不暴露给外接。假设我们要压缩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具有它们自己的默认选项集,但是它们是完全可定制的。要定义它们PHP过滤器,我们需要创建一个新的流上下文
 
  // 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 的文件句柄。我们可以将句柄以只读方式打开,上下文负责编写。
 

(编辑:航空爱好网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!