北村由衣のブログ

PHPでCSVの読み取り不具合

事象

PHP7.4.29環境で、以下のUTF-8エンコードされたCSVファイルを読み込んで処理させると、得られる解析結果に異常が見られます。

入力CSVファイル(ng.csv
"","尖","5C16","0101","1100","0001","0110"
"","答","7B54","0111","1011","0101","0100"
"","五","4E94","0100","1110","1001","0100"
"","近","8FD1","1000","1111","1101","0001"
実行するPHP
<?php
$csvFile = fopen("ng.csv", "r");
fseek($csvFile, 3); //BOM無視
while($line = fgetcsv($csvFile)) {
  for($idx = 0, $size = count($line); $idx < $size; $idx++ ){
    echo "[". $line[$idx] ."]";
  }
  echo "<br>";
}
fclose($csvFile);
?>
出力結果
[][尖",5C16"][0101][1100][0001][0110]
[][答",7B54"][0111][1011][0101][0100]
[][五",4E94"][0100][1110][1001][0100]
[][近",8FD1"][1000][1111][1101][0001]

漢字で終わっているCSVデータセルを囲っていたダブルクォーテーションマークがデータ文字列として残ってしまっています。

本事象はPHP7.4.29で再現します

発生条件

解析で不具合が発生する条件は、閉じの"の直前のマルチバイト文字にあります。 文字コードのビット数値列の後ろから5-6桁目、16進数表記3文字目に対応するバイト列の下2桁bitが01であること、がトリガーです。

対策

PHPのバージョンを8.xへアップデートしてください。事象が解消されています。

7.4.29のままで事象に対してフォローアップするような処理は申し訳ありませんが見当つきませんでした


以下は本件の発見と事象発生条件の探求記録です

事象の発見と検証

PHP7.4.29環境でCSVファイルをHTMLの<table>表示に変換する処理を書いていました。 710レコードのデータのうち、いくつもの漢字表記のセルで上記事象に示したような変換エラーが生じていました。 特定の漢字の場合は必ずエラーが生じているようだったので、探求を開始。
どんな文字で事象が起きているのか、文字コードを容疑者として追及することにし、UTF-8文字コードと突合しました。

実データの正常パターンとエラーパターンを拾っていると、ある程度法則性が見つかり、 「0000 0000 0011 0000」の11部分のビット並びが「01」の時、事象が発生するようだ、と判断しました。

最終的な検証CSV
"","言","8A00","1000","1010","0000","0000"
"","訐","8A10","1000","1010","0001","0000"
"","訠","8A20","1000","1010","0010","0000"
"","訰","8A30","1000","1010","0011","0000"
"","詀","8A40","1000","1010","0100","0000"
"","詐","8A50","1000","1010","0101","0000"
"","詠","8A60","1000","1010","0110","0000"
"","詰","8A70","1000","1010","0111","0000"
"","誀","8A80","1000","1010","1000","0000"
"","誐","8A90","1000","1010","1001","0000"
"","誠","8AA0","1000","1010","1010","0000"
"","誰","8AB0","1000","1010","1011","0000"
"","諀","8AB0","1000","1010","1100","0000"
"","諐","8AD0","1000","1010","1101","0000"
"","諠","8AE0","1000","1010","1110","0000"
"","諰","8AF0","1000","1010","1111","0000"
出力結果
[][言][8A00][1000][1010][0000][0000]
[][訐",8A10"][1000][1010][0001][0000]
[][訠][8A20][1000][1010][0010][0000]
[][訰][8A30][1000][1010][0011][0000]
[][詀][8A40][1000][1010][0100][0000]
[][詐",8A50"][1000][1010][0101][0000]
[][詠][8A60][1000][1010][0110][0000]
[][詰][8A70][1000][1010][0111][0000]
[][誀][8A80][1000][1010][1000][0000]
[][誐",8A90"][1000][1010][1001][0000]
[][誠][8AA0][1000][1010][1010][0000]
[][誰][8AB0][1000][1010][1011][0000]
[][諀][8AB0][1000][1010][1100][0000]
[][諐",8AD0"][1000][1010][1101][0000]
[][諠][8AE0][1000][1010][1110][0000]
[][諰][8AF0][1000][1010][1111][0000]

一部パターンに沿わない文字も存在していました。
「儚」(511A=0101 0001 0001 1010)エラーにならない
「久」(4E45=0100 1110 0100 0101)エラーになってしまう
規則性に外れたものはよく分からないままです。


このエントリーをはてなブックマークに追加


コメントはこちらからお寄せください