8.3. 正規表現#

正規表現regular expressions)は、文字列データのさまざまなパターンを厳密に表現するためのルールであり、文字列の中から特定のパターンを検索したり、置き換えたりする際に使われます。正規表現を利用することで、精密かつ高速に処理を行うことができます。正規表現は強力な魔法。でも、初心者が軽い気持ちで手を出すと、悲劇と後悔が待っています。覚悟を持って。

正規表現を利用するには、re モジュールをインポートします。

import re

8.3.1. 検索#

正規表現を利用して検索を行う例を見ていきましょう。ある文字列が与えられたときに、その中に「at」が存在するかどうかを検索してみます。正規表現を利用して検索を行う場合、re モジュールの re.search 関数を使用します。この関数には、検索したいパターンの正規表現と検索対象の文字列を、順に与えます。通常の文字列を検索するのであれば、正規表現もその文字列と同じ内容になります。

s = 'Everything goes wrong all at once.'

m = re.search('at', s)

検索結果は変数 m に保存されます。指定されたパターンが見つかると、m.span() で検索に一致した部分の開始位置と終了位置を取得できます。また、m.group() を使うと、実際に一致した文字列を取得できます。今回は通常の文字列をパターンとして指定しているため、検索にヒットした部分は検索パターンと同じになっています。

m
<re.Match object; span=(26, 28), match='at'>
m.span()
(26, 28)
m.group()
'at'

なお、正規表現で検索パターンを指定する際には、raw string を使うことが推奨されています。これは、検索パターンの前に r を付けて書く方法で、バックスラッシュ(\)などの特殊文字をそのままの意味で扱うことができます。理由?そんなものは知らなくていいし、理解しなくても動きます。そういう決まりです。黙って r をつけましょう。例えば、次のように。

m = re.search(r'at', s)

なお、指定されたパターンが見つからない場合、変数 mNone になります。検索がヒットしたかどうかで処理を分岐したい場合は、if 文を使って mNone かどうかを判定すればよいでしょう。

s = 'If everything seems to be going well, you have obviously overlooked something.'

m = re.search(r'at', s)
print(m)
None

次に、少し複雑な検索パターンを見ていきましょう。たとえば、「to be から始まり、well で終わる」というパターンは、'to be.*well' と書きます。ここで、.* は特殊な意味を持つメタ文字と呼ばれる記号です。. は任意の 1 文字を表し、* は直前の文字またはパターンが 0 回以上繰り返されることを意味します。これらを組み合わせることで、「to be と well の間に任意の文字が 0 個以上ある」というパターンを表現しています。

s = 'If everything seems to be going well, you have obviously overlooked something.'

m = re.search(r'to be.*well', s)
m.span()
(20, 36)
m.group()
'to be going well'

また、検索パターンの一部を () で囲むことで、その部分をあとから取り出すことができます。たとえば、to be と well の間の文字を取り出したい場合は、'to be(.*)well' と記述します。

s = 'If everything seems to be going well, you have obviously overlooked something.'

m = re.search(r'to be(.*)well', s)
m.span()
(20, 36)
m.group()
'to be going well'

group メソッドに引数として 1 を指定すると、最初のカッコで囲まれた部分(キャプチャグループ)を取得できます。このとき、(.*) は be の直後から well の直前までの文字列を含むため、be の後と well の前にある空白も取得されます。なお、検索パターンに複数の () が含まれている場合は、1 から順に番号を指定することで、それぞれのマッチ部分を取得できます。

m.group(1)
' going '

正規表現で ? を付けると、「できるだけ少ない文字」で一致する部分を検索します。違いを見てみましょう。

s = 'Anything that can go wrong will go wrong.'

m1 = re.search(r'go(.*)wrong', s)
m2 = re.search(r'go(.*?)wrong', s)

m1.group(1)
' wrong will go '
m2.group(1)
' '

'go(.*?)wrong' を指定すると、「できるだけ少ない文字」で検索されるので、最初の go と wrong の間にある空白が検索されていることが確認できます。

re.search 関数は、最初に見つかった部分だけを返します。検索対象の文字列の中に検索パターンが複数含まれている場合、それらをすべて検索したいときは、re.finditer 関数を利用します。re.finditer はリストのようなオブジェクト(イテレーター)を返すため、for 文で順に取り出すことができます。

次の例では、文字列データから "I'm fine" をすべて見つける例です。

s = '''
She said, "I'm fine," after I asked what was wrong.
Then she smiled and added, "It's nothing."
But she didn’t text for three days.
On the fourth day, she said again, "I'm fine."
And then, "No problem." Classic.
'''

matches = re.finditer(r"I'm fine", s)

for m in matches:
    msg = m.group()
    start, end = m.span()
    print(f'"{msg}" at position {start}-{end}')
"I'm fine" at position 12-20
"I'm fine" at position 168-176

また、"I'm fine" のほかに "It's nothing" も合わせて検索したい場合は、検索したい二つのキーワードの間に | を入れます。| は正規表現で「または」を意味します。

matches = re.finditer(r"I'm fine|It's nothing", s)

for m in matches:
    msg = m.group()
    start, end = m.span()
    print(f'"{msg}" at position {start}-{end}')
"I'm fine" at position 12-20
"It's nothing" at position 81-93
"I'm fine" at position 168-176

さらに応用例を見ていきましょう。この文字列データでは、彼女が話したことがすべて " で囲まれています。そこで、正規表現を利用して、彼女の話したことをすべて取得してみます。

matches = re.finditer(r'"([^"]+)"', s)

for m in matches:
    msg = m.group(1).rstrip('.,')
    start, end = m.span()
    print(f'"{msg}" at position {start}-{end}')
