PHP CSVファイル読み込みした時の邪魔な3バイト
久々にPHPの話題。
前にやっていた勤怠管理のシステムを改めてチェックしたらかなり不安定だったのでいろいろ調整してる時に見つかった不具合について。
以前CSVファイルを読み込んで一括で保存する、なんてことをちらっと書いた気がします。
今回CSVデータに含まれる意地悪な要素を見つけたの書き起こし。
自分が過去にやっていたのはCSVを配列化し、その配列の中を見て位置を指定してフォームに自動入力していくというもの。
当時は文字コードだけが問題なんじゃないかと思っていたのですが、そうじゃなかった・・・
そもそもCSVファイルを用意していたのはLibre Office Calcというフリーの表計算ソフトからでした。
当時はLinuxからしか開発してなかったので気にも止めなかったのですが、今回はWindows環境から再開発することに。
で、Windowsに入ってる表計算ソフトと言えばやはりExcel。
ExcelからもCSVファイルにする事が出来るのですが、このCSV化に問題がありました。
仮にこんなテーブルがあったとします。
A1 | B1 | C1 | |
A2 | B2 | C3 | |
A1 | B2 | C3 |
自分が表計算ソフトで何か作る時の癖なのですが一番上と一番左を完全に空スペースにしています。
これをCSV化した時、先ほどあげた二つのソフトでは中身が異なることが判明。
Excel
A1,B1,C1
A2,B2,C2
A3,B3,C3
Libre Office Calc
,,,,
,A1,B1,C1
,A2,B2,C2
,A3,B3,C3
Excelの方は空の列や行を完全に無視してリサイズしたCSVになるのに対し、Calcの方は原版に忠実にCSV化してくれます。
これだけだったらそんなに問題ないのですが、PHPでこのCSVを配列に入れた時に問題が出ました。
CSVの左上から順に$array_dataという2次元配列に値を入れ、それぞれ A1 という文字が入っている場所を確認するとします。
if( $array_data[1][1] === 'A1' || $array_data[0][0] === 'A1' ) { //整合性が取れたことにし、続けて処理を書く } else { //整合性がないため、エラーメッセージを表示 }
こんな感じ。
Calcで出したCSVファイルの場合は $array_data[1][1] === 'A1' が true 、 $array_data[0][0] === 'A1' が false のor処理になるので true になります。
しかし、Excelで出したCSVの場合、$array_data[1][1] === 'A1' が false 、 $array_data[0][0] === 'A1' が true になりそうですが、どちらも false になります。
その理由が今回のタイトル文、邪魔な3バイトになります。
調べてみたのですが、CSVファイルの最初には目に見えない形で文字コードを識別する何かが入っている事が判明。
実際にphpで var_dump($array_data[0][0]) とやるとCalcで出したCSVでは String(3) "" 、 Excelで出したCSVでは String(5) "A1" と返されます。
UTF-8では何も表示されませんが、他の文字コードでエンコードすると3バイト分の何かが入ってるのが分かります。
この3バイトをBOM(Byte Order Mark)というらしいです。
元々何が入ってるか見えないので厳密比較ではどうにも処理がしにくいのです。
そこで対策、最初に入るのは配列の一番最初と分かっているのでそれを利用してBOM部分を消去。
if ( ord($array_data[0][0]{0}) == 0xef && ord($array_data[0][0]{1}) == 0xbb && ord($array_data[0][0]{2}) == 0xbf) { $array_data[0][0] = substr($array_data[0][0], 3); if(!$array_data[0][0]) { $array_data[0][0] = null; } }
今回はUTF-8で保存した時の例。他の文字コードでCSVを保存した時はif文の中身が変わりそう。
ordは指定した文字列の指定したバイト位置をASCIIコードにする関数です。
UTF-8のBOMが入っていたら、substr()で3バイト文を消去。
substr()はバイト数以上を消去すると false が返るため、その部分は代わりにnullを入れるものにしました。
とりあえず自分の環境ではこれでよしとしますが、Excelが左上から続く空列、空行を完全に無視してCSV化するのは困りもの。
余計な物をアップロードして変な値が入らないようにするために中身を確認して整合性を取る処理を入れてましたが、ちょっと考え直す必要がありそうです。