Hatena::ブログ(Diary)

名もないテクノ手 このページをアンテナに追加 RSSフィード

EPUB版『InDesign者のための正規表現入門』

InDesignのTips一覧

2009-12-25

[][][]【正規表現03】特定の金額だけにスタイルを適用したい

さあ、きょうはInDesign上での正規表現です。InDesign上で特定の金額にだけスタイルを付けたい場合ってありませんか? 例えば下図のようなテキストがあったとします。

f:id:seuzo:20091225134354g:image

こうしたいくつかの価格のうち、「合計」の価格にだけスタイルを付けたいような場合です。

f:id:seuzo:20091225134355g:image

もちろん、これひとつだけなら手作業で変更したって罰はあたりゃしません。だがしかし! カタログなどで何百何千というスペックから特定の金額にだけ(あるいは特定の商品番号にだけ)スタイルを適用しようとしたらどうでしょう? きっとスリコギみたいにすり減っちゃうでしょう。Hey, You!


マサヒコ「わかった! 『合計\t[\d,]+』でいいんじゃない?」

せうぞー「試してみよう!」

f:id:seuzo:20091225134356g:image

マサヒコ「あれ? 『合計』の部分までスタイルが付いてしまった!」「スタイルのオンオフを表す正規表現でもあるのかなあ?」

せうぞー「それはないよ。その方向には正解もない。まったく違う考え方で、すこし変わった道具を使うんだ」


みなさんも大掃除の片手間に考えてみたらどうでしょう?

答えを思いついた人はここにコメントを付けるなり、Twitter上で#NoNameRegex03 タグでつぶやいてみてください。


正規表現の考え方

みんなもうわかっちゃったかな? いくつかのコメントはすでに出されていてみなさん正解のようですね。

ずっと正規表現を使っている人は、テキストベースで考えているので、こういう処理は下記のような正規表現をまず思いつくでしょう。

#検索:
(合計\t)([\d,]+)円

#置換:<style>は架空のタグ
$1<style>$2</style>円

たしかに、HTMLのようなマークアップを想像するなら正しいかもしれません。けれど、InDesignにはこうした処理はできないんですよ。残念でした。

じゃあ、どうする? 実は、テキストベースの人はあんまり使わないような道具(正規表現)を使うんですよ。「先読み」と「戻り読み(後読み)」という道具です。


後に書くのに「先読み」という

例えば、金額の数字にだけスタイルを適用したいような時、正規表現で数字の後に「円」をそのまま付けると、数字だけではなく「円」の文字もスタイルが適用されてしまいます。次の作例の1行目を見てください。

f:id:seuzo:20091227162709g:image

後ろに「円」のつく「数字にだけ」スタイルを適用したい場合、「先読み」というテクニックを使います。

[\d,]+(?=円)

f:id:seuzo:20091227162710g:image

マッチ部分(この場合は数字)の次に来る文字列を「先読み」するから、「先読み」というんですね。この「円」の部分は正規表現が書けます。「(?=円|ドル)」などと書けば「円」または「ドル」がつく数字を表しています。

このテクニックの最大の特徴は、先読み部分はその文字列にマッチするのではなくて位置にマッチするということです。ですからInDesign上ではスタイルが適用されないのです。InDesignでこの先読み(または後述する戻り読み)テクニックが使えると、正規表現の可能性がグンと広くなります。


「否定先読み」というバリエーション

世の中にはアマノジャクさんがいて、「円」がつかない数字にだけスタイルを適用させたい人もいるかもしれません。そんな人はこれ「(?!〜〜)」を使ってください。

[\d,]+(?!円)

f:id:seuzo:20091227162711g:image

「否定先読み」といいます。アタマに否定を冠するヒネクレモノなので、あまり多用するとあたまがこんがらがったりします。ほどほどに。ちなみに上の正規表現を全体に適用するとこうなります。

f:id:seuzo:20091227162714g:image


「戻り読み(後読み)」

