はじめに
シェルスクリプトでのデータ読込処理の実験のために手軽に出力フォーマットを変更できるやつが欲しかったので CSV パーサーを書きました。一応 POSIX 準拠 の awk で動いて(GNU 拡張は使っていません) RFC 4180 対応のつもり(フィールドの中にカンマや改行やダブルクォートや制御文字が入っていてもちゃんと動くはず)。ただし最低限の動作確認しかしてないので自己責任で。ライセンスは CC0 としますので、コピペして使うなり、加工して自分のコードとして使うなり、ご自由にどうぞ。
コード
RFC 4180 との違い
- 改行コードは CR LF だけでなく LF にも対応しています。
- 行末のカンマを受け付けます(最後に空フィールドが有るとみなされる)。
# CSV の論理的な一行毎に呼び出されるコールバック関数(サンプル)
# row: 行番号(最初の行は1) len: フィールド数 fields: フィールドデータ
function callback(row, len, fields, col, field) {
for ( col = 1; col <= len; col++ ) {
field = fields[col]
gsub(/\\/, "&&", field) # \ は \\ にエスケープする
gsub(/\n/, "\\n", field) # 改行は \n にエスケープする
print row ":" col " " field
}
}
BEGIN { row = max_idx = 0 }
{
idx = 0
sub(/\r$/, "")
while ($0 != "") {
if (match($0, /^(["]([^"]|["]["])*["]|[^,"]*)(,|$)/)) {
fields[++idx] = ""
} else if (match($0, /^["]/)) {
fields[++idx] = substr($0, 2)
for (;;) {
if (getline == 0) exit 1
sub(/\r$/, "")
if (match($0, /^([^"]|["]["])*["](,|$)/)) break
gsub(/["]["]/, "\"")
fields[idx] = fields[idx] "\n" $0
}
fields[idx] = fields[idx] "\n"
} else {
exit 1
}
field = substr($0, RSTART, RLENGTH)
$0 = substr($0, RSTART + RLENGTH)
lastcomma = sub(/,$/, "", field) > 0
gsub(/^["]|["]$/, "", field) && gsub(/["]["]/, "\"", field)
fields[idx] = fields[idx] field
}
if (lastcomma) fields[++idx] = ""
while (idx < max_idx) delete fields[max_idx--]
callback(++row, idx, fields)
max_idx = idx
}
使い方
awk -f csvparser.awk data.csv
こういう CSV ファイルが
aaa,bbb,ccc
"d,d,d","e""","""","""e","f\f\f"
"AAA
BBB
CCC
",,D D D,
"
E
E"
このように出力されます。(形式 行番号:列番号 フィールド値
)
1:1 aaa
1:2 bbb
1:3 ccc
2:1 d,d,d
2:2 e"
2:3 "
2:4 "e
2:5 f\\f\\f
3:1 AAA\nBBB\nCCC\n
3:2
3:3 D D D
3:4
4:1 \nE\nE
出力形式は awk の callback
関数で整えているだけなので、必要に応じて変更してください。
さいごに
余計なことはしてないのでそこそこ速いと思いますが gawk は結構遅いですね。実は正規表現使わずに実装した方が速いんじゃないかって思っています。
さて改行が含まれる CSV データをシェルスクリプトで読み取って処理しようとした時、一体どういうフォーマットを使えば一番効率的(高速)に処理できるんでしょうね。その実験をするのがこのコードを書いた目的です。わざわざ書かないで既存のコードを使えばいいじゃないか?と思うかもしれませんが既存の奴はやけにコードが長かったり出力を加工しづらかったりで目的に合わなかったり、ちゃんと文字列をパースせずに sed
や tr
で場当たり的に処理しているようなものは、特定の文字を扱えなかったりダブルクォート周りのバグで信頼性に疑問があったりで、修正するよりも書いたほうが早かったので。まあこのコードもバグがあるかもしれませんが。あと正規表現使って CSV パースしたことなかったのでやってみたかったのと、POSIX コマンドしか使えない環境でも動くようにしたいとか、そういう理由もありました。