Shift-JISで「5C」の文字コードは「¥」を意味しており、エスケープ記号として解釈されることに起因する問題である。
ソ」「表」「十」「予」などSJISの文字の2バイト目が「5C」になる文字が、fgetcsvなどの処理機に与えられたとき、それが文字の一部としてでなく、ASCIIのバックスラッシュ(5C)と判断してしまい、エスケープ文字として解釈される。
例えば、
1. 「表示 (95 5c 8E A6)」という文字がある。
https://qiita.com/Kohei-Sato-1221/items/c050bb23436f35666165
2. 「5c」がエスケープ文字と判断される(8Eをエスケープすると解釈)
3. 結果的に「侮ヲ(95 8E A6)」となってしまう。
PHPでShift-JISのCSVをUTF-8に変換する際に、下記のようなコードを使用するシーンが良く見られるが、これはデータによっては正しく読み込むことができない。
以下参考:https://qiita.com/suin/items/3edfb9cb15e26bffba11
// fgetcsvで読み取った後に、文字コードを変換するコード
$fp = fopen('sjis.csv', 'r');
while ($row = fgetcsv($fp) !== false) {
mb_convert_variables('UTF-8', 'SJIS-win', $row);
}
fclose($fp);
次のようなコードは試すと、5C問題が確認できる。
$data = mb_convert_encoding('"表"', 'SJIS-win', 'UTF-8');
$csv = tmpfile();
fwrite($csv, $data);
rewind($csv);
var_dump(fgetcsv($csv)); //=> bool(false)
上記のコードの fgetcsv は「”表”」を(ダブルクオーテーション)(謎の文字)(エスケープ記号)(ダブルクオーテーション)と解釈し、閉じ側のダブルクオーテーションがエスケープされた結果、カラムの終わりが無い不正なCSVの行と判断されるため、falseを返す。
このような事象を起こさないためには、fgetcsvのCSVパース処理以前に文字コードをUTF-8にしておく必要がある。
UTF-8はSJISとは設計が異なり、5Cが1文字の途中のバイトに出てくることがないため、理論上5C問題は存在しない。
事前に文字コードを変換しておく方法として、file_get_contentsなどでSJISのCSVの中身を文字列で取り出し、mb_convert_encodingでUTF-8に変換し、それをfile_put_contentsでファイルに書き出し、UTF-8のCSVを作り、それをfopenするような実装が考えられる。
$sjis = file_get_contents('sjis.csv');
$utf8 = mb_convert_encoding($sjis, 'UTF-8', 'SJIS-win');
file_put_contents('utf8.csv', $utf8);
$fp = fopen('utf8.csv', 'r');
上記のコードは読み込むCSVが大きく、サーバーのPHPのメモリが少ない場合に、メモリ不足のためエラーが発生する可能性がある。
この問題はストリーム処理を行うことで、解決できる。