さて、今度は「戻り読み(後読み)」とういうテクニックを紹介します。ご賢察のとおり、マッチ文字列の前に書かれている条件を定義します。感覚的に「前」に書くのに「後読み」(アトヨミ)と言ったりするので混乱しやすいです。マッチ部分の前の文字列を「戻り読み」するのが感覚的には近いかもしれません。

「戻り読み(後読み)」は、特定の文字列が前に来ている時だけマッチが成功します。今回の場合のように「合計タブ」を前に持つ数字だけにスタイルを付けたい場合に使用します。

(?<=合計\t)[\d,]+

f:id:seuzo:20091227162712g:image


やはり「戻り読み(後読み)」にも否定形はある

とりあえず、あるという事実だけ。

(?<!合計\t)[\d,]+

f:id:seuzo:20091227162713g:image

どちらかというと誤用例ですね^^


わたしの解答例

今回のお題は、「先読み」「戻り読み(後読み)」という道具を使ってこんな感じで書いてみましょう。

(?<=合計\t)[\d,]+(?=円)

f:id:seuzo:20091227162715g:image


参照

正規表現の先読みと後読みはどっちがどっちだかわかりにくいんだよ!(追記あり) - chalcedonyの外部記憶装置・出張版

カネムーメモ: 正規表現、先読みと後読み

2009-12-15

[][]【正規表現02】電話番号の再フォーマット

さあ、第2回にしてすでにネタ切れの様相を呈している正規表現独習シリーズです。今回、読者の方からこんなお便りをいただきました。

人間ってやつは、なんでこんなにいい加減なのでしょう? 名簿の原稿がやっと入稿したと思ったら、このザマです。

f:id:seuzo:20091215155023g:image

members.txt 直

数字の全角・半角なんて知ったことじゃありません。市外局番と市内局番と個番がハイフンで区切られているもの、パーレンで区切られているもの、果ては音引きで区切ってあるものまであるじゃありませんか? 読めればいいってもんじゃないんです。

その上「これはエクセルで作ったんだ」と自慢までされました。さすがエクセルです、自慢するほど難しいのでしょう。

そういえばWebのフォーム集計をやってる上部さんも、そんな愚痴をこぼしていたっけ。。。

とにかく、このバラバラな電話番号のフォーマットをこんな感じに再フォーマットしなくちゃいけないんです。もちろん、すべて半角数字にしなくちゃいけません。

市外局番-市内局番-個番

どうか、みなさま助けてください。お願いします(ぺこり


せうぞー「マサヒコくん*1、この問題わかるかい?」

マサヒコ「この年末の忙しい時に、そんなこと考えてられないよ」

せうぞー「ぼくだってヒマじゃないよ。でもたまに更新しないと、見捨てられちゃうんだよ」

マサヒコ「あ! ぐぐったらでてきたよ。電話番号の再フォーマット - 名もないテクノ手」「...これウチじゃん。つか、ネタ使いまわしじゃん!」

せうぞー「失礼ですね、微妙に設問が違うんですよ。それにですね、その解答じゃないユニークな解答をみなさんが考えてくれるかと思って、あえて重複問題を出しているわけで...」

マサヒコ「じゃあ、ヒントをくださいよ」

せうぞー「ヒントは、『正規表現だけでは解決しない』です」

マサヒコ「なんだソレ!」


相変わらず他力本願で謎だらけなんですが、まあそういうことです。実際にこういう原稿が入ってきたと思って考えてみてください。

ユニークな答えが見つかった人は、ここにコメントをするなり、Twitterでつぶやくなりしてください。Twitterでつぶやく人は「#NoNameRegex02」ってハッシュタグを付けるといいかもしれませんよ。

来週には、このエントリに解答例を追記します。楽しい答えをお待ちしています。




準備

来週まで待てない人に、少しずつ解法を探っていきたいと思います。ほんのちょっとづつエントリーを追記していきます。ひとつのエントリーもスルメみたいにどんどん味が出てくる(かもしれない)のでお楽しみに。

今回の問題は2つの問題がごっちゃになっています。

  • 市外局番と市内局番、個番号をそれぞれを分ける区切り子がバラバラである
  • 数字が全角半角ごっちゃになっている

で、最初の区切り子の問題は正規表現検索置換で解決するのですが、数字の全角半角を統一するのは正規表現検索置換では一回でできないのです。じゃあ何回やるかっていうと10回やるのです。「0を0に置換」「1を1に置換」「2を2に置換」...のように。4回ぐらいやった後に、「やってられっか!」とブチ切れると、「ついでにアルファベットの大文字小文字(52文字)もやってください」と追い打ちを掛けられてしまうのです。いやん。


まず全角数字をなんとかしろ、話はそれからだ

まず最初に、全角数字をすべて半角数字にしてしまいましょう。そうしたら気持ちも(見た目も)かなりラクになるはずです。

今回のように全角文字を半角文字にするなどといった処理を「文字種置換(変換)」と呼びます。InDesignでも通常の検索置換の他に「文字種変換」が用意されています。miなどのちょっと気の利いたエディタなら標準機能として実装されています。エクセルとやらを使っているのだったら、そちらでもできることなんですが...


trコマンドを使おう

でだ。こういう「文字種置換」にはtrコマンドを使うのが断然便利です。よくある処理なので、一度覚えてしまえばずっと役立つスキルになります。この機会に是非マスターしちまいましょう。

Macユーザーなら標準で使えますし、Winユーザーでもパッケージがあるはずです(詳しい人はヘルプしてください)。では、手順どおりにやってみましょう。

0)上記のリンクから「menbers.txt」をダウンロードしてきて、デスクトップに置いてください。