"I'm fine" at position 11-22
"It's nothing" at position 80-95
"I'm fine" at position 167-178
"No problem" at position 189-202

「I’m fine」を正規表現で拾えたけど、感情分析は別途必要。文字通りに受け取ると、痛い目にあうからね。

8.3.2. 置換#

文字列の中にある特定のパターンを検索し、それを他の文字列に置換するには、re.sub 関数を利用します。re.sub 関数の使い方は、検索するパターン、置換後の文字列、置換対象の文字列の順に引数を与えます。

s = 'Anything that can go wrong will go wrong.'
t = re.sub(r'wrong', r'WRONG', s)
t
'Anything that can go WRONG will go WRONG.'

re.sub 関数では、4 番目の引数に置換の回数を指定することができます。

s = 'Anything that can go wrong will go wrong.'
t = re.sub(r'wrong', r'WRONG', s, 1)
t
'Anything that can go WRONG will go wrong.'

なお、検索パターンに () を用いて一部をグループ化した場合、re.sub 関数の置換文字列の中で \1 のように参照して利用することができます。カッコが複数ある場合は、\1\2 のように番号で順に呼び出すことができます。

たとえば、日付フォーマットを「DD/MM/YYYY」から「YYYY-MM-DD」に変換する例では、まず検索パターンで DD、MM、YYYY のそれぞれを () で囲み、置換文字列の中で \3-\1-\2 と指定することで、マッチした値を順番に並び替えています。

s = 'I started my fitness journey on 01/01/2024. It ended on 01/03/2024.'
t = re.sub(r'(\d{2})/(\d{2})/(\d{4})', r'\3-\1-\2', s)
t
'I started my fitness journey on 2024-01-01. It ended on 2024-01-03.'

正規表現で日付は整えられても、人生の不確かさまでは置換できません。

8.3.3. 分割#

通常の文字列分割であれば、文字列オブジェクトの split メソッドでも十分ですが、より柔軟かつ厳密に文字列を分割したい場合は、re モジュールの split 関数を利用します。たとえば、次のように一つの文字列をカンマ(,)またはカンマと空白(, )で分割する例です。

s = '21, 31,41, 51,   61'
d = re.split(r', *', s)
d
['21', '31', '41', '51', '61']

このように、re.split を使うと、カンマの後に空白があっても、正規表現を正しく指定すればうまく分割できます。

8.3.4. メタ文字#

正規表現で使用する特殊な機能を持つ文字のことをメタ文字と呼びます。たとえば、上の例で見たように、. は任意の 1 文字を表し、* は直前の文字またはパターンの 0 回以上の繰り返しを意味します。これら以外にも、多くのメタ文字が Table 8.1 のように定義されています。

Table 8.1 正規表現で使用するメタ文字の一覧#

メタ文字

内容

.

改行以外の任意の 1 文字(DOTALL フラグで改行も含む)

^

文字列の先頭(MULTILINE フラグで各行の先頭にもマッチ)

$

文字列の末尾(MULTILINE フラグで各行の末尾にもマッチ)

*

直前のパターンを 0 回以上繰り返し

+

直前のパターンを 1 回以上繰り返し

?

直前のパターンを 0 回または1回繰り返し

{m}

直前のパターンを m 回繰り返し

{m, n}

直前のパターンを m 〜 n 回繰り返し

[]

文字の集合 - [] 内のいずれか 1 文字にマッチ

`

`

? を見て、混乱してきましたか。大丈夫、筆者も最初は ? に疑問しか感じませんでした。正規表現の世界では、同じ記号が文脈で意味を変えるなんて、日常茶飯事。そういう世界です、受け入れてください。

8.3.5. 特殊シーケンス#

正規表現でよく使われるパターンの組み合わせは、特殊シーケンスとして定義されています。たとえば、0 から 9 までのいずれかの数字 1 つを正規表現で表すと、[0123456789] または [0-9] と書きます。これに対して、特殊シーケンスを使うと \d と書くだけで数字 1 つを表現できます。よく使われる特殊シーケンスは Table 8.2 の通りです。

Table 8.2 正特殊シーケンス#

メタ文字

内容

\n

改行コード

\r

改行コード

\t

タブ

\d

数字 [0-9]

\d

数字 [0-9]

\D

数字以外 [^0-9]

\w

英数字およびアンダースコア [a-zA-Z0-9_]

\W

英数字およびアンダースコア以外 [^a-zA-Z0-9_]

\s

空白文字 [\t\n\r\f]

\S

空白文字意外 [^\t\n\r\f]

8.3.6. コンパイル#

re モジュールの使い方として、これまでに説明したように、関数に検索パターンと検索対象を順に渡して検索や置換を行う方法のほかに、正規表現をあらかじめコンパイルして利用する方法があります。正規表現をコンパイルすると、処理のたびにコンパイルを行う必要がなくなり、より効率的に処理が実行できます。ただし、生物学で扱うデータの場合、多くはコンパイルしても処理効率が劇的に変わるわけではないため、必ずしもコンパイルして使う必要はありません。

正規表現をコンパイルせずに検索する場合、次のように書きます。

s = 'Everything goes wrong all at once.'

m = re.search(r'at', s)

一方、正規表現をコンパイルしてから利用する場合は、まず re.compile 関数で検索パターンをコンパイルし、その結果得られるオブジェクトのメソッドを使って検索を行います。このコンパイル済みオブジェクトには検索パターンの情報が含まれているため、検索メソッドを呼び出す際に再度検索パターンを指定する必要はありません。

ptn = re.compile(r'at')
m = ptn.search(s)