Dream Driven Development。夢を形にしよう!

正規表現の基礎知識

正規表現の基礎知識


プログラミングをしているとテキスト情報を扱うことがよくあります。文字列の中から一部分を取り出して加工するといった場合に活躍するのが正規表現です。
この正規表現、とっつきにくいですが、一度覚えてしまうと様々なところで使えてかなり重宝します。

なので、このサイトのプログラミング記事の1番目として取り上げます。内容的には、以下のようになります。
Let’s go !

正規表現を使えるツール・プログラミング言語

正規表現を使えるツールはいくつかありますが、新しい表記に対応していないものもあります。ここではざっくり新旧でわけてみました。詳細は各ツールのマニュアル等で確認してみてください。

vi/vim
grep
sed
awk
python
javascript
ruby
perl
java

例えば、”(?: )”によるキャプチャの回避や、”(?= )”による前方参照などは新側のツールでしか対応していません。
プログラミング言語はたいてい新しい表記に追従しているので問題ないと思います。

正規表現の基礎

正規表現は「文字列のルール」を表現する方法です。例えば/^https?/と書くと「httpもしくはhttps」で始まる文字を意味します。
このように文字をルールで表すことで、文章を認識できないパソコンにさまざまな動作をさせることができるようになります。

例えば、下の正規表現は正しいメールアドレスかどうかを判定する正規表現です。

/^(?:(?:(?:(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+)(?:\.(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+))*)|(?:"(?:\\[^\r\n]|[^\\"])*")))\@(?:(?:(?:(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+)(?:\.(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+))*)|(?:\[(?:\\\S|[\x21-\x5a\x5e-\x7e])*\])))$/

それでは実際にどうやって正規表現を組み立てるのか見ていきましょう。

正規表現で使われる文字:マッチ関連(チートシート)

ここで出てくる特殊文字以外の文字はそのままの文字として扱われます(“hoge”という正規表現にマッチするのは文字列”hoge”です)

まずは正規表現で特殊な扱いとなる文字の一覧です。
 (ここから先の正規表現の例は始まりと終わりを’/’で示します)

文字 意味 使用例 マッチする文字例(太赤字)
^ 行頭を表す(文字列の先頭または改行の次)
後述の[]内で使われる場合[^]は否定を表す
/^a.b/
/[^a-z][^A-Z]/
abbabb
bAAbA
$ 文字列の末尾を表す /.$/ hoge
[] カッコ内の文字どれか1つ。”^”と組み合わせると否定[^a]は”a”以外の文字 /[a-z]+/ ZaaabzD
|(パイプ) ”または”を意味する。パイプの前の文字列または後ろの文字列 /ab|cd/ acbcda
( ) 正規表現をひとまとめにする。
別の用途としてマッチした部分を後利用のために使用
/al(ph|ex)a/ alpha, alexa
.(ピリオド) 任意の一文字 /…/ abcdef
*(アスタリスク) 直前の文字の0回以上の繰り返し /ba*/ cdcbaaaadb
? 直前の文字の0回または1回の繰り返し /ba?/ cbad, cbdd
+ 直前の文字の1回以上の繰り返し /ba+/ cbaad
{n:m} 直前の文字のn回からm回の繰り返し
{n}はちょうどn回
{n,}はn回以上
{,m}はm回以下
/ur{2:3}/
/ur{2}/
/ur{2,}/
/ur{,3}/
aurrrra
aurrrra
aurrrra
aaurrrraa
(?=pattern) 肯定先読み(positive lookahead) 後ろにpatternが続く場合マッチ /abc(?=<\/pre>)/ defabc</pre>
(?<=pattern) 肯定後読み(positive lookbehind) 直前にpatternがある場合場合マッチ /(?<=<pre>)abc/ <pre>abcdef
(?!pattern) 否定先読み(negative..) 後ろにpatternが続かない場合マッチ 25(?!%) %25
(?<!pattern) 否定後読み(negative..) 直前にpatternがない場合マッチ /(?<!%)25/ 25%
\ 上記文字を文字のまま扱いたい場合には\を付ける。”\$”等 /\$/ $

上の表のうち量を表す記号(*,?,+,{n,m})はGreedy(欲張り)なマッチング、つまり「できるだけ長く一致する部分」にマッチします。
マッチする中で最短の部分にマッチさせたい場合はさらに’?’を追加した記号を使います。(*?,??,+?,{n,m}?)
Greedy/Reluctant(欲張り/最小)マッチに関しては別の章で例を上げていきます。

さらに特殊な文字を表すエスケープ文字があります。

文字 意味
\n 改行(linux:0x0A(LF), Win:0x0D0x0A(CRLF), mac:0x0D(CR))
\r 復帰(通常は0x0D(CR)のことだが、Macでは0x0A(LF))
\f フォームフィード
\t タブ
\b 単語境界(境界なので文字には一致しない)
\B 単語境界以外
\d 数字1文字{0-9]と同じ
\D 数字以外の1文字[^0-9]と同じ
\s 空白文字1文字[&nbsp\n\r\f\t]
\S 空白以外の文字1文字
\w アルファベットまたは数字[0-9a-zA-Z]
\W アルファベットまたは数字以外

正規表現で使われる文字:参照関連(チートシート)

丸かっこ”( )”を使うとマッチした部分を後から参照することができるようになります。これを使うことでマッチした内容に応じて処理を変えたり、マッチした文字を再利用したりすることができます。
正規表現内では\1、\2とカッコの順番に応じて内容を参照することができます。

文字 意味
( ) キャプチャ。左側から順番に\1, \2と参照できる
(?: ) キャプチャせずにグルーピング機能だけ使いたい時

Greedy(貪欲)マッチとReluctant(最小)マッチ

例えばウェブページのタグにマッチさせたいときを考えます。

<h1>text title</h1><p>hoge fuga</p>

普通に考えて

/<.+>/  #=> <h1>text title</h1><p>hoge fuga</p>

とすると最初の”<”から最後の”>”まで全部がマッチングしてしまいます。
これは、’+’などの繰り返しを表す記号は「できるだけ長く」マッチさせようとすることからきます。(Greedyアルゴリズムといいいます)
解決方法は2つあります。

’>’の否定形を使う方法

先程の例だと’.+’を使っているのでなんの文字でも当てはまってしまいます。これを’>’は含まないようにすれば、次の’>’が現れたところでマッチがとまります。

/<[^>]+>/  #=> <h1>

Reluctant(最小)マッチを使う方法

繰り返し数を表す記号に’?’を追加した記号を使うと最小一致になります。(*?,??,+?,{n,m}?)

/<.+?>/  #=> <h1>

肯定先読み、肯定後読みの使い所

最近のスクリプト言語でサポートされている肯定先読み(positive lookahead)、肯定後読み(positive lookbehind)ですが、htmlのような前後をタグで挟まれている文字列を解析する時に便利です。
例えば

/(?<=<h1>).+(?=<\/h1>)/

というパターンを使うとh1タグの中身を抜き出すことができます。
ただ、先読み・後読みの中に量指定子(+とか*とか)を置けない言語がほとんどなので、htmlタグにclass等の文字が入っている場合はもうひと工夫必要になります。

/(?<=<h1)(?:[^>]*>)(.+)(?=<\/h1>)/

このようにして”\1”を利用するというのが一つの方法になります。

正規表現のいろいろなパターン例

正規表現を一通り見てきましたが、やはり実際に使われている例をみて慣れていくのが上達の早道です。

日付マッチ

例えば、日付が’yyyy-mm-dd’のフォーマットで書かれている場合に「正しいフォーマット」にマッチして「年、月、日」を別々に取り出すことを考えます。

与える文字列: 2020-06-07
/(\d{4})-(\d{2})-(\d{2})/  #=> \1=2020, \2=06, \3=07

できてるように見えますね?
ですが、日付としてありえない数字をいれるとどうなるでしょうか?

与える文字列: 2020-18-50
/(\d{4})-(\d{2})-(\d{2})/  #=> \1=2020, \2=18, \3=50

2020年18月50日でも通ってしまいますね。月を1-12, 日を1-31に制限しましょう(2,4,6,9,11の月の対応は正規表現ではかなり難易度高いのでここでは取り上げません)
月の部分はこのようになります。

1-12にマッチ
/(0[1-9]|1[0-2])/

パイプ”|“を使ってパイプの左または右にマッチするようにして、左に’01 - 09’、右に’10-12’となるようにします。
日にちの部分も同じように考えて

1-31にマッチ = 01-09, 10-29,30-31
/(0[1-9]|[12][0-9]|3[01])/

全部つなげると下のようになります。

/(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])/

