hnwの日記

巨大なSJISのCSVファイルをfgetcsv関数で処理する

さて、前回記事「PHPでメモリ上に一時ファイルを作る」では、file_get_contents関数でCSVファイル全体を取得して文字エンコーディングの変換を行い、変換結果に対してfgetcsv関数を利用しました。しかし、CSVファイルが巨大な場合にはfile_get_contentsを使うとメモリ上限に引っかかってPHPが停止してしまいます。


もちろん、UTF-8CSVファイルに対してfgetcsvを利用するのであれば、どんな大きなCSVファイルだろうと処理することが可能です。なぜなら、fgetcsvはファイルを1行単位で読み込む関数ですから、1行分のメモリ消費だけでファイル全体を読み込み続けられるからです(正確にはストリーム上のデータはバッファリングされるので、バッファサイズ分のメモリは消費しますが)。


それでは、巨大なSJISCSVファイルをfgetcsvで処理したい場合はどうすれば良いのでしょうか。このような場合には、ストリームフィルタが便利です。ストリームフィルタというのは、ストリーム上に流れる文字列を加工するための仕組みです。今回、Stream_Filter_Mbstringという、文字エンコーディング変換を行うストリームフィルタを作ってみました。


前回同様、SJISCSVファイルを処理するコード例を示します。

<?php

require_once 'Stream/Filter/Mbstring.php';

$ret = stream_filter_register("convert.mbstring.*", "Stream_Filter_Mbstring");

$fp = fopen("example.csv", 'r');
$filter_name = 'convert.mbstring.encoding.SJIS-win:UTF-8';
$filter = stream_filter_append($fp, $filter_name, STREAM_FILTER_READ);

$current_locale = setlocale(LC_ALL, '0'); // 現在のロケールを取得
setlocale(LC_ALL, 'ja_JP.UTF-8');
while ($values = fgetcsv($fp, 10000)) {
  print_r($values);
}
setlocale(LC_ALL, $current_locale); // ロケールを戻す
fclose($fp);


このように、fopenで作成したストリームにstream_filter_appendするだけでストリームフィルタを適用できます。これで実際に数百MBあるCSVを最後まで読むことができます。


もちろん、実案件ではもう少しエラー処理を真面目にやるべきです。また、前回記事のコメントでid:moriyoshiさんからツッコミが入っていましたが、fgetcsvの第2引数にも注意が必要です。本当に10000バイトを超える行が来ないのかどうか、巨大な行があった場合にエラーを出す仕組みがあるのか、などは注意が必要な点ですね。


このStream_Filter_Mbstringはpearで簡単にインストールできます。

$ sudo pear channel-discover openpear.org
$ sudo pear install openpear/Stream_Filter_Mbstring


実は似た目的のconvert.iconv.*というフィルタがPHPデフォルトで利用できますが、これを実案件に利用するのは厳しいと思います。というのも、不正な文字が1バイトでも含まれていると、ストリーム全体を0バイトに変換してしまうのです(何かうまい回避方法があるのかもしれませんが)。ですので、Stream_Filter_Mbstringを自作したわけです。


他にどんなフィルタが存在するかは「PHP: 利用できるフィルタのリスト - Manual」を参照してください。


最後に、今回Stream_Filter_Mbstringを置かせてもらっているopenpearは素晴らしいですね。これについては別記事にまとめたいと思います。