PerlでCSVをパースする時のメモ。
まず、CSVってのは、
名前のとおり「,」でデータを区切ったデータファイルだから、「,」を区切り文字にsplitすれば簡単にパースできるはずなんだが、
本来データ内に「,」を含む場合「,」はエスケープするのが筋と思うんだが、
エスケープせずに最初から文字列データをすべて「"」で囲んでるCSVが多いんですよね。
そんな糞仕様のCSVだと、区切り文字以外に「,」が含まれる可能性があるから単純に「,」でsplitはできない・・・
さらに!
"文字列1","文字列2"
な感じのCSVで、文字列の中にさらにエスケープせずに「"」が含まれる糞仕様のCSVまで・・・
こんな糞仕様だと、正規表現で抜き取ることもできませんね。
最初からダブルクォートで囲まずに、「,」をエスケープすればいいだけなのにこんなクソ仕様のCSV作るのは勘弁してほしい・・・
って感じだが、そんなCSV使いたいならどうにかしないと・・・
my(@list,$line,@line,$c,$s);
foreach $line(@file){
$line=~s/(?<=[^,])"(?=[^,\r\n])/&#quot;/g;
undef(@line);
$s='';
while(($c=substr($line,0,1,'')) ne ''){
if($s eq '' && $c eq '"'){
$line=~s/^([^"]*?)"[,\r\n]//;
push(@line,$1);
$s='';
next;
}
if($c eq ','){
push(@line,$s);
$s='';
next;
}
if($c eq "\r" || $c eq "\n"){
push(@line,$s) if($s);
last;
}
$s.=$c;
}
@{$list[$#list+1]}=@line;
}
@fileにはCSV全体の改行区切りの配列を入れる。(普通にCSVをopenして、@file=<FILE>のように)
$lineに1行ずつ入れてループ。
「先頭」又は「末尾」以外の「"」は&#quot;に変換。(HTML以外で使うんなら、後で戻す必要がありますね)
@lineが1行分のデータの配列とします。
1文字切り取って、$cに入れる。
先頭が「"」なら、次の「"」までをデータとして取り出して、次のデータへ。
「"」「,」以外なら、$sに文字を追加。
「,」又は改行が現れたら、$sをデータとして追加。
@listがCSV全体の2重配列としてパースが完了します。
$list[行番号][列番号]
って感じに。
ダブルクォートで囲まない、まともなCSVの中に先頭の「"」が出現するってことも考えられますから、これで無理な場合も考えられますけど、
だいたいこれでいけるんじゃないかと。