🦔
要素内に改行などを含む CSV を gawk で処理
AWKはテキスト処理には便利なツールで、項目区切りにカンマを指定(FS=","
)すれば、簡単な CSV ファイルを処理できます。
しかしながら、RFC4180 の規格上の CSV ファイルでは、二重引用符で囲むことで区切り文字や改行コードを要素の中に含めることが出来ます。これを AWK で処理するのは一般に難しいとされています。
これに対して、最近の gawk では「二重引用符で囲まれたカンマ」については、区切り文字列ではなく、要素のパターンを指定する(FPAT="([^,]+)|(\"[^\"]+\")"
)ことで処理できるようになりました。あとは改行コードだけです。
本文書では、これを外部ツールの補助なしに gawk 5 のコードだけで処理する方法を説明します。
方針
- 改行コードが要素に含まれている場合、純粋なテキストファイル的に見ると、その行の二重引用符が閉じられていないように見える
- その場合、行に含まれる二重引用符の個数は奇数個になる
- 奇数個の場合は次の行を連結する
- 連結した上で、まだ二重引用符の個数が奇数個であれば、二重引用符の個数が偶数になるまで、行を連結し続ける
- 行の連結が終了した時点で、項目を FPAT で分割し、本体の処理を始める
ここで面倒なのが、二重引用符の数を数えるところです。当初は地道に
function countq(s, c,i){
c = 0
while( (i=index(s,"\"")) > 0 ){
c++
s = substr(s,i+1)
}
return c
}
という関数を作っていたのですが、よくよく考えると split 関数で二重引用符をデミリタとして指定して分割すれば、その要素数マイナス1が二重引用符の数でした。ただし、空文字列の場合は要素数0になるので特別扱いにしなくてはいけないようです。
やってみました。
BEGIN{
FPAT="([^,]+)|(\"[^\"]+\")"
}
{
last = last $0
if ( last != "" && split(last,trash,"\"") % 2 == 0 ){
last = last RS
next
}
$0 = last ; last = ""
}
# test
{
for(i = 1 ; i <= NF ; i++ ){
printf "<%s>",gensub("\"","","g",$i)
}
print ""
print "---------"
}
比較的コンパクトに書けました。これならコピペでの転用もできそうです(無論コピー自由)
使用しているグローバル変数は last と tmp 。last は継続扱いになった前行までを記憶するバッファ。tmp は split で分割結果を格納するダミー配列名です(中身は使わない)。
gensub は最近の gawk で追加された組み込み関数で、元データを破壊せず、置換結果を戻り値で得るバージョンの gsub みたいなものです。
さて、うまく動作するでしょうか。
$ type sample.csv
A1,B1,C1,"D1-1
D1-2
D1-3"
A2,"B2-1
B2-2","C2-1
C2-2",D2
"A3-1
A3-2",B3,C3,"D3-1
D3-2"
$ gawk -f richcsv.awk sample.csv
<A1><B1><C1><D1-1
D1-2
D1-3>
---------
<A2><B2-1
B2-2><C2-1
C2-2><D2>
---------
<A3-1
A3-2><B3><C3><D3-1
D3-2>
---------
だいたい意図通りのようです。
Discussion