7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

League/Csvで5C問題を回避してCSVを読む

Posted at

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および読み込みの実装で再現できます。

data.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、これを解決するためにもストリームフィルタが利用できます。

参考文献

  1. https://csv.thephpleague.com/9.0/connections/controls/#the-escape-character

  2. 容量の大きいCSVファイルの扱い (Shift-JIS)

  3. PHP: fgetcsvでもSJISのCSVをUTF-8として《安全》に読む方法(ストリームフィルタ使用)

  4. PHP bug: escape characters not stripped by SplFileObject:fgetcsv · Issue #149 · thephpleague/csv · GitHub

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?