ゼロから覚えるPython正規表現の基本とTips

Pythonの正規表現についてです。今まで必要なときにググって調べて実装していましたが、そろそろ理解を深めようと思いまとめてみました。「必要なときにググって調べて実装」なんて偉そうなことを言っていますが、初心者レベルです。**初心者がゼロから覚えたほうがいい内容**、および**たまに使う人が学習し直す内容**を意識してい書いています。
当記事では[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)[「第3章: 正規表現」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch3)で学習したことを整理しています。

Python実行が面倒なときは、以下ツールでチェックしています(便利)。

https://weblabo.oscasierra.net/tools/regex/

https://regex101.com/

# 参考リンク

|リンク|備考|
|:--|:--|
|[正規表現 HOWTO](https://docs.python.org/ja/3/howto/regex.html#regex-howto)|Python公式の正規表現 How To|
|[re --- 正規表現操作](https://docs.python.org/ja/3/library/re.html)|Python公式のreパッケージ説明|

# 基本
Pythonでは[パッケージ`re`](https://docs.python.org/ja/3/library/re.htm)を使って正規表現の実装します。以後のPython文内では`import re`を省略します。

```python
import re
```

## 2種類の使い方
### 1. 関数を直接使用
`re.match`や`re.sub`などの関数を使います。

```python
# 第1引数が正規表現パターン(検索語句)、第2引数が検索対象
result = re.match('Hel', 'Hellow python')

print(result)
# <_sre.SRE_Match object; span=(0, 3), match='Hel'>

print(result.group())
# Hel
```

### 2. コンパイルして使用
正規表現パターンをコンパイルした後に`match`や`sub`などの関数を使用します。

```python
# あらかじめ正規表現パターンをコンパイル
regex = re.compile('Hel')

result = regex.match('Hellow python')

print(result)
# <_sre.SRE_Match object; span=(0, 3), match='Hel'>

print(result.group())
# Hel
```

### 2種類の使い分け
**複数の正規表現パターンを何度も使う場合はコンパイル方式を使います**。[公式](https://docs.python.org/ja/3/library/re.html#re.compile)に以下の記載があります。

> re.compile() を使い、結果の正規表現オブジェクトを保存して再利用するほうが、一つのプログラムでその表現を何回も使うときに効率的です。
> re.compile() やモジュールレベルのマッチング関数に渡された最新のパターンはコンパイル済みのものがキャッシュされるので、一度に正規表現を少ししか使わないプログラムでは正規表現をコンパイルする必要はありません。

同じ正規表現パターンを何度も使う場合はコンパイルしても速さのメリットはないようです。どれだけキャッシュされるのかは調べていません。

# 正規表現パターン(検索語句)の定義
## raw文字列でエスケープシーケンス無効
raw文字列(raw string)は正規表現固有のトピックではありませんが、使うことで**エスケープシーケンスを無効にできます**。

以下の例の前者だと`\t`はタブ、`\nが`改行となるが、後者だとそのまま`\t`,`\n`文字列として扱われる

```python
print('a\tb\nA\tB')
print(r'a\tb\nA\tB')
```

```bash:ターミナル出力結果
a	b
A	B

a\tb\nA\tB
```

**正規表現パターン内でバックスラッシュなどに対してエスケープシーケンスを書きたくないのでraw文字列を使います**

```python
result = re.match(r'\d', '329')
```

記事[「Python の raw 文字列を用いて正規表現を書く」](https://pyxis-dev.hatenadiary.org/entry/20080229/1204290507)や[「Pythonでエスケープシーケンスを無視（無効化）するraw文字列」](https://note.nkmk.me/python-raw-string-escape/)に詳しい解説があります。


## トリプルクォートと`re.VERBOSE`で改行・コメント・空白無視
`'''`トリプルクォート(`"""`でも可能)で囲むことにより、正規表現パターン中に改行を使うことができます(改行がなくても問題なし)。
`re.VERBOSE`を渡すことで、空白やコメントを正規表現パターンから除外できます。
**リプルクォートと`re.VERBOSE`で非常に読みやすくなります**
以下のような正規表現パターンを記述すると見やすいですね。

```python
a = re.compile(r'''\d +  # the integral part
                   \.    # the decimal point
                   \d *  # some fractional digits''', re.VERBOSE)
```

トリプルクォートに関しては、記事[「Pythonで文字列生成（引用符、strコンストラクタ）」](https://note.nkmk.me/python-str-literal-constructor/)に詳しくかかれています。

ちなみに`compile`のパラメータ`flags`で複数のコンパイルフラグを使いたい場合は、単純に`+`(加算)してやればOKです。

```python
a = re.compile(r'''\d''', re.VERBOSE+re.MULTILINE)
```

## 特殊文字

|文字|説明|備考|例|マッチする|マッチしない|
|:---|:---|:---|:---|:---|:---|
\d|数字|[0-9]と同じ
\D|数字以外|[^0-9]と同じ
\s|空白文字|[\t\n\r\f\v]と同じ
\S|空白文字以外|[^\t\n\r\f\v]と同じ
\w|英数文字と下線|[a-zA-Z0-9_]と同じ
\W|英数文字以外|[\a-zA-Z0-9_]と同じ
\A|文字列の先頭|^と類似
\Z|文字列の末尾|$と類似
\b|単語の境界(スペース)|
.|任意の一文字|-|1.3|123, 133|1223
^|文字列の先頭|-|^123|1234|0123
$|文字列の末尾|-|123$|0123|1234
*|０回以上の繰り返し|-|12*|1, 12, 122|11, 22
+|１回以上の繰り返し|-|12+|12, 122|1, 11, 22
?|０回または１回|-|12?|1, 12|122
{m}|m回の繰り返し|-|1{3}|111|11, 1111
{m,n}|m〜n回の繰り返し|-|1{2, 3}|11, 111|1, 1111
[]|集合|[^5]とすると5以外|[1-3]|1, 2, 3|4, 5
&#124;|和集合(or)|-|1&#124;2|1, 2|3
()| グループ化|-|(12)+|12, 1212|1, 123

# マッチ関数
以下の関数をよく使います。

|関数|目的|
|:-:|:--|
|[match](https://docs.python.org/ja/3/library/re.html#re.match)|文字列の**先頭で**正規表現とマッチするか判定|
|[search](https://docs.python.org/ja/3/library/re.html#re.search)|正規表現がどこにマッチするか検索|
|[findall](https://docs.python.org/ja/3/library/re.html#re.findall)|マッチする部分文字列を全て探しリストとして返します|
|[sub](https://docs.python.org/ja/3/library/re.html#re.sub)|文字列置換|

## [match](https://docs.python.org/ja/3/library/re.html#re.match)と[search](https://docs.python.org/ja/3/library/re.html#re.search)
文字列の先頭でのみのマッチするのが`re.match`で文字列中の位置にかかわらずマッチするのが`re.search`。詳しくは[公式の「search() vs. match()」](https://docs.python.org/ja/3/library/re.html#search-vs-match)を参照。両者とも最初のパターンのみを返します(2回目以降にマッチしたものを返さない)。

```python
>>> re.match("c", "abcdef")    # 先頭が"c"でないのでマッチしない
>>> re.search("c", "abcdef")   # マッチする
<re.Match object; span=(2, 3), match='c'>
```

結果は`group`に入っています。`group(0)`に結果がすべて入っていて、グループ化した検索結果は連番で1から入っています。

```python
>>> m = re.match(r"(\w+) (\w+)", "Isaac Newton, physicist")
>>> m.group(0)       # The entire match
'Isaac Newton'
>>> m.group(1)       # The first parenthesized subgroup.
'Isaac'
>>> m.group(2)       # The second parenthesized subgroup.
'Newton'
>>> m.group(1, 2)    # Multiple arguments give us a tuple.
('Isaac', 'Newton')
```

## [findall](https://docs.python.org/ja/3/library/re.html#re.findall)
パターンにマッチした全ての文字列をリスト形式で返すのが[findall](https://docs.python.org/ja/3/library/re.html#re.findall)。

```python
>>> text = "He was carefully disguised but captured quickly by police."
>>> re.findall(r"\w+ly", text)
['carefully', 'quickly']
```

`()`を使ってキャプチャ対象を指定できますが、複数指定した場合は以下のようになります。グループごとにタプルで返ってきます。

```python
>>> print(re.findall(r'''(1st)(2nd)''', '1st2nd1st2nd'))
[('1st', '2nd'), ('1st', '2nd')]
```

## [sub](https://docs.python.org/ja/3/library/re.html#re.sub)
文字置換をします。引数の順に1. 正規表現パターン、2. 置換後の文字列、3. 置換対象文字列です。

```python
>>> re.sub(r'置換前', '置換後', '置換前 対象外 置換前')
'置換後 対象外 置換後'
```

※ちなみに4つ目の引数は`count`, 5つ目は`flags`(コンパイルフラグ)です。4つ目に気づかずに必死でコンパイルフラグを渡した気になっていて、うまく行かずに30分ほど無駄にしたことがありました・・・

# [コンパイルフラグ](https://docs.python.org/ja/3/howto/regex.html#compilation-flags)
コンパイルフラグでよく使うものは以下のあたり。関数のパラメータ`flags`に渡します。

|フラグ|意味|
|:-:|:--|
|[DOTALL](https://docs.python.org/ja/3/library/re.html#re.DOTALL)|`.`を改行を含む任意の文字に設定|
|[IGNORECASE](https://docs.python.org/ja/3/library/re.html#re.IGNORECASE)|大文字小文字を区別しない|
|[MULTILINE](https://docs.python.org/ja/3/library/re.html#re.MULTILINE)|`^`や`$`で複数行文字列に対するマッチングを行います。|
|[VERBOSE](https://docs.python.org/ja/3/library/re.html#re.VERBOSE)|正規表現内のコメントと空白無視|

既に説明したVERBOSEとわかりにくいやつと以外を少し詳しく解説します。

### [DOTALL](https://docs.python.org/ja/3/library/re.html#re.DOTALL)
re.DOTALLはワイルドカードである`.`(DOT)に対して改行を含めるオプションです。

```python
string = r'''\
行頭 1st line
行頭 2nd line'''

print(re.findall(r'1st.*2nd', string, re.DOTALL))
# ['1st line\n行頭 2nd']

print(re.findall(r'1st.*2nd', string))
# No Match
```

詳細は記事[「Python: 正規表現で複数行マッチングの置換を行う」](http://messefor.hatenablog.com/entry/2017/01/15/215722)参照。

### [MULTILINE](https://docs.python.org/ja/3/library/re.html#re.MULTILINE)
複数行に対してそれぞれ検索したい場合に使います。以下の例だと`re.MULTILINE`を使うと2行目(「行頭 2nd line」)も対象となります。
[※`match`関数の場合は、`re.MULTILINE`を使っても意味ありません](https://docs.python.org/ja/3/library/re.html#search-vs-match)

```python
string = r'''\
行頭 1st line
行頭 2nd line'''

print(re.findall(r'^行頭.*', string, re.MULTILINE))
# ['行頭 1st line', '行頭 2nd line']

print(re.findall(r'^行頭.*', string))
# ['行頭 1st line']
```

詳細は記事[「Python: 正規表現で複数行マッチングの置換を行う」](http://messefor.hatenablog.com/entry/2017/01/15/215722)参照。

# Tips
## キャプチャ対象外
`(?:...)`をつけると**検索結果文字列に含めず**キャプチャ対象外となります。公式の[正規表現のシンタックス](https://docs.python.org/ja/3/library/re.html#regular-expression-syntax)では以下のように説明があります。

> 普通の丸括弧の、キャプチャしない版です。丸括弧で囲まれた正規表現にマッチしますが、このグループがマッチした部分文字列は、マッチを実行したあとで回収することも、そのパターン中で以降参照することも できません 。

以下の例では、`4`の部分を正規表現パターンとはしていますが、結果には出力していません。

```python
>>> re.findall(r'(.012)(?:4)', 'A0123 B0124 C0123')
['B012']
```

## 貪欲・非貪欲マッチ
**検索結果対象文字列の長さをコントロール**することができます。**最大限の長さでマッチさせるのが貪欲マッチ**(greedy match)で**最小限の長さでマッチさせるのが非貪欲マッチ**(Non-greedy match)です。
デフォルトは貪欲マッチで、非貪欲マッチにするためには連続系特殊文字(`*, ?, +`)に`?`をくっつけます。以下が両者の例文です。

```python
# 貪欲マッチ
>>> print(re.findall(r'.0.*2',  'A0123 B0123'))
['A0123 B012']

# 非貪欲マッチ(*の後に?)
>>> print(re.findall(r'.0.*?2', 'A0123 B0123'))
['A012', 'B012']
```

詳細は記事[「貪欲マッチと非貪欲マッチ」](https://python.atelierkobato.com/multi/#outline__3)を参照ください。

## 後方参照
`\number`を使うことで前のグループの中身にマッチさせることができます。[公式シンタックス](https://docs.python.org/ja/3/library/re.html#regular-expression-syntax)では下記の記述。

> 同じ番号のグループの中身にマッチします。グループは 1 から始まる番号をつけられます。例えば、 (.+) \1 は、 'the the' あるいは '55 55' にマッチしますが、 'thethe' にはマッチしません(グループの後のスペースに注意して下さい)。この特殊シーケンスは最初の 99 グループのうちの一つとのマッチにのみ使えます。 number の最初の桁が 0 であるか、 number が 3 桁の 8 進数であれば、それはグループのマッチとしてではなく、 8 進値 number を持つ文字として解釈されます。文字クラスの '[' と ']' の間では全ての数値エスケープが文字として扱われます。

具体的にはこんな感じで、`\1`部分は前の`(ab)`でマッチした部分と同じ意味でabcabはマッチしますが、abdddは4文字目と5文字目がabではないのでマッチしません。
※Pythonのリストなどは0からカウントしますが、正規表現では1からカウントです。

```python
>>> print(re.findall(r'''(ab).\1''', 'abcab abddd'))
['ab']
```

## 先読み・後読みアサーション
マッチ対象には含めないけど、検索条件に文字列を含める・含めないという使い方に対して以下の4つがあります。

- 肯定の先読みアサーション(Positive Lookahead Assertions)
- 否定の先読みアサーション(Negative Lookahead Assertions)
- 肯定の後読みアサーション(Positive Lookbehind Assertions)
- 否定の後読みアサーション(Negative Lookbehind Assertions)

マトリックスにすると以下の形。

||肯定|否定|
|:-:|:--|:--|
|**先読み**|`(?=...)`<br>`...`部分が次に続けばマッチ|`(?!...)`<br>`...`部分が次に続かなければマッチ|
|**後読み**|`(?<=...)`<br>`...`部分が現在位置より前でマッチがあればマッチ|`(?<!...)`<br>`...`部分が現在位置より前でマッチがなければマッチ|

細かい説明より、具体例の方がわかりやすいです。

```python
>>> string = 'A01234 B91235 C01234'

# 肯定の先読みアサーション(Positive Lookahead Assertions)
# '123'の次に'5'が続く文字列('(?=5)'の部分は後続の'.'がなければ取得しない)
>>> print(re.findall(r'..123(?=5).', string))
['B91235']

# 否定の先読みアサーション(Negative Lookahead Assertions)
# '123'の次に'5'が続かない文字列('(?!5)'の部分は後続の'.'がなければ取得しない)
>>> print(re.findall(r'..123(?!5).', string))
['A01234', 'C01234']

# 肯定の後読みアサーション(Positive Lookbehind Assertions)
# '0'が'123'の前にマッチする文字列('(?<=0)'の部分は先頭の'.'がなければ取得しない)
>>> print(re.findall(r'..(?<=0)123', string))
['A0123', 'C0123']

# 否定の後読みアサーション(Negative Lookbehind Assertions)
# '0'が'123'の前にマッチしない文字列('(?<!0)'の部分は先頭の'.'がなければ取得しない)
>>> print(re.findall(r'..(?<!0)123', string))
['B9123']
```