done is better than perfect

自分が学んだことや、作成したプログラムの記事を書きます。すべての記載は他に定める場合を除き個人的なものです。

7つのPythonにおける正規表現例 (Re Match Search FindAll)

7 Python Regular Expressions Examples – Re Match Search FindAlの一部翻訳+αです。

正規表現は特に混乱することが多いのでメモ。一部端折るので必ず元記事の方も参照して下さい。 また、必要に応じてPythonの公式ドキュメントも参照しています。


Python正規表現パッケージは以下でimportできる。

In [1]: import re

1. Raw Strings in Python

Pythonのパーサーは文字列リテラル中にある\(バックスラッシュ、Windowsなど環境によっては円記号)をエスケープ文字と見做す。

例えば、Pythonのパーサーは\nを改行と置き換える。

この動作は正規表現を使用する際には結構困る。なぜなら、reパッケージでは正規表現において特別な意味を持つ文字(例えば*+)などを エスケープしたいときにも\を使用するからである。

従って、上で挙げたような文字を正規表現で文字としてマッチさせたいときには、エスケープ文字をエスケープする必要がある。 ([訳注]例えば、バックスラッシュ自体を正規表現でマッチさせたいときには\\\\という正規表現を書く必要がある。なぜなら、 正規表現でバックスラッシュをマッチさせるときには\\と書く必要があり、更にPythonの文字列リテラルでは バックスラッシュそれぞれをエスケープするためにバックスラッシュを書く必要があるためである。公式ドキュメント参照)

頭を抱えていくつバックスラッシュが必要か数えるより、raw stringを使いましょう。

raw stringを使うには、普通の文字列記法の左側のクオートの左側にrと書く(Python2系列までのユニコード文字列と同様)。 文字列がrawであるとき、Pythonは普通の文字列リテラルに行うような解析を試みない。

In [2]: string = "This is a\nnormal string"

In [3]: rawString = r"and this is a\nraw string"

In [4]: print(string)
This is a
normal string

In [5]: print(rawString)
and this is a\nraw string

Perfoming Queries with Regex in Python

reパッケージには色んなメソッドがあるが、ここでは以下の3つについて話す。

  • re.match()
  • re.search()
  • re.findall()

それぞれ正規表現と文字列をマッチさせるためのパターンとして使える。それぞれどう違うのかを見ていこう。

2. Find Using re.match - Matches Beginning

まずはmatch()から。match()は検索したいパターンが検索中の文字列の先頭とマッチした場合にマッチしたものを返す。

In [6]: re.match(r'dog', 'dog cat dog')
Out[6]: <_sre.SRE_Match object; span=(0, 3), match='dog'>

In [7]: match = re.match(r'dog', 'dog cat dog')

In [8]: match.group(0)
Out[8]: 'dog'

group()については後で話す。今はgroup()は引数として0を与えると、クエリにマッチしたパターンを返すことだけ覚えておけば良い。

SRE_Matchについてもはぐらかしているけど、こちらも後で。

上記の例でクエリとしてcatを与えるとこうなる。

In [10]: re.match(r'cat', 'dog cat dog')

In [11]: 

3. Find Using re.search - Matches Anywhere

search()match()と似ているが、search()は文字列の先頭だけでなく、文字列中どこにあってもクエリにマッチするパターンを探し出す。

先ほどのcatの例でも、search()なら探しだすことができる。

In [12]: match = re.search(r'cat', 'dog cat dog')

In [13]: match.group(0)
Out[13]: 'cat'

しかしsearch()メソッドは、最初にパターンを見つけたところで止まってしまう。検索対象文字列中に複数回パターンがある場合でも、 最初に見つかったパターンだけを返す。

In [14]: match = re.search(r'dog', 'dog cat dog')

In [15]: match.group(0)
Out[15]: 'dog'

4. Get Using re.findall - All Matching Objects

著者が最もPythonでよく使うのはfindall()である。match objectを返すのではなく、findall()を使用した場合には マッチしたパターン全てをリストとして返してくれる。著者にとってこれはとてもシンプルである。

In [16]: re.findall(r'dog', 'dog cat dog')
Out[16]: ['dog', 'dog']

