🦔

要素内に改行などを含む CSV を gawk で処理

2022/02/19に公開

AWKはテキスト処理には便利なツールで、項目区切りにカンマを指定(FS=",")すれば、簡単な CSV ファイルを処理できます。

しかしながら、RFC4180 の規格上の CSV ファイルでは、二重引用符で囲むことで区切り文字や改行コードを要素の中に含めることが出来ます。これを AWK で処理するのは一般に難しいとされています。

これに対して、最近の gawk では「二重引用符で囲まれたカンマ」については、区切り文字列ではなく、要素のパターンを指定する(FPAT="([^,]+)|(\"[^\"]+\")")ことで処理できるようになりました。あとは改行コードだけです。

本文書では、これを外部ツールの補助なしに gawk 5 のコードだけで処理する方法を説明します。

方針

  1. 改行コードが要素に含まれている場合、純粋なテキストファイル的に見ると、その行の二重引用符が閉じられていないように見える
  2. その場合、行に含まれる二重引用符の個数は奇数個になる
  3. 奇数個の場合は次の行を連結する
  4. 連結した上で、まだ二重引用符の個数が奇数個であれば、二重引用符の個数が偶数になるまで、行を連結し続ける
  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>
---------

だいたい意図通りのようです。

GitHubで編集を提案

Discussion