1,"2","wao""gao""pao"
Googleの上位にヒットするような正規表現を拾ってきて試すと、最後の"wao以下がすべて無視されてしまう。おれはこれが気に入らない。1,"2","wao\"gao"pao",gaogao!
確かに、こんなの解析できなくてもいいじゃないか!と言われそうだが、テキストというものは何でもありの、汚いデータなのだから、常にきっちりと可能な限りデータを拾ってもらいたいのである。そうでなければ気が済まないのだ。,,,,,,
こういう書き方はよくあって、長さ0の文字列でしかもクォートがない場合である。これを、「フィールド自体がない」と判定されては困るのである。#
# テストデータの作成
#
push @testData, <<'EndOfCSV1';
1,"2","wao""gao""pao"
EndOfCSV1
push @testData, <<'EndOfCSV2';
1,"2","wao\"gao"pao",'gaogao!'
EndOfCSV2
push @testData, <<'EndOfCSV3';
Ten Thousand,10000, 2710 ,,"10,000","It's ""10 Grand"", baby",10K
EndOfCSV3
foreach ( @testData ) {
execTest($_);
}
#
# テストの実行
#
sub execTest () {
my $csvString = shift;
chomp($csvString);
print STDOUT "[$csvString]\n";
$idx = 1; print STDOUT "parse_CSV0\n";
print STDOUT join ( "\n", map { $idx++.'=['.$_.']' } parse_CSV0($csvString) )."\n";
$idx = 1; print STDOUT "parseCSV0\n";
print STDOUT join ( "\n", map { $idx++.'=['.$_.']' } parseCSV0($csvString) )."\n";
$idx = 1; print STDOUT "parseCSV1\n";
print STDOUT join ( "\n", map { $idx++.'=['.$_.']' } parseCSV1($csvString) )."\n";
$idx = 1; print STDOUT "parseCSVX\n";
print STDOUT join ( "\n", map { $idx++.'=['.$_.']' } parseCSVX($csvString) )."\n";
print STDOUT "\n";
}
# parse_CSV0
# ダブルクォーテーションのエスケープは\"であるタイプ用。
# 標準モジュールText::ParseWordsを利用。
# 特徴:
# 項目中に囲み文字が現れただけでは、項目の終わりとは判断せず、有効な区切り文字が続くときにだけ切る…ように見えるが、
# それは囲み文字が偶数ペアになっているときだけで、不正な囲み文字が単独で現れると混乱して、結果が空になることさえある。
# @param $ CSV文字列
# @return @ 分解された配列
sub parse_CSV0 {
use Text::ParseWords;
my $csvString = shift;
# quotewords の第二引数を0にした場合、文字列中の""なども除去されてしまうので、いったんそのまま返してもらい、
# 自前で前後のクォートを外している。
#my @fields = map { s/""/"/g; s/(?:^"|"$)//g; $_; } quotewords( ',', 1, $csvString );
#my @fields = map { s/(?:^"|"$)//g; $_; } quotewords( ',', 1, $csvString );
my @fields = map { s/(?:^"|"$)//g; $_; } quotewords( ',', 1, $csvString );
return @fields;
}
# parseCSV0
# ダブルクォーテーションのエスケープが\"であるタイプ用。
# 詳説正規表現からの引き写し。
# 標準モジュールをが利用できない環境でも使えるよう、自前で実装。
# 特徴:
# 項目中に囲み文字が現れると、項目の終わりと判断する。次に有効な区切り文字が見つかるまでの部分は、捨てられる。
# @param $ CSV文字列
# @return @ 分解された配列
sub parseCSV0 {
my $csvString = shift;
my $field, @fields = ();
while ( $csvString =~ m{
(?:^|,)
(?:
# (1) エスケープありの場合
"( (?: [^"\\]+|\\.)* )"
|
# (2) エスケープなし
( [^",]* )
)
}gx ) {
if ( defined $2 ) {
# (2) エスケープなし
$field = $2;
} else {
# (1) エスケープありの場合
( $field = $1 ) =~ s/""/"/g;
}
push @fields, $field;
}
return @fields;
}
# parseCSV1
# ダブルクォーテーションのエスケープは""であるタイプ用
# 詳説正規表現からの引き写し。
# 標準モジュールText::ParseWordsでは解析できないので、自前で実装。
# 特徴:
# 項目中に囲み文字が現れると、項目の終わりと判断する。次に有効な区切り文字が見つかるまでの部分は、捨てられる。
# @param $ CSV文字列
# @return @ 分解された配列
sub parseCSV1 {
my $csvString = shift;
my $field, @fields = ();
while ( $csvString =~ m{
(?:^|,)
(?:
# (1) エスケープありの場合
"( (?: [^"]|"")* )"
|
# (2) エスケープなし
( [^",]* )
)
}gx ) {
if ( defined $2 ) {
# (2) エスケープなし
$field = $2;
} else {
# (1) エスケープありの場合
( $field = $1 ) =~ s/""/"/g;
}
push @fields, $field;
}
return @fields;
}
# parseCSVX
# ネットで拾った正規表現。比較用。
# 特徴:
# 一見よさそうなのだが、囲み文字が乱れている場合に最後の項目をとりこぼしてしまう。
# Ten Thousand,10000, 2710 ,,"10,000","It's ""10 Grand"", baby",10K
# の10kを取りこぼしたり…
# @param $ CSV文字列
# @return @ 分解された配列
sub parseCSVX {
my $csvString = shift;
#my $field, @fields = ();
return map {/^"(.*)"$/s ? scalar($_ = $1, s/""/"/g, $_) : $_} ($csvString =~ /("[^"]*(?:""[^"]*)*"|[^,]*),/g);
}
たぶん、自分が完全に気に入るカンマ解釈は、正規表現より、一文字ずつ愚直にサーチするロジックの方がスッキリ書けそうなことは判っている。