In [17]: re.findall(r'cat', 'dog cat dog')
Out[17]: ['cat']

5. Use match.start and match.end Methods

match objectについて説明する。

search()match()はシンプルにマッチした部分文字列を返すのではなく、そのラッパーを返す。

さっきgroup()でマッチした部分文字列を取得していたけど、(match objectとはgroupingと一緒に使うとマジ便利。後述)、match objectはマッチした部分文字列以上の情報を含んでいる。

例えば、マッチした部分が文字列のどこの位置にあるのかを知ることができる。

In [18]: match = re.search(r'dog', 'dog cat dog')

In [19]: match.start()
Out[19]: 0

In [20]: match.end()
Out[20]: 3

時々便利ですよね

6. Group by Number Using match.group

さっき行ったように、match objectはgroupingと一緒に使うとマジ便利。

Groupingは正規表現でマッチした場所にアクセスするための機能である。 私達はgroupを正規表現の検索文字列の一部分であると定義することができ、マッチした部分にアクセスすることができる。

例を見てみましょう。

In [21]: contactInfo = 'Doe, John: 555-1212'

文字列には誰かの名前とアドレスが記載されている。この文字列に対して、私達は正規表現をマッチさせることができる。

In [22]: re.search(r'\w+, \w+: \S+', contactInfo)
Out[22]: <_sre.SRE_Match object; span=(0, 19), match='Doe, John: 555-1212'>

正規表現のパーツを丸括弧(())で囲むことにより、コンテンツをグループ分けすることができる。

In [23]: match = re.search(r'(\w+), (\w+): (\S+)', contactInfo)

group化したパターンはgroup()メソッドで取得することができる。パターンが出現した順番でインデキシングされている。 スタートはgroup(1)である。

In [24]: match.group(1)
Out[24]: 'Doe'

In [25]: match.group(2)
Out[25]: 'John'

In [26]: match.group(3)
Out[26]: '555-1212'

なぜgroup(1)から始まるかというと、group(0)はマッチした部分全てを表すものであるからである。

In [27]: match.group(0)
Out[27]: 'Doe, John: 555-1212'

7. Grouping by Name Using match.group

特に沢山のgroupを持つ正規表現だと、数字でgroupにアクセスするのはめんどくさい。 Pythonでは名前でgroupにアクセスすることができる。

In [28]: match = re.search(r'(?P<last>\w+), (?P<first>\w+): (?P<phone>\S+)', contactInfo)

group()で引数として数字ではなく名前を渡す。

In [33]: match.group('first')
Out[33]: 'John'

In [34]: match.group('last')
Out[34]: 'Doe'

In [35]: match.group('first')
Out[35]: 'John'

In [36]: match.group('phone')
Out[36]: '555-1212'

むっちゃ読みやすくなったよね。正規表現がもっともっと複雑になった場合を考えると、数字でgroupにアクセスするのは非常に難しい。 名前を割り当てることはコードを読む人にとって非常にわかりやすい。

Groupingはmatch objectを返さないfindall()メソッドでも使える。その代わり、findall()はタプル(tuple)のリストを返す。 N番目のタプルの要素はN番目のgroupと等しい。

In [37]: re.findall(r'(\w+), (\w+): (\S+)', contactInfo)
Out[37]: [('Doe', 'John', '555-1212')]

しかしながら、名前付きgroupはfindall()では使えない。

この記事ではPythonでの基本的な正規表現の使い方を紹介した。raw stringの使い方や、match()search()findall()、について学び、 groupingについても学んだ。

いつものように、もっと情報を探したいときには、公式ドキュメントのreパッケージは 重要な情報源となりますよ。

これからの記事では、もっと深く正規表現について紹介する。もっと幅広い表現をキャプチャできるマッチの仕方について語ったり、 文字列の置換えや、文字列以外のPythonのデータ構造をパースするときにはどうすればいいかなどについて教える。

翻訳終わり

個人的には、findall()よりfinditer()をよく使います。finditer()では、match objectのインスタンスを見つかった順に返すiteratorです。

以下のように使います。

n [45]: for m in re.finditer(r'dog', 'dog cat dog'):
....:     print(m.group(0))
....:     
dog
dog