正規表現入門

この記事は、TUT Advent Calendar 2016 - Adventar の13日目の記事です。

エクストリーム正規表現入門はこちら

 

813.hatenablog.com

 

正規表現とは?

正規表現は、文字列のパターンを表す文字列である。これによってgrepなどで検索や置換をすることができる。
プログラミング言語にかかわらずエディタでも同様に使えるため、プログラムを書かない人も、覚えると便利だと思う。

今回は環境構築等を省くため、ブラウザ上で動く以下のツールを紹介する。
PHP正規表現チェッカー
ただ、正規表現は言語や環境で大きく変わるものではないため、リンク切れやうまく動かない場合は適当なツールで試すことができるはずである。

正規表現を学ぶ前に

まず大事なことは、最初から正規表現で全てを表現しようとしないことである。
入力値の法則を完璧に見極めないうちから細かな正規表現を書くと、人的リソースや計算リソースを浪費するばかりでなく、正常な動作が損なわれる原因にもなる。

投稿日時: 2016/9/7 1:23:45
最終更新日時: 2016/11/29 23:0:0

投稿日時: 2016/08/10 11:45:14
最終更新日時: 2016/08/10 11:45:14

こんなテキストから投稿日時を抜き出すとしよう。今回説明することをマスターすれば、下のような正規表現は簡単に書くことができるようになる。この記事を読み終わったら戻ってきて解読してみてほしい。

(?<=投稿日時: )(\d{4}/(?:0?[1-9]|1[0-2])/(?:0?[1-9]|[12]\d|3[01]) (?:[01]\d|2[0-3])(?::([0-5]\d))+)

しかしこれは4月31日や平年の2月29日も含めてしまうし、もっと致命的な例では閏秒を含まない。日付チェックなどは組み込み関数やライブラリに任せてしまって、正規表現ではそれっぽい箇所を抜き出すだけに留めるべきである。以下で十分だ。

(?<=投稿日時: )(\d{4}(?:/\d{,2}){2} \d{,2}(?::\d{,2}){2})

長々と正規表現を書いていると、気持ちよくなってきて更に長い正規表現を書いてしまう人がたまに居るが、あくまで奥の手か孫の手くらいにしておいて、複雑な正規表現をプログラムでゴリゴリ回すのはやめるべきである。メンテナンス性が悪い上に重くなる可能性が高い。

まずは単なる文字列探索

foobarbazquxquuxhogepiyofugahogerahamspam

ここからhogeを探してみよう。
対象文字列foobarbazquxquuxhogepiyofugahogerahamspam
パターン文字列hogeと入力してみると、

hoge

hogeに一致(以後マッチ)する箇所に色がつく。ただの文字列検索のようだが、これも立派な正規表現である。

文字種と文字クラス

.
改行を除くあらゆる文字にマッチする。
[]
[]で囲んだ中の1文字。
[flmstw]ax
fax, lax, max, sax, tax, wax
のいずれにもマッチする。
文字と文字の間にハイフンを入れるとその間の各文字のいずれにもマッチするようになる。
あ[いさ-そり]
あい, あさ, あし, あす, あせ, あそ, あり
のいずれにもマッチする。文字コードによっては
あざ, あじ, あず, あぜ
にもマッチする。
[^ac-f]のように内に^を付けると、それらと改行を除くあらゆる文字にマッチする。
[^ac-f]
!, ", #, ..., 0, 1, 2, ..., 9, ..., Z, b, g, h, i, ..., z, あ, い, う, ...
など、様々な文字にマッチする。ここには一部の制御文字も含まれる。
\n
改行
\t
水平タブ
\s
[ \t\n\r\f]
\S
[^ \t\n\r\f]
\d
[0-9]
\w
[0-9A-Z_a-z]
\
[, ], \, ^や、あとで紹介する(, )など、特殊な役割を果たす文字にマッチさせたい場合は、その文字の前に\を付ける(エスケープする)。
たとえば、
foo["hoge"]
bar["piyo"]
baz["huga"]
qux["hogera"]
から
baz["huga"]
正規表現を使って検索したい場合、
baz\["huga"\]
とエスケープする必要がある。\にマッチするには\\とする。
^
の外に^をおいた場合、検索対象の先頭(状況によっては行頭)にマッチする。
foo1foo2foo3foo4
の中で
^foo\d
にマッチするのは、foo1のみである。
$
検索対象の先頭(状況によっては行頭)にマッチする。
foo1foo2foo3foo4
の中で
foo\d$
にマッチするのは、foo4のみである。

量指定子

?
0回または1回の繰り返し。
meteo?r
metermeteorのどちらにもマッチする。
*
0回以上の繰り返し。
go*gle
ggle, gogle, google, goooglegoooooooooooogleなどにマッチする。
+
1回以上の繰り返し。
go+gle
gogle, google, goooglegoooooooooooogleなどにマッチする。ggleにはマッチしない。
文字列から数字列を取り出すには
\d+
とすればよい。
{}
{num}とすると、num回の繰り返しを意味する。
IE{3}
とひとしい。
{min,max}とすると、意味するmin回以上max回以下の繰り返しを意味する。
つまりa?a{0,1}とひとしい。
{min,}とすると、min回以上の繰り返しを意味する。
つまりa*a{0,}とひとしい。
また、a+a{1,}とひとしい。

