taketiyo.log

Web Engineering 🛠 & Body Building 💪

【PHP】リモートファイルをストリーミングで読み込みメモリ上に乗せずローカルへ保存する

Programming

  / /

PHPにて最もお手軽にリモートファイルをダウンロードしてローカルへ書き出す処理として、file_get_contents()file_put_contents()を用いた下記のような実装がよく見られます。

$url      = 'https://github.com/php/php-src/archive/php-7.3.5.tar.gz'; 
$savePath = './php-7.3.5.tar.gz'; 

file_put_contents($savePath, file_get_contents($url));

 
非常に少ないコード量で素早く実装は可能ですが、上記の場合リモートファイル全体が一旦そのままメモリ上に乗ってしまうため、数百メガ~数ギガといった巨大なファイルを読み込もうとするとメモリに乗り切らず問題が発生します。
 
そのため巨大なリモートファイルを読み込む際は、fread()を用いたストリーミングによる読み込みが必要となります。
 

目次

 

達成出来ること

  • リモートファイルをストリーミングで読み込み、ファイル全体をメモリ上へ乗せずにローカルへ保存する

 

fopen()fread()でリモートファイルを読み込む

function handleStream(string $url, string $savePath): int 
{ 
    // 一度で読み込むチャンクサイズを設定 
    $chunkSize = 1024 * 1024; 

    $saveDir = dirname($savePath); 
    // 保存先のディレクトリが存在していない場合は再帰的に作成 
    if (!is_dir($saveDir)) { 
        @mkdir($saveDir, 0777, true); 
    } 

    // ファイルハンドルを生成 
    $remoteFileHandle = fopen($url, 'rb'); 
    $localFileHandle  = fopen($savePath, 'wb'); 

    $cnt = 0; 

    if ($remoteFileHandle === false || $localFileHandle === false) { 
        return $cnt; 
    } 

    // リモートからファイルをストリーミングしてローカルへ保存 
    while (!feof($remoteFileHandle)) { 
        // 一度に読み込む容量は $chunkSize バイトに制限 
        $buffer = fread($remoteFileHandle, $chunkSize); 
        fwrite($localFileHandle, $buffer); 
        $cnt += strlen($buffer); 
    } 

    // ファイルハンドルを閉じる 
    fclose($remoteFileHandle); 
    fclose($localFileHandle); 

    return $cnt; 
} 

$url      = 'https://github.com/php/php-src/archive/php-7.3.5.tar.gz'; 
$savePath = './php-7.3.5.tar.gz'; 

$totalDownloadBytes = handleStream($url, $savePath);

 

ピーク使用メモリ量を計測する

file_get_contents()の場合

実行コード
$url      = 'https://github.com/php/php-src/archive/php-7.3.5.tar.gz'; 
$savePath = './php-7.3.5.tar.gz'; 

file_put_contents($savePath, file_get_contents($url)); 

echo sprintf("Memory peak usage: %s bytes\n", number_format(memory_get_peak_usage())); 
echo sprintf("Total download bytes: %s bytes\n", number_format(filesize($savePath)));

 

結果
Memory peak usage: 17,767,416 bytes 
Total download bytes: 17,346,901 bytes

 

handleStream()の場合

実行コード
$url      = 'https://github.com/php/php-src/archive/php-7.3.5.tar.gz'; 
$savePath = './php-7.3.5.tar.gz'; 

$totalDownloadBytes = handleStream($url, $savePath); 

echo sprintf("Memory peak usage: %s bytes\n", number_format(memory_get_peak_usage())); 
echo sprintf("Total download bytes: %s bytes\n", number_format($totalDownloadBytes));
結果
Memory peak usage: 1,478,600 bytes 
Total download bytes: 17,346,901 bytes

 
file_get_contents()の場合、ファイル全体がメモリ上に乗ってしまっている一方、handleStream()の場合メモリのピーク使用量はチャンクサイズで指定した容量である1024 * 1024、つまり1MB程度で収まっていることが分かります。
 

おわりに

巨大なファイルを扱う際は、メモリ上に乗せても問題無いサイズなのか、チャンク毎に読み込むべきなのかを適宜判断して実装を行いましょう。