Python Regular Expressions in Google's Python Class in Japanese

Google による Python の授業 (日本語訳)/ Google's Python Class (英語原文)

資料
日本語訳: 設定/ 入門/ 文字列/ リスト/ 整列/ 辞書とファイル/ 正規表現 / ユーティリティ
英語原文: Set Up/ Introduction/ Strings/ Lists/ Sorting/ Dict and Files/ Reg. Exp./ Utilities

練習問題
日本語訳: 基本問題/ 新生児名/ 特殊なコピー/ アクセス履歴のパズル
英語原文: Basic Exercises/ Baby Names Exercise/ Copy Special Exercise/ Log Puzzle Exercise

講義映像
英語
1日目: Introduction, Strings/ Lists and Sorting/ Dicts and Files
2日目: Regular Expression/ Utilities/ Utilities urllib/ Conclusions

Python の正規表現


正規表現はテキストのパターンを一致させるには強力な言語です. このページでは, Python の練習問題に取り組むのに十分な正規表現を紹介し, 正規表現が Python でどのように使えるかを説明します. Python の "re" モジュールにより正規表現が使えるようになっています.
Python では, 正規表現によりパターンを探すときに次のように書きます.
  match = re.search(pat, str)

re.search() メソッドは正規表現のパターンと文字列を引数に取り, そのパターンがその文字列の中にあるかを探します. パターンが見つかったときは, search() は一致したオブジェクトを返し, 見つからないときは None を返します. このため次に示す, 3文字の単語の後に 'word:' があるパターンを探す例 (詳細は以下) にあるように, パターンが見つかったかどうかを確認するために, 通常パターンを探した直後に if 文が続きます.
str = 'an example word:cat!!'
match = re.search(r'word:\w\w\w', str)
# search() の後の if 文で見つかったかどうかを確認する. 
  if match:                      
    print 'found', match.group() ## 'found word:cat'
  else:
    print 'did not find'

match = re.search(pat, str) というコードは search の結果を "match" に取っておきます. そして if 文が一致を確認する, つまり match が真なら search は成功していて, match.group() には一致したテキスト (例えば, 'word:cat') が入ります. そうでないとき, つまり一致がなかったとき, search は成功せず, 一致するテキストはありません.
パターン文字列の先頭に 'r' を付けると, バックスラッシュを変更せずに処理する Python の "生の" 文字列を指し示すことが出来るので, 正規表現を使うとき非常に便利です (Java にこの機能が是非とも欲しい!). 習慣として 'r' を付けて正規表現を書くよう薦めます.

基本パターン


