PHP str_getcsv/fgetcsvのバグ・不具合について【謎の空文字・CSV読み込みバグ】

PHPのstr_getcsv/fgetcsv関数では、不正な入力が行われると、予想外の結果が返される場合がある。

具体的には「1バイトの空文字」が返されたり、「改行コードやカンマが混じったフィールド」が返されたりする。

場合によっては「NULLフィールドが返される」ケースや、「レコードが読み込まれない」問題に遭遇することもある。

本記事ではそれぞれの問題の原因と対処方法について解説していく。

目次

str_getcsvで1バイトの空文字が返される問題

str_getcsv関数には謎の1バイト文字が混入するバグがある。環境によっては、空の1バイト文字や、文字化けした1バイト文字などが返される。

$r = str_getcsv('a,"');
var_dump($r); // [string(1) "a", string(1) ""]

この原因は入力文字列の末尾に単体のダブルクォーテーションが挿入されることによって生じるstr_getcsv関数側の不具合である。

ダブルクォーテーションは開始と閉じのペアで用いなければならない。

入力文字列の末尾に意図しない単体のダブルクォーテーションが用いられると、str_getcsv関数はダブルクォーテーション以降の文字列を機械的に読み取ろうとしてしまい、結果として文字列の終端記号であるヌル文字("\0" ASCII code 0《0x00》)を読み取ってしまう。おそらくこれはPHP標準関数側のバグや未定義動作の類ではないかと考えられる。

よってこのヌル文字が謎の1バイト文字(string(1) "")の正体である。

対処方法

CSVの行末のフィールド(項目)にダブルクォーテーションを文字として含めたい場合には「""""」という形でダブルクォーテーションを四つ並べて記述すれば良い。

var_dump(str_getcsv('"a", """"')); // [string(1) 'a', string(1) '"']
四つのダブルクォーテーションの内、両側の二つはCSVの囲み文字であり、残りの二つはエスケープ文字とダブルクォーテーション文字である。詳しくは「CSVのフィールド内にダブルクォーテーションを記述する方法」を参考にされたい。

入力文字列が意図的なものである場合、このstr_getcsv関数の不具合に対処するには、各フィールドに対してヌル文字チェックを行うなどして、対象のフィールドを手動で削除するか、あるいは空のフィールドとして見なすなどして対処する必要がある。前者の場合はarray_filter関数、後者の場合はarray_map関数を用いると良いだろう。

$r = str_getcsv('a,"');
var_dump($r); // [string(1) "a", string(1) ""]

$r = array_filter($r, function ($v) { return "\0" != $v; });
var_dump($r); // [string(1) "a"]
$s = ' a , "';

var_dump(
  array_map(function ($v) { return "\0" == $v ? '' : $v; }, str_getcsv($s))
); // [string(3) " a ", string(0) ""]

var_dump(
  array_map('trim', str_getcsv($s))
); // [string(1) "a", string(0) ""]

fgetcsvで改行コードやカンマが混じったフィールドが返される問題

CSVファイル側のデータ表現に誤りがあると、fgetcsv関数は改行コードやカンマを含んだフィールドを返してしまう場合がある。

$r = fgetcsv(fopen("sample.csv", "r"));
var_dump($r); // [string(1) "a", string(1) "\nb,c"]

sample.csv

a,"
b,c

これは一見するとfgetcsv関数側のバグのようにも思われるが、そもそも多くのCSV実装には、改行やカンマを含むフィールドを容認するケースがあるため、これはバグなどではなく、一般的な動作仕様に基づいたものでもある。

以下のファイルも同様に["abc", "A\nBC", "xyz"]という単一のレコード(行)として解釈される。

abc,"A
BC",xyz

CSVではダブルクォーテーションは「囲み文字」として解釈される。それによって開始ダブルクォーテーションから閉じダブルクォーテーションまでは一つのフィールドとして認識される。ダブルクォーテーションで囲まれた「改行コード」や「カンマ」がフィールド内のテキストとして解釈されてしまうのはそのためである。

もっとも、冒頭の例では閉じダブルクォーテーションが記述されていないため、そちらについては未定義動作が引き起こされてしまっていると考えられる。
なおCSVファイルの末尾を開始ダブルクォーテーションで終えてしまった場合には、レコードそのものが読み込まれなくなる問題に遭遇する。

ダブルクォーテーションをフィールド内の文字として記述したい場合には、次々項の# CSVのフィールド内にダブルクォーテーションを記述する方法を参考にすると良い。

str_getcsv/fgetcsvでNULLフィールドが返される問題

str_getcsv関数の引数に空文字や改行文字を渡すと、当関数はNULLを格納した配列を返してしまう。

str_getcsv('')   // array(1) { [0] => NULL } // fgetcsvの場合はFALSE
str_getcsv("\n") // array(1) { [0] => NULL } // fgetcsvと共通の動作

fgetcsv関数でも同様に、改行のみが記述された空の行では、nullフィールドを一つだけ含む配列が返される。これはfgetcsvの仕様である。ただしstr_getcsvでは、この空行の動作はドキュメント化されていない。

str_getcsvに空文字を渡した際にnullフィールドが返される動作については、未定義動作もしくはエラーや例外を示すものであると考えられる。なおfgetcsvで改行コードを含まない空行を読み取った際には、FALSEが返される。

CSVのフィールド内にダブルクォーテーションを記述する方法

CSVのフィールドにダブルクォーテーション文字そのものを含めたい場合には、ダブルクォーテーションを二つ並べて「""」と記述する。なおその場合、対象のフィールドはダブルクォーテーションで囲まなければならない。

aa, "b""b", cc // 入力後の結果:['aa', 'b"b', 'cc']

四つのダブルクォーテーションの内、両側の二つはCSVの囲み文字として解釈され、残りの二つはエスケープ文字とダブルクォーテーション文字として解釈されるようになる。

広告