概要
大きなCSVファイルを扱う際に、調べたことをメモしておく。
文字コード問題
CSVファイルは、だいたいShift-JISのエンコードで渡されてくることが多いです。
そのため、文字コードをUTF8に変換するなどの処理が必要になったりします。
しかし、Shift-JISのエンコードには、「5C問題」というものがあるようです。
※ 5C問題
そのため、shift-jisエンコードのCSVファイルをそのまま扱おうとすると問題が発生します。
例)下記のようなCSVの行が合った場合、「ソ」がC5問題に引っかかります
"ほげ","ミソ","1","2"
↓↓↓ ※「ソ」の後の「"」がエスケープされる
"ほげ","ミソ\",\"1","2" ( 行がずれる... )
エンコード変換
CSVパーズする前に文字コードを変換する。
下記のようにエンコード変換したファイルを作成してみました。
$str = file_get_contents('sjis.csv');
$str = mb_convert_encoding($str, 'UTF-8', 'ASCII,JIS,UTF-8,SJIS-win');
file_put_contents('utf8.csv', $str);
しかし、このやり方だと、CSVファイルの容量が大きくなるとPHPの消費メモリーが増えます。
そのため、頻繁にメモリーオーバーのエラーを起こします。
Allowed memory size of 536870912 bytes exhausted (tried to allocate 483345347 bytes)
このやり方は、駄目でした...
対応方法
fopen
でもSplFileObject
などで、1行ごと読んで処理する方式だと、メモリーの消費を抑えられそう。
そこで、「 SplFileObject
」を使いました。
( ただし、CSVとしてパーズせず、1行ごとにエンコード変換するようにする )
DB::beginTransaction();
try {
// ファイルの読み込み
$file = new SplFileObject($file);
$file->setFlags(
// SplFileObjectでCSVパーズしない (Shift-JISの5C問題)
// SplFileObject::READ_CSV | // CSV解析(パーズ)する
SplFileObject::READ_AHEAD | // 先読み/巻き戻しで読み出す
SplFileObject::SKIP_EMPTY | // 空行は読み飛ばす
SplFileObject::DROP_NEW_LINE // 行末の改行を読み飛ばす
);
foreach ($file as $line) {
// 文字エンコード変換
// ※ Shift-JISの5C問題があるため、CSVパーズ前にエンコードを変換する
$line = mb_convert_encoding($line, 'UTF-8', 'ASCII, JIS, UTF-8, SJIS-win');
$row = str_getcsv($line);
// 何かしら処理を実装する
DB::commit();
}
} catch (Exception $e) {
DB::rollback();
// Laravelのエラーハンドリングに渡すため、スローする
// Throw to pass to error handling of Laravel.
throw $e;
}
とりあえず、このやり方で、30万件のデータの処理ができました。
fgetcsv
関数のストリームフィルタでも対応できそうですが、実装が複雑になりそうだったので断念。
文字コードの問題は、毎回面倒になりますね(^^;)
参考サイト
- shift_jisでの5C問題回避法(PHPのみ)
- PHP: fgetcsvでもSJISのCSVをUTF-8として《安全》に読む方法(ストリームフィルタ使用)
- SplFileObjectでのCSVファイル読み込みでの文字化け対策
- Laravelで大容量CSVファイルをSplFileObjectクラスで確実に処理する
以上