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

こちらの記事は [free_dev_com Advent Calendar 2019 - Adventar](https://adventar.org/calendars/4736) 9 日目の記事です。

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

## 大容量 CSV を読み込む

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

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

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

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

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

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

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

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

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

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

## 対策

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

[PHP: file_get_contents - Manual](https://www.php.net/manual/ja/function.file-get-contents.php)

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

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

```php
<?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 行取得する「だけ」の関数なので，ファイルの開閉は別途 `fopen` と `fclose` を使って実装します．
そして while 文で空行が来るまでひたすら「読み込む->処理する」のループを続けます．
読み込んだ行は都度上書きされていくので，メモリがどんどん増えていくことはありません．

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

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

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

## まとめ

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

`file_get_contents` くん，本当にごめんなさい．

これからは，適宜 `file_get_contents` と `fgets` を使い分けていけたらいいなと思います．

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

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

### 参考

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

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

```php
<?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](https://dumps.wikimedia.org/other/analytics/)