• ベストアンサー

awk 正規表現を使って置換

あるファイルの中の2列目に含まれる "ab" "ac" "ae" という文字列をそれぞれ "zb" "zc" "ze"に置き換えたいのですが、awkまたはsedで正規表現を使って効率のいい方法はないでしょうか。 awk ' { gsub(/ab/,"zb",$2); gsub(/ac/,"zc",$2); gsub(/ae/,"ze",$2); print }' testfile でやりたいことはできるのですが、「aの後にb,c,eが続く場合にaをzに置換する」というアイディアを使えばもっと効率のよいスクリプトが書けるはず、と思いつつ、awkの勉強を始めたばかりでなかなか思い浮かびません。 testfileの中身は以下の通り: abcde abaab aaaae acbec accee adabd dceba aeecs hhhgf sbacc 以下のような出力を望んでいます: abcde zbazb aaaae zcbec accee adzbd dceba zeecs hhhgf sbzcc awk ' BEGIN { var = "[bce]" } { gsub("a"var,"z"var,$2); print }' だと "zb" "zc" "ze"ではなくすべて"z[bce]"に置き換わってしまうし、 awk ' BEGIN { var = "[bce]" } { gsub("a"var,"z&",$2); print }' だと"zab" "zac" "zae"になってしまうし… まずはawk,sedで勉強したいと思っていますが、それ以外でもいい案がありましたら教えてください。よろしくお願いします。

質問者が選んだベストアンサー

  • ベストアンサー
  • kumoz
  • ベストアンサー率64% (120/185)
回答No.4

中間の空白が半角スペース1つに見えたものですから、タブとは思いませんでした。 最初のコードを少し変更してみました。[^ ] と [ ] は半角スペース1つにに見えるか も知れませんが、半角スペースとタブの両方を入れてください。これで、中間の空白が 半角スペースまたはタブ1つに対応できると思います。 sed ":loop; s/a\([bce]\)\([^ ]*\)$/z\1\2/; /[ ].*a[bce]/t loop" testfile 上のコードを行を分けて書き直すと、次のようになります。 :loop s/a\([bce]\)\([^ ]*\)$/z\1\2/ /[ ].*a[bce]/t loop 最後の行の /.../t loop というのは、正規表現にマッチする場合はラベル loop にジャンプ せよという意味です。正規表現に指定してあるのが 「空白文字に続く文字列中に a[bce] が 存在する場合」ですので、2番目の文字列中の置き換えが終了するまでループが続きます。 Perl の場合は、もっと簡単に書くことができます。試してみてください。 perl -ple "s/a([bce])(?!.*\s)/z$1/g" testfile

drmel
質問者

お礼

ありがとうございます!!! Perlのスクリプトは、1番目の方の回答をいただいてから自分なりに挑戦していたのですが、何行にも渡った挙句、なかなか目的が達成できずにいました。それが、こんなシンプルな1行でできてしまうとは、目からウロコです。 sedのloopに関してもとてもわかりやすくご説明いただきありがとうございました。よく理解できました。 同じことを実現するのでも、言語によって記述が違うだけでなくアプローチも違うのですね。これからもっといろいろ勉強しようと意欲がわいています。皆さんありがとうございました。

その他の回答 (3)

  • kumoz
  • ベストアンサー率64% (120/185)
回答No.3

ループを使ってみました。 sed ":loop; s/a\([bce]\)\([^ ]*\)$/z\1\2/; / .*a[bce]/t loop" testfile

drmel
質問者

お礼

ありがとうございます。 でもこれだと各行最初に出てきたab ac aeのみが置き換わってしまうようです… 例えば最初の行はabcde zbazbではなくzbcde abaabになってしまいます。

drmel
質問者

補足

先ほど下の欄に「回答へのお礼」を記入したのですが、その後試行錯誤してみた結果、 sed ":loop; s/a\([bce]\)\([^ ]*\)$/z\1\2/; / .*a[bce]/!t loop" testfile [^ ] をスペースではなくタブに変換し、t loopを !t loopに変換したところうまくいきました!loopの仕組みがよくわかっていないのですが、/ .*a[bce]/でなくなったら「:loop」に戻る、つまりその行にabもacもaeもなくなったら行頭に戻る、という意味でしょうか?(←よくわかっていない...)

noname#227025
noname#227025
回答No.2

効率がよいかどうかは分かりませんが、sed ならホールドスペースを使えばできます。 例) sed 'h; s/.* \(.*\)/\1/; s/a\([bce]\)/z\1/g; G; s/\(.*\)\n\(.*\) .*/\2 \1/' testfile

drmel
質問者

お礼

早速にご回答ありがとうございます。 sedのホールドスペースというのは知りませんでした。教えていただいたスクリプトそのままだと各行2回ずつ(置換前と置換後)、しかも1列目と2列目で1行ずつずれて出力されてしまいますが、ホールドスペースについてもうちょっと調べて自分で調整してみます。

  • notnot
  • ベストアンサー率47% (4900/10358)
回答No.1

sed であれば、sed 's/a\([bcd]\)/z\1/g' というようなことが出来ますが、こんどは第二カラムだけ対象というのが難しい。Perlのような高機能な物を使う手もあります。 awkだと、このように配列を使うくらいかな。 awk 'BEGIN{split("b c d",a,/ /)}{for(i in a) gsub("a"a[i],"z"a[i],$2);print}'

drmel
質問者

お礼

早速にご回答ありがとうございます。 なるほど、sed 's/a\([bcd]\)/z\1/g'のようなシンプルな形でawkでできればいいなと思っていましたが、awkの場合は配列を使わなければならないのですね。 ご回答いただいてからPerlについてちょっと調べてみましたが、なるほどパワフルなようですね。これまで回りくどい方法で書いてきたスクリプトもすっきりまとめられそう。これから勉強してみようと思います。 Perlだったら2列目だけ置換というのも可能ですか?どのようにすればいいかご教示いただければ幸いです。