正規表現の補遺として「ルックアラウンドアサーション」(周りを見回す言明)というのを紹介します。また『イジハピ』からの焼き直しとなりますがご寛恕下さい。ちなみに過去記事はこちら。

イジハピ! : 【第638回】正規表現の先読み、後読みのアサーション

Kodomo-Ginkou-Ken-Japanese-Toy-Money

 正規表現は「.*」(ゼロ文字以上の文字列)とか、「[a-z]+」(1文字以上の文字列)というものは、検査する文字列中のある長さのある文字列にマッチします。
 一方、「^」(行の先頭)や「$」(行の末尾)、「\b」(バウンダリー=単語の境目)というものは、検査する文字列のある「場所」にマッチして、長さを持ちません。こういう正規表現をアンカー(船についている錨(いかり)のこと)とも言います。
 今日紹介する「ルックアラウンドアサーション」は、「ある文字列の前」、「ある文字列の後」、「ある文字列以外の文字列の前」、「ある文字列以外の文字列の後」という「場所」にマッチし、やはり長さを持ちません。

 正規表現による検索置換を使っていて突き当たる問題として、ある条件の時のみ検索置換を行いたい、ということがあります。たとえばIT用語で、カタカナ列の末尾にある音引き(ー)を取りたいとします。こういう風に置換したい。

 (置換前)コンピューターが使えて便利だ
 (置換後)コンピュータが使えて便利だ

 単純に「ー」を全削除すると

 (置換後)コンピュタが使えて便利だ

となります。

 そこで、カタカナ以外の前にある音引きを削除する、と考えます。しかし、カタカナ以外の1文字は[^ァ-ン]ですが、

 s/ー[^ァ-ン]//g;

のようにすると、

 (置換後)コンピュータ使えて便利だ

という、ちょっとカタコトな感じになります。そこで新しい技術を導入します。

 s/(?<=[ァ-ン])ー(?![ァ-ン])//g;

 ここで(?<=[ァ-ン])は、任意のカタカナ列[ァ-ン]の後ろという場所にマッチします。これを肯定の後読みと言います。
 Perl正規表現エンジンは長音記号(ー)を見つけたら、そこの後ろをちょいと振り返って、そこがカタカナであれば真とします。
 (?<=[ァ-ン])は場所にマッチするので、文字列を消費しません。

 また(?![^ァ-ン])は、任意のカタカナ1文字[ァ-ン]以外の前という場所にマッチします。これを否定の先読みと言います。
 長音記号を見つけたら、そこの前をちょいと先読みして、そこがカタカナ以外であれば真とします。

 (置換後)コンピュータが使えて便利だ

となるはずです。テストします。

#! /usr/local/bin/perl
#
# regTestLookarround.pl -- 正規表現のテスト

use 5.010;
use strict;
use warnings;
use utf8;
binmode STDOUT, ":encoding(UTF-8)";

while (<DATA>) {
 s/(?<=[ァ-ン])ー(?![ァ-ン])//g;
 print;
}
__DATA__
コンピューターが使えて便利だ
便利だぞコンピューター
ひらがなの長音は取らなくていいー
いいぞー取らなくて

[sample]$ ./regTestLookarround.pl
コンピュータが使えて便利だ
便利だぞコンピュータ
ひらがなの長音は取らなくていいー
いいぞー取らなくて
[sample]$

 いい感じですね。
 これはもともとPerlが拡張した正規表現ですが、いまはほとんどの正規表現処理系で使われています。ルックアラウンドアサーションをまとめると、

 (?=パターン)  肯定の先読み  この先にパターンが出てくる場所にマッチ
 (?!パターン)  否定の先読み  この先にパターンが出てこない場所にマッチ
 (?<=パターン) 肯定の後読み  この前にパターンが出てきた場所にマッチ
 (?<!パターン) 否定の後読み  この前にパターンが出てこなかった場所にマッチ

となります。

 急にまとめて言われるとパニックになると思いますが、まず、(?=パターン)だけ覚えて、後ろに戻る時だけ<を使う、否定は=を!にすると覚えればカンタンだと思います。

 さて、ここで超絶面白い例を紹介します。書いたのは神戸の森卓司さんという、新聞の組版をやっている方です。

Sus scrofa liaodongensis: 漢数字を無思慮に洋数字に変える

 新聞は縦書きなので、基本数字は漢数字ですが、それをネットにアゲるときは横書きになるので、漢字は洋数字(インド数字)にしたい。そこで、漢数字を洋数字に変えるスクリプトが以下のようになります。テスト実行するためにちょっと変更させてもらいました。

