Help us understand the problem. What is going on with this article?

file_get_contentsくん、今までごめんね。

私が弊社に新卒で入社してから,プロジェクトの中で様々な調査作業をやってきました.
中でも統計を取るタイプの調査が多かったです.
その中でも印象に残っていることがありますので,紹介しようと思います.

大容量 CSV を読み込む

弊社,広告関連の企業なのですが,ある日,上司からある調査依頼が来ました(一部アレンジしています).

弊社 HP へのアクセスや広告表示,コンバージョンの履歴で,どの県からのアクセスが多いか,また,どの時間帯にアクセスが多いかを分析してくれ

私は早速,サーバでデータを収集しました.
普段から PHP を使っておりましたので,この集計作業も PHP でやることにしました.
慣れた手つきでファイル名を指定し, file_get_contents 関数でファイルを読み込む処理を記述していきます.

(ファイル読み込み部分のみ)

<?php
$filename = 'data.csv';
$data = explode(PHP_EOL, file_get_contents($filename));

そしていざ,実行の時.
スクリプトを走らせると,予想外のところでエラーが発生...

PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted (tried to allocate 2600008224 bytes) in script.php on line 4

PHP が許容量を超えてメモリを割り当てしようとしたため,落ちたようです.
「許容量の 2 倍くらい読み込もうとしたなぁ,じゃあメモリの許容量を増やせばいいかぁ」なんて思っていたのですが,よーく桁を見ると

メモリの上限:  134217728 bytes
割り当て予定: 2600008224 bytes

約 20 倍...w
ちなみに自分の PC ではなくリモートの本番サーバなので(調査用途),仮に勝手にメモリの上限値を上げた結果サーバが落ちる,とかになったらシャレにならないわけです.

対策

file_get_contents 関数 (や file 関数) はファイルの内容を全て文字列に読み込みます(メモリに乗っけます).

PHP: file_get_contents - Manual

なので,最低でもファイルサイズ以上のメモリが必要になります.
ファイルサイズが大きくない場合は,一括で処理することで実行時間が早くなることが期待されます.
メモリが足りないときは,自分の PC であれば上限値を上げればよいのですが,本番サーバなどで上限値を変えられない場合や,あまりにもファイルサイズが大きい場合などは,こまめにファイルを読み込んで処理していく必要があります.

1 行ずつ読み込むためには fgets 関数 (他に fgetcsv など) を使って読み込みます.
サンプルコードがこちらになります.

<?php

$filename = 'data.csv';

$rowCount = 0;

try {
    $handle = fopen($filename, 'r');
    while ($imported = trim(fgets($handle))) {
        process($imported);
        $rowCount++;
    }
} catch (Exception $exception) {
    fwrite(STDERR, $exception->getMessage());
} finally {
    fclose($handle);
}

fwrite(STDOUT, sprintf("読み込んだ行数: %d 行\n", $rowCount));
fwrite(STDOUT, sprintf("メモリ最大使用量: %d KB\n", memory_get_peak_usage()));

// Def. of Functions
function process($row)
{
    // 何らかの処理
}

file_get_contents にはファイルの開閉処理が含まれていますが, fgets はファイルの中から 1 行取得する「だけ」の関数なので,ファイルの開閉は別途 fopenfclose を使って実装します.
そして while 文で空行が来るまでひたすら「読み込む->処理する」のループを続けます.
読み込んだ行は都度上書きされていくので,メモリがどんどん増えていくことはありません.

このスクリプトを実行すると,

読み込んだ行数: 100000000 行
メモリ最大使用量: 439584 KB

メモリ最大使用量が約 430 KB で済みました!(先ほどと比べて約 6000 分の 1)
(これは割り当て量とは異なります)

まとめ

私は今まで,何でもかんでも file_get_contents でファイルを読み込んでいましたが,理不尽なメモリの割り当てを要求されていた彼 (関数) からするとエラーで落ちまくってつらかったんだと思います.

file_get_contents くん,本当にごめんなさい.

これからは,適宜 file_get_contentsfgets を使い分けていけたらいいなと思います.

fgets は,ファイルを 1 行ずつ読み込むと file_get_contents と比べて時間はかかってしまいますが,メモリ使用量は非常に少なく抑えられていますので,環境によっては非常に便利な関数になります.
理論上はテラとかペタレベルのファイルをうまく処理できるようになるのではないかと思っています.
(1 行が数 GB だとか,集計結果が数 GB とかになってしまう場合は他の方法を考えないといけません)

みなさんもご自身の環境を使って,大容量のファイルを読み込んでみてはいかがでしょうか?

参考

  • 記事内で読み込んだデータについて

今回使用した data.csv はこちらのスクリプトで生成しています.
実行するとファイルサイズは約 2.6 GB になりますのでご注意ください.

<?php
$filename = 'data.csv';
$handle = fopen($filename, 'w');
$output = "Tokyo,2019-12-06 22:03:24\n";
for ($i = 0; $i < 100000000; ++$i) {
    fwrite($handle, $output);
}
fclose($handle);
  • なんかでかいファイルが集まってるサイトはないの?

wikipedia に関する統計データのウェブサイトが大容量のファイルを取りそろえているようなのでご紹介します.
ファイル読み込みの練習などにご活用ください(?).

Wikimedia Downloads: Analytics Datasets

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away