ホームページ  >  記事  >  バックエンド開発  >  PHP を使用して大きなファイルを読み取る方法について話しましょう (チュートリアルの共有)

PHP を使用して大きなファイルを読み取る方法について話しましょう (チュートリアルの共有)

青灯夜游
青灯夜游転載
2022-09-22 20:09:265335ブラウズ

PHP はどのようにして大きなファイルを読み取るのでしょうか?次の記事ではPHPを使って大容量ファイルを読み込む方法を紹介しますので、ぜひ参考にしてください。

PHP を使用して大きなファイルを読み取る方法について話しましょう (チュートリアルの共有)

PHP 開発者として、メモリ管理について心配する必要はありません。 PHP エンジンは、私たちの背後でクリーンアップという優れた仕事をしてくれます。また、一時的な実行コンテキストの Web サーバー モデルにより、どんなにずさんなコードであっても永続的な影響はありません。

まれに、快適さの限界を超える必要がある場合があります。たとえば、作成できる最小の VPS で大規模なプロジェクトの Composer を実行しようとしたときや、大きなファイルを読み取る必要があるときなどです。同様に小規模なサーバー上でのファイル時間。

これは、このチュートリアルで説明する質問です。

このチュートリアルのコードは、GitHub にあります。

成功の測定

コードに加えた改善が効果的であることを確認する唯一の方法は、悪い状況を測定し、それをどのように改善したかを比較することです。測定後の改良を適用しました。言い換えれば、それが(もしあったとしても)どれだけ役立つかが分からない限り、その「解決策」が解決策であるかどうかはわかりません。

2 つの指標に注目することができます。 1つ目はCPU使用率です。私たちが扱っているプロセスの実行速度はどれくらいですか? 2つ目はメモリ使用量です。スクリプトの実行にはどのくらいのメモリが必要ですか?通常、これらは反比例します。つまり、CPU 使用率を犠牲にしてメモリ使用量を削減でき、またその逆も可能です。

非同期処理モデル (マルチプロセスまたはマルチスレッド PHP アプリケーションなど) では、CPU とメモリの使用量の両方が重要な考慮事項になります。従来の PHP アーキテクチャでは、通常、サーバーの制約に達すると、これが問題になります。

PHP 内の CPU 使用率を測定することは困難です。これを本当に気にする場合は、Ubuntu または macOS で top のようなコマンドを使用することを検討してください。 Windows の場合は、Ubuntu で top コマンドを使用できるように、Linux サブシステムの使用を検討してください。

このチュートリアルでは、メモリ使用量を測定します。 「従来の」スクリプトが使用するメモリの量を見てみましょう。また、いくつかの最適化戦略を実装し、それらを測定します。最後に、合理的な選択ができることを願っています。

メモリ使用量を表示するために使用するメソッドは次のとおりです:

// 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];
}

これらのメソッドをスクリプトの最後で使用して、どのスクリプトが最も多くのメモリを使用しているかを一度に理解できるようにします。時間。

私たちの選択肢は何ですか?

ファイルを効率的に読み取る方法はたくさんあります。これらは次の 2 つのシナリオで使用されます。すべてのデータを同時に読み取り、処理したり、処理されたデータを出力したり、その他の操作を実行したりすることが必要な場合があります。データにアクセスせずにデータ ストリームを変換したい場合もあります。

最初のケースとして、ファイルを読み取り、10,000 行ごとのデータを別のキューに渡して処理する場合を想像してください。少なくとも 10,000 行のデータをメモリにロードし、それらをキュー マネージャー (使用されるキュー マネージャー) に渡す必要があります。

2 番目のケースでは、特に大きい API 応答のコンテンツを圧縮するとします。ここではその内容は気にしませんが、圧縮形式でバックアップされていることを確認する必要があります。

どちらの場合も、大きなファイルを読み取る必要があります。違いは、最初のケースではデータが何であるかを知る必要があるのに対し、2 番目のケースではデータが何であるかを気にしないことです。次に、これら 2 つのアプローチについて詳しく説明します...

ファイルを 1 行ずつ読み取る

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.5MBです。メモリ使用量は 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 でピークに達します。ファイルデータの処理を追加する必要があるため、このデータはあまり意味がありません。たとえば、2 つの空白行が表示された場合は、ドキュメントをいくつかのチャンクに分割します。

// 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&#39;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&#39;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://memoryphp://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"
);

关于流,在 Understanding Streams in PHPUsing PHP Streams Effectively 文章中已经进行了广泛的讨论,如果你想要换个角度思考,可以查看以上这两篇文章。

自定义流

fopenfile_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 这样的函数时,这方面的错误将全部从应用程序中消失,这不失为一件好事。

英文原文地址:https://www.sitepoint.com/performant-reading-big-files-php/

译文地址:https://learnku.com/php/t/39751

推荐学习:《PHP视频教程

以上がPHP を使用して大きなファイルを読み取る方法について話しましょう (チュートリアルの共有)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はlearnku.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。