例題

6桁の十六進カラーコードにマッチするパターンを記せ(14文字以内)。ただし大文字小文字は区別しないものとする。
つまり以下のようにマッチすればよい。
PHP正規表現チェッカーを用いる場合は、マルチラインモードをオフにするとよい。

#000000
#FFFFFF
#ff0000ABC
a#000000
#77777
#GGGGGG
000000

余力があれば、13文字での表現も考えてみてほしい。

^#[0-9A-F]{6}$
^#[\dA-F]{6}$

グループ化

(?:pattern)のように、(?:)で囲むとそのグループをひとかたまりとすることができる。

(?:nya){7}!

Nyanyanyanyanyanyanya!

にマッチする。

www.nicovideo.jp

OR

_(?:foo|bar)!のように、|で区切ると_foo!にも_bar!にもマッチする。|(?:)の外でも使用できるが、可読性の向上のためにつけることを推奨する。

例題

IPv4アドレスを示す行にマッチする正規表現を記せ(目標100文字以内、どうしても思い浮かばなければ175文字以内)。
つまり、以下のように0-255の数4つをドット区切りした文字列にマッチすればよい。
PHP正規表現チェッカーを用いる場合は、マルチラインモードをオフにするとよい。

192.0.2.0
198.51.100.35
203.0.113.59
192.0.2.256
192.000.002.012
123.456.789.012

IPv4アドレスは、先頭に0があるバイトは八進数として扱われる(192.000.002.012192.0.2.10と解釈される)。余力があれば、これらの変則的な表記に対応した表現も考えてみてほしい。

^(?:(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$
グループのネストを用いる。ドットのエスケープに気をつけてほしい。

後方参照

(pattern)のように、()で囲むとそのグループをひとかたまりとした上で、後から番号で参照することができる。

(\d{4})([A-Z]{4}),\2\1

1234ABCD,ABCD1234

にマッチする。

1234ABCD,ABCD1233

にはマッチしない。つまり、\1には1234\2にはABCDが入っている。
PHP正規表現チェッカーを用いる場合は、マッチグループを表示するとこれらを確認することができる。

また、後方参照は置換に用いることができる。

13日午前の東京外国為替市場で円相場は上げ幅を縮めている。10時時点では1ドル=115円02〜05銭と前日17時時点に比べ80銭の円高・ドル安水準で推移している。日経平均株価が一時上昇に転じるなど底堅く推移しており、株価の下げ渋りに歩調を合わせる形で円の上昇に歯止めがかかっている。
円は対ユーロで下落に転じた。10時時点では1ユーロ=122円38銭〜41銭と同3銭の円安・ユーロ高水準で推移している。対ドルでの円買いの一服がユーロにも波及した。

に対して、パターン文字列を

(1(?:ドル|ユーロ))=(\d+円)(?:\d*銭?〜\d+銭)

置換え文字列を

\1あたり\2

とすると、

13日午前の東京外国為替市場で円相場は上げ幅を縮めている。10時時点では1ドルあたり115円と前日17時時点に比べ80銭の円高・ドル安水準で推移している。日経平均株価が一時上昇に転じるなど底堅く推移しており、株価の下げ渋りに歩調を合わせる形で円の上昇に歯止めがかかっている。
円は対ユーロで下落に転じた。10時時点では1ユーロあたり122円と同3銭の円安・ユーロ高水準で推移している。対ドルでの円買いの一服がユーロにも波及した。

と置き換えることができる。
環境によっては$1, $2, ...を用いる場合もある。

先読み・後読み

後読み

(?<=foo)のように(?<=)で囲むと、遡ってマッチさせることができる。

foo
bar
foobar

に対して

(?<=foo)bar

foo
bar
foobar

といったようにマッチする。

先読み

(?=foo)のように(?=)で囲むと、先行してマッチさせることができる。

foo
bar
foobar

に対して

foo(?=bar)

foo
bar
foobar

といったようにマッチする。

後読み否定

後読みの=の代わりに、!とすると先読みグループ内にマッチしないものにマッチする。つまり、先読みグループに続かないものにマッチする。

foo
bar
foobar
barbar

に対して

(?<!foo)bar

foo
bar
foobar
bazbar

といったようにマッチする。

先読み否定

先読みの=の代わりに、!とすると後読みグループ内にマッチしないものにマッチする。つまり、後読みグループが続かないものにマッチする。

ちょうど6桁の数を表す正規表現

(?<!\d)[1-9]\d{5}(?!\d)

である。

結び

正規表現の様々な構文を紹介したが、これらを一度に覚えるのは難しい。日常の作業で長い文字列を扱うことがあれば、その都度調べながら正規表現の恩恵を享受し、覚えていってほしい。