1)アプリケーション-ユーティリティにある「ターミナル.app」をダブルクリックして立ち上げます。

2)ターミナルで以下をタイプして、デスクトップをカレントディレクトリに指定します。

cd ~/Desktop/

3)ターミナルで以下をタイプして、trコマンドを使用します。

tr "-" "0-9" < members.txt

4)全角数字がすべて半角数字に変わったはずです。これをエディタにコピペしてもいいんですが、せっかくですからそれもターミナルでやっちゃいましょう。

tr "-" "0-9" < members.txt > members_new.txt

どうですか? デスクトップに「members_new.txt」ってファイルが出来ていますよね!


trの正体

trコマンドを初めて使われた方もいらっしゃるかもしれません。trコマンドの簡単な使い方のフォーマットを見てみましょう。

tr "検索文字" "置換文字" < 入力ファイル > 出力ファイル

trはコマンド名です。

trは基本的に「字種置換」という検索置換をします。字種置換というのは、特定の1文字を特定の1文字に置き換えることです。「"0-9"」は略書きで、「"0123456789"」と書くのと同じ意味になります。この全角数字を「"0-9"」の半角数字に置き換える仕事をしてくれます。

「< 入力ファイル」で入力用のファイルを指定します。「> 出力ファイル」で出力用のファイルを指定します。

trコマンドは数字だけを扱うコマンドではありません。「tr "0-9A-Za-z" "0-9A-Za-z" < members.txt > members_new.txt」などとすれば全角アルファベットをすべて半角アルファベットに置き換えられます。


マサヒコ「ねえねえ、文字種変換だけならInDesignの検索置換ダイアログから出来るよ」

せうぞー「知っています。けれど、InDesignやエディタの字種変換では『,』や『.』などの記号類もすべて半角になってしまうよね。trコマンドなら変換したい文字だけを並べるだけでいいんだ」「たとえば、『tr "いろは" "イロハ"』とすれば『い』『ろ』『は』って3文字だけをカタカナに替えられるでしょ」

マサヒコ「なるほど! だからtrコマンドを覚えればいろいろ応用が効くんだね」


この機会にターミナルコマンドに興味を持った人は「[改訂版] Mac OS X ターミナルコマンド ポケットリファレンス」なんかを参考にするといいですよ。ずぶずぶとぬかるみの世界にハマっていくだろうから……


正規表現の考え方

せうぞー「さんざんtrでひっぱってぬかる民してしまいましたが、これでようやく準備が整いました。あとは肝心の正規表現を考えるだけですね」