#! /usr/local/bin/perl
#
# regTestKanYou.pl -- 漢数字を洋数字に変える

use strict;
use warnings;
use utf8;
binmode STDOUT, ":encoding(UTF-8)";

while() {
 # 単位語の前に数字がない場合「一」を補う
 s/(?  s/(?
 # 欠けている桁をゼロとして復活させる
 s/(?<=兆)(?![一二三四五六七八九][十百千万]?億)/〇億/g;
 s/(?<=億)(?![一二三四五六七八九][十百千]?万)/〇万/g;
 s/(?<=[兆億万])(?![一二三四五六七八九]千)/〇千/g;
 s/(?<=千)(?![一二三四五六七八九]百)/〇百/g;
 s/(?<=百)(?![一二三四五六七八九]十)/〇十/g;
 s/(?<=十)(?![〇一二三四五六七八九])/〇/g;

 # 単位語のうち千百十を消す
 s/[千百十]//g;

 # 億万でゼロを整理
 s/〇〇〇〇[億万]//g;
 s/(?<=[兆億万])〇+//g;

 # 洋数字に変換
 y/〇一二三四五六七八九/0123456789/;

 # 3桁毎にコンマを挿入
 s/(?<=\d)(?=(\d\d\d)+(?!\d))/,/g;
 print $_;
}

__DATA__
あのう、千円ください
今度は五千円ください
あのダムを作るのに五百億円は掛かっている
ウルトラマンに「三億五千年前の化石だ!」というセリフがあったが、絶対「三億五千万年」の間違いだと思う

実行してみる。

[sample]$ ./regTestKanYou.pl
あのう、1,000円ください
今度は5,000円ください
あのダムを作るのに500億円は掛かっている
ウルトラマンに「3億5,000年前の化石だ!」というセリフがあったが、絶対「3億5,000万年」の間違いだと思う

 出来ました。面白いですね!
 「億」「兆」はプラクティカルに妥協して漢数字を残していますが、他の漢数字は洋数字に変換し、3桁毎にカンマ編集しています。
 これは徹頭徹尾ルックアラウンドアサーションを使った検索置換です。

 s/(?

という置換演算子に

あのう、千円ください

という文字列を作用させると

あのう、一千円ください

になる。

 [一二三四五六七八九]は一から九の漢数字です。
(ここで注意点としては、Unicodeに漢数字が出現する順番がバラバラなので(画数順なので)[一-九]とは出来ません。)

 これが前に存在しない、かつ、後に[千百十]が後ろに存在する、「空間」を、「一」に置換します。ですから、「、」と「千」の間に一を結果的に挿入しています。次に、

 s/(?<=千)(?![一二三四五六七八九]百)/〇百/g;
 s/(?<=百)(?![一二三四五六七八九]十)/〇十/g;
 s/(?<=十)(?![〇一二三四五六七八九])/〇/g;

を連続的に作用させることによって、

あのう、一〇百円ください
あのう、一〇〇十円ください
あのう、一〇〇〇円ください

となります。この〇は「ぜろ」を日本語変換すると出てくる、言うところの「漢数字のゼロ」です。で、

 y/〇一二三四五六七八九/0123456789/;



あのう、1000円ください

となります。
 ここでy///はtr///と一緒で、translitalation(逐字変換)を行います。yというのはあらゆるコマンドが英字1文字からなる変態環境sedの名残りですが、Perlはsedから乗り換えた人も多いので使えるようになっています。

 s/(?<=\d)(?=(\d\d\d)+(?!\d))/,/g;

は、いわゆるフクロウ本(『詳説 正規表現』)からの引用ということですが、これがまたすごくてカンマ編集を行います。これは、前に数字があって、後ろに数字が3桁あり、かつその後ろに数字が来ない空間にカンマを入れるというもので、

1234567

の場合は

1234,567
1,234,567

となるそうです。へぇー! ぼくはループを使ってやっていました。

 以上、面白いだけでなく、実際に役に立っているプログラムです。逆に、洋数字を漢数字に変換するのはこちら。

Sus scrofa liaodongensis: 洋数字を単位語入りの漢数字に変換するぞ


 どちらも、百千万は十刻みだが億兆は一万刻みになる、「一万飛んで十」のようなケースがある、一が省略されるケースがあるなどの、「文化」の違いを考慮してプログラミングされています。役に立つプログラムというのは技術と文化の交わるところに生じるものであるということが分かって、勉強になります。