csvの内容を登録する機能を実装していた時に詰まった話です。
結論
BOMに気をつけましょう
何が起きたか
実装内容
csvのヘッダーを比較して項目に過不足がないか確認する為に下記のような実装を行なった。
<?php
class test{
private const CSV_HEADERS = [
'No.',
'姓',
'名',
]
public function handle(){
// csvのヘッダー項目を要素にもつ配列 $firstLine が存在する想定
// 例: $firstLine = ['No.','姓', '名'];
if(!empty($missingHeader = array_diff(self::CSV_HEADER, $firltLine))){
// $missingHeader に存在しないヘッダー項目があるので出力する
}
// 以下 array_diff() の引数の順序を変更して余計な項目が存在しないかチェック
}
}
ローカルでも動作確認が取れていたのですが、実際にテストを行なってもらうとエラーとなる場合があるようでした。
エラー内容
エラー内容は No. が存在しません
という内容の出力でした。
該当のテストで使用されたcsvはexcelで文字コードUTF-8を指定して出力したものでした。
原因特定の為に何をしたか
① 文字コードのチェック
file --mime sample.csv
# => sample.csv: application/csv; charset=utf-8
UTF-8でした
② 文字列比較時の文字コードチェック
ヘッダー項目1つずつに対して、 mb_etect_encoding()
を使用して、文字コードを確認しました。
$defEnc = mb_detect_encoding($def); // php内定義のヘッダー項目
$actualEnc = mb_detect_encoding($actual); // csvの項目のヘッダー項目
var_dump($defEnc);
// => string(5) "ASCII"
var_dump($actualEnc);
// => string(5) "UTF-8"
ここで1つ目の項目 No. の文字コードが違うことに気づきます。
ここで文字コードに関して知識があれば理由がわかったと思いますが、
自分は文字コード何もわからん状態だったので無理矢理 UTF-8 の No. をASCIIに変更する実装をしました。
どうなった
次の日に他の人がエラーに関するやりとりを見ており、バイナリから BOM が原因だということを知りました。
BOMとは?
BOM は バイト順マークのことです。
プログラムがテキストデータを読み込む時、その先頭の数バイトからそのデータがUnicodeで表現されていること、また符号化形式(エンコーディング)としてどれを使用しているかを判別できるようにしたものである。
BOMがファイルの頭についていたのが原因でした。
なぜ気づかなかったか
エディタやPHP DEBUG などで処理を止めて確認しても目視できない為
どうやって確認すれば良いか
ファイルのバイナリを見て確認ができます。
hexdump sample.csv
# => 0000000 ef bb bf ・・・
hexdump した 最初の3バイトが ef bb bf となっています。
これは UTF-8 のBOMです。
文字コードによってBOMは変わります。
結果
BOM削除処理を挟んだら正しく処理が通るようになりました。
<?php
class test{
private const CSV_HEADERS = [
'No.',
'姓',
'名',
]
public function handle(){
// csvのヘッダー項目を要素にもつ配列 $firstLine が存在する想定
// BOM削除処理
$bom = hex2bin('EFBBBF');
$firstLine[0] = preg_replace("/\A{$bom}/", '', $firstLine[0]);
if(!empty($missingHeader = array_diff(self::CSV_HEADER, $firltLine))){
// $missingHeader に存在しないヘッダー項目があるので出力する
}
}
}
まとめ
これを機に少しだけ文字コードに関して調べて、UTF-8 と ASCII の関係など文字コードに関して少しだけ理解が深まった気がします。
ありがとうBOM(嘘)
そもそも UTF-8 は ASCII と同じ部分は1バイトで扱うので本当に同じ文字なら比較で false になることはないということをあらかじめ知っていればこんなことにはならなかった。