PHPのCSV操作ライブラリLeague/Csvでは文字コードがShift-JISのCSVを読むことができますが、5C問題のためにフィールドがうまく区切られない場合があります。5C問題はShift-JIS固有の問題で、バックスラッシュとして解釈される0x5c
が一部の文字コードの先頭に含まれることに起因する問題を指します。これを発生させる文字はいわゆるダメ文字として知られており、例として「貼」「表」「能」などが挙げられます。
この記事では、League/Csvを利用してCSVを読む際に5C問題を回避する方法について記します。記事中で利用するLeague/Csvのバージョンは9.8.0、PHPのバージョンは8.1.0となります。
フィールドをうまく区切れない例
読み込むCSVファイルが
- 文字コードがShift-JISである
- ダメ文字がエンクロージャの直前で用いられている
の条件をすべて満たす場合にフィールドがうまく区切れない現象が発生します。具体的には次のようなCSVおよび読み込みの実装で再現できます。
"0160841","養蚕","ようさん","能代市","秋田県"
"4370061","久能","くの","袋井市","静岡県"
"9071542","西表","いりおもて","八重山郡竹富町","沖縄県"
<?php
use League\Csv\CharsetConverter;
use League\Csv\Reader;
$csv = Reader::createFromPath($path, 'r');
$encoder = (new CharsetConverter())->inputEncoding('SJIS');
$records = $encoder->convert($csv);
$data = [];
foreach ($records as $record) {
$data[] = $record;
}
print_r($data);
Array
(
[0] => Array
(
[0] => 0160841
[1] => 養蚕",ようさん"
[2] => 能代市
[3] => 秋田県
)
[1] => Array
(
[0] => 4370061
[1] => 久能",くの"
[2] => 袋井市
[3] => 静岡県
)
[2] => Array
(
[0] => 9071542
[1] => 西表",いりおもて"
[2] => 八重山郡竹富町
[3] => 沖縄県
)
)
ここで「蚕」「能」「表」はいずれもダメ文字で、久能",くの"
のように、きちんと区切られていないことが分かります。"能代市"
のように、ダメ文字が含まれていてもエンクロージャの直前でない場合はフィールドは正常に区切られます。
回避方法1: エスケープ文字に\
を利用しない場合
League/Csvではデフォルトのエスケープ文字が\
です1。したがって、フィールドが区切られない原因としてダメ文字に含まれる0x5c
が直後のエンクロージャーをエスケープしている、つまり"久能","くの"
を"久能\","くの"
と解釈していると考えられます。そこで、エスケープ文字を\
以外のものにすることで回避します。
<?php
use League\Csv\CharsetConverter;
use League\Csv\Reader;
$csv = Reader::createFromPath($path, 'r');
$csv->setEscape(''); // エスケープ文字を設定
$encoder = (new CharsetConverter())->inputEncoding('SJIS');
$records = $encoder->convert($csv);
$data = [];
foreach ($records as $record) {
$data[] = $record;
}
フィールドをうまく区切れない例の実装に$csv->setEscape('')
を追加し、エスケープ文字を空文字に設定しています。エスケープ文字を空文字にするとRFC4180に従った形式となり、"
は""
としてエスケープできます。
回避方法2: エスケープ文字に\
を利用する場合
読み込むCSVで\
をエスケープ文字に利用している場合は上記の方法は利用できません。その場合はCharsetConverter
の代わりにmb_convert_encoding
で文字コードの変換を行ったうえで読み込みます。
<?php
use League\Csv\Reader;
$data = mb_convert_encoding(file_get_contents('data.csv'), 'UTF-8', 'SJIS');
$csv = Reader::createFromString($data);
$records = $csv->getRecords();
$data = [];
foreach ($records as $record) {
$data[] = $record;
}
CSVファイルの容量が大きい場合、この方法ではメモリ使用量が増えるため注意が必要です2。容量の大きいファイルを扱う場合はストリームフィルタを利用する方法があります3。また、エスケープ文字が削除されないSplFileObjectのバグも報告されており4、これを解決するためにもストリームフィルタが利用できます。