マサヒコ「もうできてるよ。こないだのエントリーのまんまでしょ!」

[((]?(\d+)[()()− ー−\-]+(\d+)[()()− ー−\-]+(\d+)

せうぞー「やっちまったなあ!!」(...すいません。自分で言って萎えました)

f:id:seuzo:20091217161536g:image

マサヒト「あ、日付にもマッチしてる!」「でも同じハイフンへの置き換えだから大丈夫だよね」

せうぞー「今回はね。だけど、もし住所フィールドが補完されるとしたらちょっとまずいかもしれないよ」


唐突ですが、正規表現でいちばん大事なことってなんだと思いますか? メタ文字をたくさん覚えること? より短く書くこと?

違います。対象になる文字列をモデル化することです。それはどんな形をしているか、どんな形になりうるかを考えることです。

フライパンでシチューはうまくつくれません。正規表現という道具を選ぶ前に、まずどんな材料なのかを見極めなくちゃいけません。

上の例で失敗しているのは、電話番号に注目するあまり他の部分にもマッチする可能性を見逃してしまいました。正しく電話番号にだけマッチをするようにもっと全体のモデル化をすすめていきましょう。


全体をよく見て、電話番号を特定しよう

今回のテキストをよく見てください。これは「タブ区切りテキスト」です。そして、電話番号のフィールドは6番目のカラムです。つまり、行頭から5番目のカラムまでを表現できれば、正しく電話番号へ行き着けることにはなりませんか?

正規表現でタブ区切りテキストの1カラム分はこう表現できます。*2

[^\t\r\n]+\t

「タブと改行以外が1文字以上あって、タブがある」という意味です。*3では、行頭から5カラム分をちゃんと書いてみます。

^[^\t\r\n]+\t[^\t\r\n]+\t[^\t\r\n]+\t[^\t\r\n]+\t[^\t\r\n]+\t

あとで$1で参照できるようにグループ化しておきます。

^([^\t\r\n]+\t[^\t\r\n]+\t[^\t\r\n]+\t[^\t\r\n]+\t[^\t\r\n]+\t)

実際にmiで検索して確認してみましょう。

f:id:seuzo:20091217161537g:image

さあ、これで次のカラムを正規表現で書ければ、正しく電話番号にアクセスできますよ。


電話番号おさらい

ここで電話番号の正規表現についておさらいしておきましょう。

[((]?(\d+)[()()− ー−\-]+(\d+)[()()− ー−\-]+(\d+)

の意味をひとつづつ分解して見ていきます。

[((]?

このパートは、「半角パーレンか全角パーレン」が「1回または0回」あるという意味です。

ちょっと実例を挙げてみます。例えばこういうテキストがあると思ってください。

-123456
123456

もし、正規表現「^-\d+」とだけ書いてしまうと、一行目の「-123456」の方しかマッチしません。2行目の「123456」は無視されてしまいます。2行目をマッチさせるためには、マイナス記号がない正規表現でもう一度検索してやる必要があります。それは二度手間ですよね。

そんな時「?」を使うんですよ! マイナス記号が1回または0回として

^-?\d+

すると、どちらの場合も1回でマッチが成功します。

正規表現には「*」を使って0回以上を表現することがありますが、「1回または0回」がわかっているのだったら「?」を使うようにしましょう。


(\d+)

このパートは数字がひとつ以上にマッチします。今回の場合、市外局番のすべてにマッチします。

このマッチはあとで$1などで参照しますので、(〜)で括ってグループ化しておきます。

下記の正規表現(ここでは便宜的に行頭を指定します)で、下図のようにマッチしています。

^[((]?(\d+)

f:id:seuzo:20091217161540g:image


[()()− ー−\-]+

このパートは、パーレンとかハイフンとかの区切り文字が1文字以上あるという意味です。なぜ「+」を使って1つ以上としているのでしょう? それは「 (」(スペースと半角パーレン)を区切りにしている番号があるからです。ですから「 ?[()()−ー−\-]」などとしてもいいかもしれません。

最初から繋げるとこんな感じです。

^[((]?(\d+)[()()− ー−\-]+

f:id:seuzo:20091217161541g:image


次の「(\d+)」は市内局番です。最初から繋げてみます。

^[((]?(\d+)[()()− ー−\-]+(\d+)

f:id:seuzo:20091217161542g:image


さらに市内局番と個番を区切る区切り子「[()()− ー−\-]+」を追加してみます。

^[((]?(\d+)[()()− ー−\-]+(\d+)[()()− ー−\-]+

f:id:seuzo:20091217161543g:image


最後に個番「(\d+)」を追加します。

^[((]?(\d+)[()()− ー−\-]+(\d+)[()()− ー−\-]+(\d+)

f:id:seuzo:20091217161544g:image


これで電話番号のすべての番号部分をグループ化して捕捉しました。これを後方参照($1〜$3)で置換します。

$1-$2-$3

f:id:seuzo:20091217161545g:image


ぼくの解答例(と別解)

これまでの正規表現の検索置換をまとめるとこうなります。

#検索:
^([^\t\r\n]+\t[^\t\r\n]+\t[^\t\r\n]+\t[^\t\r\n]+\t[^\t\r\n]+\t)[((]?(\d+)[()()− ー−\-]+(\d+)[()()− ー−\-]+(\d+)

#置換:
$1$2-$3-$4

タブ区切りテキスト内の特定のカラムを正規表現で触りたければ、こうした書き方は定石といえます。ダラダラ長くなるのが難ですが。

もちろん、これは解答例にすぎません。今回ローカルで考えるなら、他にもたくさんの解答例があると思います。ひとつだけ思いついたものをご紹介しましょう。

#検索:
\t[((]?(0\d+)\D?\D(\d+)\D?\D(\d\d\d\d\t)

#置換:
\t$1-$2-$3

f:id:seuzo:20091221131655g:image

この表現のキモは一般的な電話番号が必ず「0」から始まることに注目しています。さらに、もう少し厳密にするために個番号が4桁であることを条件にしています。そして、見落としがちなポイントとしてタブをアンカーに使っています。行末や行頭をアンカーにする重要性や利点はくどいくらいに語られますけれど、他のアンカーになりうる文字についてはゾンザイに扱いがちです。しかし、この表現でタブを指定しない場合を考えてみてください。途端にあちこちにマッチしてしまうでしょう。

「\D」を使うのは個人的に好みじゃありません。しかし全体の文脈を熟知して限定的に使うのならさほど副作用はないと思います。


【おまけ】一行野郎でやろう

これら正規表現検索置換は、InDesignでも使えますしmiエディタでもつかます。そして、コマンドラインでも使えるのです。

コマンドライン上で正規表現検索置換ができるアプリケーションは多いのですが、今回はrubyを使います。

以下、詳しい説明はしませんが「こんなことができるんだ」と頭の隅に記憶しておくと、きっといいことあるでしょう。たぶん。

ruby -Ku -np -e '$_.sub!(/^([^\t\r\n]+\t[^\t\r\n]+\t[^\t\r\n]+\t[^\t\r\n]+\t[^\t\r\n]+\t)[((]?(\d+)[()()− ー−\-]+(\d+)[()()− ー−\-]+(\d+)/){"#{$1}#{$2}-#{$3}-#{$4}"}' members_new.txt 

どうですか? すべての電話番号の区切りがハイフンになりました。

そしてさらに! さいしょのtrコマンドと一緒に指定することで、完全な一行野郎(ワンライナー)になります。ターミナルで1行書くだけで、半角全角処理から電話番号の再フォーマットまで全部おわってしまうんですよ!

tr "-" "0-9" < members.txt | ruby -Ku -np -e '$_.sub!(/^([^\t\r\n]+\t[^\t\r\n]+\t[^\t\r\n]+\t[^\t\r\n]+\t[^\t\r\n]+\t)[(]?(\d+)[()()− ー−\-]+(\d+)[()()− ー−\-]+(\d+)/){"#{$1}#{$2}-#{$3}-#{$4}"}' > members_new2.txt 

うーん、マンダム。

バックスラッシュいっぱいなので、とりあえずファイルにしてあります。

telno.sh.zip 直



一週間つづけてきた更新も、ひとまず大団円いや中団円にします。いかがでしたか? 案外長くて難しそうと思った人、それは答えだけを見ているからです。「正規表現脳」はメタ文字を使いません。だから、頭の中ではもっとスッキリ書けています。すこしずつトレーニングをすれば、誰だって「正規表現脳」を手に入れられます。頑張ってください。

*1:このシリーズのマスコットキャラクタ。苗字は近藤。

*2:今回は空のフィールドは考慮していません。空のフィールドは「[^\t\r\n]*\t」と表現できます。できることならばなるべく「*」を使いたくないという心理的な壁ですね^^

*3:否定文字クラスやその略記法を使う場合は改行文字にマッチする可能性を必ず考慮してください。これ、ハマリポイントですよ。

2009-12-01

[][]【正規表現01】日付の表現

はじめに

正規表現が便利だとわかっていても、思い通りに使いこなすのはなかなか難しいと感じる方も大勢いらっしゃると思います。難しいメタ文字を覚えるのが正規表現ではありません。検索をモデル化する「考え方」がもっとも重要です。簡単な表現からひとつひとつ学んで行けば、少しづつ使えるようにるでしょう。いくつかのメタ文字を暗記してみたけれど「実際に」どう使えばいいのか(正しいのかどうか)迷っている方は参考にしてみてください。

これからたまに(気が向いた時に)「正規表現のワンポイントレッスン」を取り上げていきます。なにかしら具体的な実例を上げて、その解法を考えてみます。よかったら、みなさんも考えてみませんか? 解法を思いついたら、気軽にコメントを付けてみてください。解答は翌日(または数日後)エントリーに追記します。

このブログの読者はDTPユーザーが多いですから、正規表現の検証や図示にはAdobe InDesign CS4 (V6.0) 日本語版 Macintosh版 (旧製品)を主に使います。このInDesign CS4では拡張正規表現が採用されていますから、テキストエディタのmiやプログラム言語のRubyなどでも類似点が多いと思います。適宜読み替えていただければ幸いです。


日付の表現

日付の表現を考えてみましょう。たとえばこんな感じの日付です:

1月23日

2月28日

3月1日

4月30日

12月31日

  • 日付の数字は半角のアラビア数字のみとします。
  • 月を表す数字の後に「月」、日にちを表す数字の後に「日」が入ります。

マサヒコ*1「できました! 『\d+月\d+日』ですね。カンタンじゃん!」

せうぞー「うーん、惜しいなあ。間違ってはいないんだけどね」

マサヒコ「そうなの? だって上の例文にはぜんぶマッチするよ」

せうぞー「そう。例文には全部マッチする。じゃあこれはどうだろう」

0月0日

23月456日

マサヒコ「あれっ? めちゃめちゃな日付にもマッチしちゃう。うーん、こまったなあ...」

せうぞー「じゃあ、明日まで考えてみて。お友達と相談してもいいよ」

マサヒコ「はあい。みんな助けてね〜!(他力本願)」








みなさん、いくつものコメントありがとうございます*2。正解だったひとも惜しかった人も、いろいろ考えることで「考え方」を深めていただけたかと思います。もしちょっと恥ずかしい間違いをしてしまって「削除して〜!」と思っている人はお申し出くださいませ。「間違ってしまうのは恥ずかしいことじゃない」と、ぼくは思いますけどね... ;-)


考え方

まず、月の数字から考えてみましょう。もっとも陥りがちな罠は、1から12までをこんな風に表現してしまうことです。

1?[0-9]月

こうしてしまうと、「0月」や「19月」にもマッチしてしまうんですね。これはちょっとゆるい。ユルユルです。

1桁と2桁で分けて考えてみましょう。1桁の月数は「[1-9]月」と表現できます。2桁の月数は「1[012]月」と表現できます。ですからこの2つを選択で繋いでしまえばいいんですね。

([1-9]|1[012])月

日にちも同様に考えられます。1桁の「[1-9]日」、10日〜29日は「[12][0-9]」と表現できます。最後の30日と31日は「3[01]日」と書けばいいですね。選択で繋ぐとこうなります。

([1-9]|[12][0-9]|3[01])日

さて、この月日のふたつの正規表現を合体すれば日付を表現できたことになります。めでたしめでたし。

([1-9]|1[012])月([1-9]|[12][0-9]|3[01])日

マサヒコ「先生、まちがえてるよ。だって、これだと『2月31日』にもマッチしちゃう!」

せうぞー「マサヒコくん、余計なところによく気がつきました。確かに小の月、大の月にちゃんと対応していないですね」


ちょっと厳密に考えるの巻

月ごとの最終日をちゃんと正規表現で考えるとどうなるでしょうか? 28日(または29日)しかない2月、30日までしかない4・6・9・11月、31日まである1・3・5・7・8・10・12月のグループに分けられますね。分かりやすく行をわけて書いてみましょう。

(
2月([1-9]|[12][0-9])日|
([469]|11)月([1-9]|[12][0-9]|30)日|
([13578]|1[02])月([1-9]|[12][0-9]|3[01])日
)

かなり複雑になってきました。1行で書くともれなく混乱しますね。しかも閏年かどうかがわからない限り「正当な」日付であるかどうかを判定できません。

さらにこうして正確さを追求することで、もうひとつの不具合がでてきます。それは「11月31日」です。はい「11月31日」自体のマッチには失敗しますが「1月31日」にマッチしてしまうんですね。miで検索するとこんな感じです。

f:id:seuzo:20091201174551g:image


どこまで正規表現でやるのか問題

途中からマッチしてしまう問題に対しては、こんな感じに書いたらどうでしょう?

(?<![0-9])(2月([1-9]|[12][0-9])日|([469]|11)月([1-9]|[12][0-9]|30)日|([13578]|1[02])月([1-9]|[12][0-9]|3[01])日)

否定後読みが使えない場合は、「(^|[^0-9\r\n])」などとしてグループを別処理しなければなりません。ちょっと複雑すぎます。かつ、閏年問題は依然残ったままです。

マサヒコ「げげーっ! これだから正規表現ってキライだよ!」

せうぞー「そう、みんなの正規表現嫌いは、こんなことが原因なんだよね。あるしきい値を超えると急に複雑になってしまう。どんな用途が正規表現に向いているか(または向いていないか)がわからない」


最初にこの問題を選んだのは、ちょっとイジワルだったかもしれません。でも悪気があったわけじゃないんです。正規表現には限界があるってことを身近な例で知っていただきたかったんです。*3

それに、どんな文字列がマッチするのか(またはマッチしないのか)を探り、深く考えることこそ「正規表現を知る近道です。この日付の例で言うならば「([1-9]|1[012])月([1-9]|[12][0-9]|3[01])日」でざっくりチェックして、あとは目視するとしたら小の月の最終日に注意すればいいとわかります。その妥当性を自動で判定しなければならないとしたら、正規表現のマッチ部分を別プログラムに渡すなどします。

$ ruby -r date -e 'p Date::exist?(2009, 2, 29)'
# => nil 2009年2/29は存在しないのでnilが返った

テキスト処理の世界は罠に満ちています。正規表現はその罠に目印をつけてくれる便利な道具です。これから(一緒に|楽しく)勉強していきましょう。

*1正規表現を覚えて間もない生徒。(先生の期待通りに)予定調和な失敗をしてくれる。名字はもちろん「近藤」!

*2:Twitterなどでつぶやいていただいた方もいらっしゃいますので、次回からはハッシュタグを考えた方がいいかもしれません。

*3:たとえば詳説 正規表現には、メールヘッダに記されているメールアドレスが正しいかどうかを判定する複雑な正規表現の例が挙げられている。