CSVでカンマセパレータだけを置換する(フィールド内カンマはスルー)・・・SED版
この記事でわかること。
・sedを使ってカンマセパレータを別文字に置換する方法
「CSVでカンマセパレータだけを置換する(フィールド内カンマはスルー)」の記事で紹介した正規表現は、sedでは使えない。 sedは先読みとか後読みに対応していないためだ。 別の方法でCSVのカンマセパレータの置換方法を検討する。 ロジックを作り上げるプロセスを載せて備忘録とし、今後の応用に活かしたい。
まずはテストデータと期待結果を再掲載する。
テストデータ:a,"b,c",d,"e,f,g",h
期待する結果:a@"b,c"@d@"e,f,g"@h
問題を単純化するために、一旦テストデータをシンプルなものに変更する。
テストデータ:a,"b,c",h
期待する結果:a@"b,c"@h
最初に、左端のカンマを置換するsedを考えて、以下のように試してみた。
1$ echo -e 'a,"b,c",h' | sed -r 's/^.*,/@/'
2@h
先頭から h の直前のカンマまで置換されてしまった(失敗)。
メタキャラクタを使って、置換してはいけない部分の出力を試みる。
1$ echo -e 'a,"b,c",h' | sed -r 's/^(.*),/\1@/'
2a,"b,c"@h
よくなったが、左端のカンマが置換されていない(失敗)。
最短マッチさせるため、? を使ってみる。
1$ echo -e 'a,"b,c",h' | sed -r 's/^(.*)?,/\1@/'
2a,"b,c"@h
出力結果は変わらない(失敗)。
sedは最短マッチに ? を使えないことを思い出した。
否定の文字クラスを使って最短マッチさせる。
1$ echo -e 'a,"b,c",h' | sed -r 's/^([^,]*),/\1@/'
2a@"b,c",h
左端のカンマを置換できた(OK)。
次はダブルクォートで括られた中のカンマはスルーして、セパレータである h の左隣りのカンマの置換を目指す。
ここで、テストデータを上記出力結果 a@"b,c",h
とすることで問題を簡単にする。以下を試してみた。
1$ echo -e 'a@"b,c",h' | sed -r 's/^(([^,"]*)("[^"]*")?),/\1@/'
2a@"b,c"@h
うまくいった(OK)。さらに冗長と思われる()を削除して試してみる。
1$ echo -e 'a@"b,c",h' | sed -r 's/^([^,"]*("[^"]*")?),/\1@/'
2a@"b,c"@h
同じ結果が得られたので、やはり冗長であった。
それではテストデータを元の a,"b,c",h
に戻して、上記正規表現で試してみる。
1$ echo -e 'a,"b,c",h' | sed -r 's/^([^,"]*("[^"]*")?),/\1@/'
2a@"b,c",h
右端のカンマが置換されていないが、実は想定通り。
目論んでいるのは、このアウトプットに対し繰り返し同じパターンで置換を 実行することだ。
そうすれば上で試したように a@"b,c",h
→ a@"b,c"@h
と置換されるはずだ。
繰り返し置換するには、tコマンドを使ってスクリプトの先頭に制御をジャンプさせる。以下を試してみた。
1$ echo -e 'a,"b,c",h' | sed -e ':a' -re 's/^([^,"]*("[^"]*")?),/\1@/' -e 'ta'
2a@"b,c"@h
うまくいった(OK)。さて、それでは本来のテストデータで試してみよう。
1$ echo -e 'a,"b,c",d,"e,f,g",h' | sed -e ':a' -re 's/^([^,"]*("[^"]*")?),/\1@/' -e 'ta'
2a@"b,c"@d,"e,f,g",h
dの左側までしか置換されていない(失敗)。パターンの繰り返しが漏れていたので追加して試してみた。
1$ echo -e 'a,"b,c",d,"e,f,g",h' | sed -e ':a' -re 's/^(([^,"]*("[^"]*")?)*),/\1@/' -e 'ta'
2a@"b,c"@d@"e,f,g"@h
OK。これで完全に期待通りの結果が得られた。
上記のように繰り返し制御を使用すれば、先読み・後読みができなくても対応可能性が高まる。
追記)QAサイトで以下の質問が投稿された。
カンマが空白に置き換わっただけの類似問題。
以下のような sed と awk の合わせ技で可能。sed -E '{:a;s/^(([^ "]*("[^"]*")?)*) /\1@/;ta}' | awk -F@ '{print NF}'
awk だけでも可能。正規表現を知っていれば応用が効く。awk '{ while(sub(r,"@"))i++ print i+1 }' r='^(([^ "]*("[^"]*")?)*) '