2008年11月22日土曜日

正規表現、先読みと後読み

正規表現でテキスト処理をする際、いちばん分かりにくくて、いちばん役に立ったのが先読み後読み。どうもここが正規表現のキモのような気がする。
そういうわけでメモ。

Groovyの場合、肯定的先読みは(?=pattern)、肯定的後読みは(?<=pattern)と書く。
patternの部分は判別させたい文字列。

先読み、後読みは何に使うのかというと、「文字列の前と後にどんな文字が来るか判別したくて、なおかつその部分にはマッチさせたくない」という場合に使う。
たとえば、「hoge(?=pattern)」は patternが後ろにあるhogeにマッチするが、(?=pattern)部分にはマッチしない。
「(?<=pattern)hoge」は patternが前にあるhogeにマッチするが、(?<=pattern)部分にはマッチしない。

マッチしたい文字の前に後読み、後ろに先読みを使うのがミソ。
順番でいうと(後読み)(欲しい部分)(先読み)と使う。
ここまでOK?

実例。下のような文章があったとする。
長嶋 茂雄の誕生日は1936年2月20日。
1974(昭和11)年2月20日から
2008(平成20)年11月20日の間の日数は26,572日、
72年と274日になります。ところで、ただいまの時間は7:19です。

「ところで」の部分が苦しい…のは置いといて、
このテキストの「1桁と2桁の数字だけ<tcy></tcy>というタグで囲みたい」
どうしよう?

まず、1桁と2桁の数字だから、
\d{1,2}

だけど、これだと3桁以上の数字にもマッチしてしまうので、前後に数字がない場合だけマッチさせたい。
(?<=[^\d])(\d{1,2})(?=[^\d])

また、コンマ区切りと小数点、時間を表しているコロンがあった場合もマッチさせたくない。
(?<=[^\d\.,:])(\d{1,2})(?=[^\d\.,:])

この文章にはないけれど、行頭もしくは行末に数字が来るかもしれない。
(?<=^|[^\d\.,:])(\d{1,2})(?=[^\d\.,:]|$)


これで、数字2桁までの「数字部分にだけ」マッチする。
以下Groovyスクリプト。
def text="""長嶋 茂雄の誕生日は1936年2月20日。
1974(昭和11)年2月20日から
2008(平成20)年11月20日の間の日数は26,572日、
72年と274日になります。ところで、ただいまの時間は7:19です。"""

//半角1,2桁の数字にタテ中ヨコ用タグ付け
text=text.replaceAll(/(?<=^|[^\d\.,:])(\d{1,2})(?=[^\d\.,:]|$)/) {
m0, m1 -> "<tcy>${m1}</tcy>"
}
println(text)



ところで、JavaScriptも正規表現を使えるけれど、後読みをサポートしていない
仕方がないので、後読み部分を普通にマッチさせる。後読み部分も1字なので不具合は出ないと思う。
以下ExtendScript。
var text="長嶋 茂雄の誕生日は1936年2月20日。\n"
+"1974(昭和11)年2月20日から\n"
+"2008(平成20)年11月20日の間の日数は26,572日、\n"
+"72年と274日になります。ところで、ただいまの時間は7:19です。"
//半角1,2桁の数字にタテ中ヨコ用タグ付け
text=text.replace(/(^|[^\d\.,:])(\d{1,2})(?=[^\d\.,:]|$)/g,"$1"+"<tcy>"+"$2"+"</tcy>");
$.writeln(text);

3 件のコメント:

ゲンゾウ さんのコメント...

先読み、後読み。
これは強力ですね。

勉強になりました。

kanemu1117nc さんのコメント...

ゲンゾウさん、はじめまして
リンクありがとうございますm(_ _)m
時々読ませていただいてます。

自分もこれまで正規表現って謎だらけだったんですが、これで少しは使えそうな気がしています。

匿名 さんのコメント...

javascript では後読みが使えないんですね。
情報ありがとうございました。