日付にマッチさせるだけでも結構長くなりましたが、正規表現を作るときはこのように、各部分に分けて作成してつなげると作りやすいです。

各プログラミング言語・ツールでの正規表現

冒頭でいろいろなツールやプログラミング言語で正規表現を使えると書きましたので、各言語での使い方を見ていきましょう。

Python

‘re’モジュールをimportして使います。

import re
re.match(r'(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])',"2020-06-07").groups()
#=> ('2020', '06', '07')

javascript

javascriptは”\d”が使えなかったので代わりに”[0-9]”を使います。
‘new RexExp’か”/xxxx/”でパターンを作ってからマッチさせます。

  <script type="text/javascript">
        var pat = new RegExp("([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])");
        var pat2 = /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])/;
        alert("2020-06-07".match(pat));
        alert("2020-06-07".match(pat2));
  </script>
#=> ["2020-06-07", "2020", "06", "07"]

ruby

rubyは正規表現を直接書いてマッチさせるだけの簡単仕様です。

/([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])/.match("2020-06-07").to_a
#=> ["2020-06-07", "2020", "06", "07"]

しかも、正規表現が先でも文字列が先でも良いんです。

"2020-06-07".match(/([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])/).to_a
#=> ["2020-06-07", "2020", "06", "07"]

さらに、文字列に大かっこを付けて、正規表現で文字を抜き出す変態的な使い方もできます。

"2020-06-07"[/([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])/,1]
#=> "2020"

java

プログラミング時の文字数が多いjavaは正規表現利用もきっちり書くことが要求されます。

import java.util.regex.*;
Pattern pDateTime = Pattern.compile("([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])");
Matcher m = pDateTime.matcher("2020-06-07");
#=> m.group(1)=> "2020", m.group(2) => "06", m.group(3) => "07"

vim/vi

ちょっと変わったところでエディタのvi/vimですが、検索や置換で正規表現が使えます。
僕がよく使うのは置換です。

:0,$s/v\(im\)/r\1/g
#=> vimと書いてあるところがすべてrimに変わる

find(コマンド)とgrep

過去に書いたプログラムを検索して、モジュールの使い方とかを思い出したいときには手っ取り早くfindとgrepを使って検索できます。

find ./ -name "*.rb" | xargs grep -E "match.*to_a"

enjoy!

|

お気に入りサイト