正規表現の素晴らしさは, 決まった文字 (列) ではなく パターンを指定できるところにあります. 次は, 単一文字に一致する基本パターンです.
  • a, X, 9, < -- 通常の文字はそれ自身のみに一致します. 特別な意味を持つため, 自分自身に一致しない メタ文字は次のものがあります . ^ $ * + ? { [ ] \ | ( ) (詳しくは以下で).
  • . (ピリオド) -- 改行文字 '\n' 以外の任意の単一文字に一致します.
  • \w -- (小文字の w) は "単語" の文字, つまりアルファベット, 数字, 下線 [a-zA-Z0-9_] の文字に一致します. w は "単語" の省略形ですが, 単語全体ではなく, 単語の中の単一文字だけに一致します. \W (大文字の W) は任意の単語でない文字に一致します.
  • \b -- 単語や単語でない文字の間にある境目
  • \s -- (小文字の s) は単一の空白に関する文字, つまり空白文字, 改行文字, 復帰文字, タブのいずれかで [ \n\r\t\f] の形に一致します. \S (大文字の S) は空白に関する文字でない任意の文字に一致します.
  • \t, \n, \r -- タブ, 改行文字, 復帰文字
  • \d -- 10進数の数字 [0-9] (ある古い正規表現のユーティリティで \d が使えない ものがありますが, それらのすべてでは \w, \s が使えます)
  • ^ = 先頭, $ = 末尾 -- は文字列の先頭と末尾に一致します.
  • \ -- 文字の特別性を失くします. なので例えば, ピリオドに一致させるには \. , スラッシュに 一致させるには \\ を使うこと. '@' のような文字が特別な意味を持つかどうかはっきりしないとき, その前にスラッシュを置いて \@ とすると, 確実に文字そのものとして扱われます.

基本の例


冗談: 目 (eye) が 3つある豚は何? piiig! (eye = i) (訳注: この冗談は, Jokes.smasHits.com のエントリにもあります. )
文字列中のパターンを探す正規表現の基本規則は,
  • 探索は文字列の最初から最後に向かって進み, 最初の一致が見つかると停止する.
  • パターンのすべてに一致しなければいけないが, 文字列のすべてではない.
  • match = re.search(pat, str) が成功したなら, match は None ではない値になり, 特に match.group() は一致したテキストになる.
  ## 文字列 'piiig' の中の 'iii' を探す
  ## パターンのすべてが一致しなければならないが, それはどこの場所でも構わない. 
  ## 成功したときは, match.group() に一致したテキストが入る. 
  match = re.search(r'iii', 'piiig') # =>  見つかる, match.group() == "iii"
  match = re.search(r'igs', 'piiig') # =>  見つからない, match == None

  ## . = \n 以外の任意の文字
  match = re.search(r'..g', 'piiig') # =>  見つかる, match.group() == "iig"

  ## \d = 数字, \w = 単語の文字
  match = re.search(r'\d\d\d', 'p123g') # =>  見つかる, match.group() == "123"
  match = re.search(r'\w\w\w', '@@abcd!!') # =>  見つかる, match.group() == "abc"

反復


パターン中の反復を示す +, * を使うと, 面白くなってきます.
  • + -- その左側のパターンの 1回以上の出現, 例えば, 'i+' = 1回以上の i の出現
  • * -- その左側のパターンの 0回以上の出現
  • ? -- その左側のパターンの 0回または 1回の出現

最左と最大


探すときにはまず, 最も左にあるパターンの一致を見つけ, 次に出来る限りその文字列を使い尽くそうとします. つまり, +, * は出来る限り伸びていこうとします (+, * は "貪欲" といわれます).

反復の例

  ## i+ = 1つ以上の i, 出来るだけ多く
  match = re.search(r'pi+', 'piiig') # =>  見つかる, match.group() == "piii"

  ## 最初/最左に現れる解を見つけ, その中で
  ## 出来る限り (最左で最大に) + を適用します. 
  ## この例では, 2番目の i の集まりまでは届かない. 
  match = re.search(r'i+', 'piigiiii') # =>  found, match.group() == "ii"

  ## \s* = 0個以上の空白に関する文字
  ## 空白に関する文字で分断されている場合を含む 3つの数字を探す. 
  match = re.search(r'\d\s*\d\s*\d', 'xx1 2   3xx') # =>  found, match.group() == "1 2   3"
  match = re.search(r'\d\s*\d\s*\d', 'xx12  3xx') # =>  found, match.group() == "12  3"
  match = re.search(r'\d\s*\d\s*\d', 'xx123xx') # =>  found, match.group() == "123"

  ## ^ = 文字列の先頭に一致, なので次は失敗します
  match = re.search(r'^b\w+', 'foobar') # =>  not found, match == None
  ## その ^ がなければ成功します
  match = re.search(r'b\w+', 'foobar') # =>  found, match.group() == "bar"

電子メールの例


文字列 'xyz alice-b@google.com purple monkey' の中にある電子メールアドレスを 見つけたいとします. もっと正規表現の特徴を説明するために, この例を使います. ここでは r'\w+@\w+' というパターンを使ってみます.
  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'\w+@\w+', str)
  if match:
    print match.group()  ## 'b@google'

\w が電子メールアドレスに含まれる '-' や '.' に一致しないので, この探し方では電子メールアドレスの全体を得られません. そこで以下に示す正規表現の特徴を使って, これを直します.

鉤括弧

鉤括弧は文字の集まりを示すために使うことが出来るので, [abc] は 'a', 'b', または 'c' に一致します. \w, \s などの規則は鉤括弧の中でも使えます. ひとつ例外があってドット (.) は文字通りドットを意味します. 電子メール問題に対して, 鉤括弧は電子メールアドレスの @ の前後のパターン r'[\w.-]+@[\w.-]+' に現れる文字の集合に '.' や '-' を加える簡単な方法です.
  match = re.search(r'[\w.-]+@[\w.-]+', str)
  if match:
    print match.group()  ## 'alice-b@google.com'
