EC-CUBE2(2.13.5)で外字を含んだデータをCSVファイルに出力すると、含んだ行は出力されない。UTF-8からcp932に変換する際、iconvのライブラリを使用しており、このライブラリは変換できない文字が含まれていると、その行毎出力されなくなる。
そもそもCSVを”sjis(というかcp932)”で出力するのが間違い(と主張したいの)ですが、多数決でExcelによせないといけないので妥協案を探さないといけない。
データが正しく出力されない原因は正しく変換できないためであり、変換出来ない原因は”cp932″に該当の文字が無いためです。そしてExcelのために”cp932″は使い続けたい。
この場合、対応策は以下の2択ぐらいになるのではないかと思う。
1. “cp932″が持たない外字を拒否する(入力チェック段階)
2. 変換できなくても良いのでデータは出力したい。
どちらにせよ変換出来ない文字があるかどうか判断できれば何とでもなる。今回は以下の方法で判断し、CSVに出力する方法を記載する。
判断方法
1文字づつ mb_convert_encoding で変換して結果が空なら「変換できない」として判断する。あきらかにベストとは思えないが、調べても上手い方法が見つからなかったのでこのようにした。
if (mb_convert_encoding($chr, 'cp932', mb_internal_encoding()) === "") {
// 変換出来ない
} else {
// 変換出来る
}
実装
イメージとしては、CSV出力ボタンクリック → SC_Helper_CSV_Ex → SC_Utils_Ex class の順で呼び出される感じで思って欲しい。
それぞれ大雑把に説明書きを残す。
SC_Helper_CSV_Ex class 内に追記
public static function &fopen_for_output_csv($filename = 'php://output')
{
$fp = fopen($filename, 'w');
stream_filter_append($fp, 'convert.mb_convert_utf8_to_cp932');
// stream_filter_append($fp, 'convert.iconv.utf-8/cp932');
stream_filter_append($fp, 'convert.eccube_lf2crlf');
return $fp;
}
もともとCSV出力時 stream_filter の仕組みを利用して iconv でエンコードの変換を行っている。
該当箇所
stream_filter_append($fp, 'convert.iconv.utf-8/cp932');
iconv には変換出来ない場合、似た文字に置き換えるオプションもあるのだが、触ってみた感じあまり精度が良くない。それに変換された文字か判別できないのはこわいと考えたので、stream_filter を自作することにした。
該当箇所
stream_filter_append($fp, 'convert.mb_convert_utf8_to_cp932');
PHP: stream_filter_register – Manual
SC_Helper_CSV_Ex class 外末尾に追記
class mb_convert_utf8_to_cp932_filter extends php_user_filter
{
function filter($in, $out, &$consumed, $closing)
{
while ($bucket = stream_bucket_make_writeable($in)) {
$bucket->data = SC_Utils_Ex::sfMbConvertStrWithErrorChr($bucket->data, 'cp932', '?');
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
}
stream_filter_register("convert.mb_convert_utf8_to_cp932", "mb_convert_utf8_to_cp932_filter");
自作した stream_filter がこれです。簡単です。
PHP: php_user_filter::filter – Manual
変換出来ない文字は ? に変換する仕組みにしており、変換処理は別に切り出している。? にしたから、なんだという話ですが、入る確率も低いので、良いのではと考えた、という言い訳があります。
該当箇所
$bucket->data = SC_Utils_Ex::sfMbConvertStrWithErrorChr($bucket->data, 'cp932', '?');
SC_Utils_Ex class 内末尾に追記
function sfMbConvertChrWithResultFlg($chr, $toEnc)
{
$result = [true, ""];
$result[1] = mb_convert_encoding(mb_substr($chr, $i, 1), $toEnc, mb_internal_encoding());
if ($result[1] === "") {
$result[0] = false;
}
return $result;
}
function sfMbConvertStrWithErrorChr($str, $toEnc, $errorChr = "?")
{
$convertStr = "";
for ($i=0; $i < mb_strlen($str); $i++) {
list($isSuccess, $convertChr) = SC_Utils_Ex::sfMbConvertChrWithResultFlg(mb_substr($str, $i, 1), $toEnc);
$convertStr .= ($isSuccess) ? $convertChr : $errorChr;
}
return $convertStr;
}
sfMbConvertStrWithErrorChr で渡された文字列を1文字づつバラバラにして、変換できるか確認しているイメージです(バラバラにする方法も正直あまり自信なし)。
該当箇所
for ($i=0; $i < mb_strlen($str); $i++) {
list($isSuccess, $convertChr) = SC_Utils_Ex::sfMbConvertChrWithResultFlg(mb_substr($str, $i, 1), $toEnc);
$convertStr .= ($isSuccess) ? $convertChr : $errorChr;
}
コメントは自分で記載してもらいたい。