(鉤括弧の他の特徴) 範囲を示すのにハイフンを使うことが出来るので, [a-z] はすべての小文字に一致します. 範囲を示さないハイフンを表すには, [abc-] のようにハイフンを最後に書きます. 鉤括弧の集合の先頭に (^) をおくと, その集合の補集合を表すので, [^ab] は 'a', 'b' 以外の任意の文字を表します.

グループの抽出


正規表現の "グループ" の特徴により, 一致したテキストの部分を取り出すことが出来ます. 電子メール問題に対して, ユーザ名とホストを別々に取り出したいとします. そうするには, パターン中のユーザ名とホストを括弧 ( ) で囲い, r'([\w.-]+)@([\w.-]+)' のようにします. この場合, その括弧はパターンが一致する部分を変化させることはありませんが, 一致したテキストの中に論理的なグループを作ります. 一致したテキストが見つかったら, match.group(1) は左から 1つ目の括弧に対応する 一致したテキストになり, match.group(2) は左から 2つ目の括弧に括弧に対応します. 引数を取らない match.group() は通常通り, 一致したテキスト全体になります.
  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search('([\w.-]+)@([\w.-]+)', str)
  if match:
    print match.group()   ## 'alice-b@google.com' (一致全体)
    print match.group(1)  ## 'alice-b' (ユーザ名, グループ 1)
    print match.group(2)  ## 'google.com' (ホスト, グループ 2)

正規表現を使うときには, まず 探しているものに対するパターンを書いて, 必要な部分を取り出す括弧のグループを追加すること.

findall

findall() は re モジュールの中で最も強力な関数でしょう. これまで, パターンに対する 1つ目の一致を見つけるのに re.search() を使いました. findall() は *すべての* 一致を見つけ, 各要素がそれぞれの一致を表す文字列であるリストを返します.
  ## 多くの電子メールアドレスを含むテキストがあるとします. 
  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'

  ## re.findall() は, 見つかった電子メールの文字列をすべて含むリストを返します. 
  emails = re.findall(r'[\w\.-]+@[\w\.-]+', str) ## ['alice@google.com', 'bob@abc.com']
  for email in emails:
    # 見つかった電子メールの文字列に対する作業
    print email

ファイルに対する findall


ファイルを処理するときに, ファイルの各行に対する繰り返し処理を書くのが 習慣になっていると, 各行で findall() を呼び出すことになるでしょう. その代わりに, もっと良い方法として, findall() に繰り返し処理をさせてみましょう. ファイルのテキスト全体を findall() に渡し, 1回ですべての一致からなるリストを返させることが出来ます (f.read() はファイルのテキスト全体を文字列として返します).
  # ファイルを開く
  f = open('test.txt', 'r')
  # ファイルのテキストを findall() に渡して, 見つかった文字列全部のリストを返す. 
  strings = re.findall(r'some pattern', f.read())

findall とグループ


括弧 ( ) によるグループの仕組みは findall() と組み合わせられます. もしパターンが 2つ以上の括弧のグループを含むなら, 文字列のリストを返す代わりに, findall() はタプルのリストを返します. 各タプルはパターンの一致を 1つ表し, タプルの中に group(1), group(2), ... の データが入っています. このため, もし 2つの括弧のグループが電子メールのパターンに加えられたら, findall() は ('alice', 'google.com') のようにユーザ名とホストを含む, 長さ 2のタプルのリストを返します.
  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  tuples = re.findall(r'([\w\.-]+)@([\w\.-]+)', str)
  print tuples  ## [('alice', 'google.com'), ('bob', 'abc.com')]
  for tuple in tuples:
    print tuple[0]  ## ユーザ名
    print tuple[1]  ## ホスト
一旦タプルのリストを得れば, それぞれのタプルに対する何らかの処理を リスト全体に対して繰り返し行えます. もしそのパターンが括弧を含んでいなければ, findall() は前の例のように 見つかった文字列のリストを返します. (曖昧な追加的特徴: パターン内で括弧によりグループにしたいが取り出したくない場合があります. その場合に (?: ) のように括弧の先頭に ?: をつけると, その左の括弧はグループの結果として数に入らなくなります. )

正規表現に関する作業の流れとデバッグ


正規表現のパターンは数文字に多くの意味を詰め込みますが, それはとても密度が濃いので, そのパターンをデバッグするのに多くの時間が掛かります. 対話的なインタプリタを起動すると, 例えば小さな試しのテキストで実行して findall() の結果を表示する, といった ように, パターンを実行し, 一致した結果を簡単に表示できます. もしそのパターンが何にも一致しなければ, パターンのいくつかを取り除くなど, パターンの条件を弱めることで 多くの一致を得られるようになります. 何にも一致しないとき, 参考にする情報が何もないため, 作業を進めることができません. 一旦, 多くが一致するようになれば, 必要な文字列だけに当てはまるように 少しずつ条件を厳しくすることが出来ます.

選択肢


正規表現の関数はパターンの一致の動作を修正する選択肢を取ることが出来ます. 選択肢のフラグは, re.search(pat, str, re.IGNORECASE) のように, search() や findall() などに追加の引数として追加します.
  • IGNORECASE -- 一致させるときに大文字/小文字の違いを無視します. このため, 'a' は 'a' と 'A' の両方に一致します.
  • DOTALL -- ドット (.) を改行記号に一致させます. 通常, ドットは改行記号以外の任意の記号に一致します. この通常の設定ではつまづいてしまうことがあります. .* はすべてに一致すると考えていても, + による探索が行末を超えて進むことは初期設定のままではありません. \s (空白に関する文字) は改行記号を含むので, もし改行記号を含む可能性のある空白文字の一続きに一致させたいなら, \s* が使えます.
  • MULTILINE -- 複数行からなる文字列の中で, ^, $ を各行の先頭と末尾に一致させます. 通常, ^, $ は文字列全体の先頭と末尾のみに一致します.

貪欲対非貪欲 (任意)


ここでは練習問題に必要ない, より進んだ正規表現の技術を説明します.
タグのあるテキスト <b>foo</b> and <i>so on</i> を持っているとします.
それぞれのタグを '(<.*>)' というパターンに一致させようとすると, 何が初めに一致するでしょうか?
結果には少し驚かされますが, .* の貪欲な面によって '<b>foo</b> and <i>so on</i>' 全体が 1つの大きな一致 になります. この問題点は, .* が 1つ目の > で停止せずに, 出来る限り長くなろうとすることです (これは "貪欲" と呼ばれます).
.*? や .+? といった, 最後に ? を加える正規表現の拡張があり, これらを使うと貪欲な一致を行わなくなり, 出来るだけすぐに停止するようになります. パターン '(<.*?>)' に対して, 1つ目の一致は '<b>' に, 2つ目の一致は '</b>' になる, といった具合に それぞれの <..> の組が順番に得られます. その書き方は, まず .*? を書いて, そのすぐ右に .*? の一続きの最後になる完全な目印 (この場合は >) を付けます.
*? という拡張は Perl に由来して, Perl における拡張を含む正規表現は, Perl 互換の正規表現 (Perl Compatible Regular Expressions; pcre) として知られています. Python では pcre が使えるようになっています. 多くのコマンドラインユーティリティなどが, pcre のパターンを受け付けるフラグを持っています.
"ある文字で停止するまでのすべての文字" という考えをコード化する, 昔からある広く使われている方法は鉤括弧を使ったものです. 上の例に対しては, すべての文字を捉えるための .* の代わりに, > でないすべての記号を飛ばす [^>]* を使った パターンが使えます. (先頭の ^ は鉤括弧の中の集合に対する "補集合" を表すので, 鉤括弧の中にない記号ならどれとでも一致します).

置換 (任意)


re.sub(pat, replacement, str) 関数は, 与えられた文字列中にある パターンに一致する部分をすべて探し, 置き換えます. 置き換える文字列は, 元の置き換えられるテキストから取った group(1), group(2), .. にあるテキストを参照する '\1', '\2' を含むことが出来ます.
すべての電子メールアドレスに対して, ユーザ名 (\1) はそのままで ホストを yo-yo-dyne.com に変更する例を次に示します.
  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  ## re.sub(pat, replacement, str) -- 置き換えた新しい文字列を返します. 
  ## \1 は置換における group(1), \2 は group(2) を表します. 
  print re.sub(r'([\w\.-]+)@([\w\.-]+)', r'\1@yo-yo-dyne.com', str)
  ## purple alice@yo-yo-dyne.com, blah monkey bob@yo-yo-dyne.com blah dishwasher

練習問題


正規表現の練習問題として, Baby Names Exercise (英語原文), 新生児名問題 (日本語訳) があります.
この資料は Google の Nick Parlante によって作成されたものの翻訳です. Google による Python の授業の文章と映像は Creative Commons Attribution 2.5 ライセンスの下で利用できます.