正規表現を使って検索・置換する ― 基本編【Python】

正規表現とは文字列の検索・置換をする方法の1つですが、検索する文字列をパターンで指定することで高度な「あいまい検索」を可能とします。ここではPythonを用いた正規表現での検索・置換方法を見ていきます。

正規表現とは

正規表現は高度な「あいまい検索」を実現させるための手法の1つです。例えば、電話番号を描くときは「090-1234-5678」とハイフンを入れて書く場合もあれば、 「09012345678」 と数字だけで書く場合もあります。あるいは「(090)1234-5678」のような形式で書かれているかもしれません。通常の検索でこれらをすべてマッチさせるのは大変ですが、正規表ではこれらをすべて含むパターンを表現することができ、そのパターンを使って検索を実行することができます。

正規表現に用いられる記号は以下のようなもので、これらを組み合わせて複雑なパターンを作成することができます。

正規表現に用いる記号

特定の1文字の表現方法

正規表現意味指定例マッチする結果
.任意の1文字a.bacb, adb, aeb , …
[〇〇〇][]内の任意の1文字。ハイフン(-)で範囲を表すこともできる[a-d]a, b, c, d
[^〇〇〇][]内の文字以外の任意の1文字。ハイフン(-)で範囲を表すこともできる[^a-d]e, f, g, h , …
(〇|●)〇または●a|ba, b
^文字列の先頭^abcabcdef
$文字列の末尾abc$xyzabc

特殊シークエンス

よく使われる文字列のパターンは特殊シークエンスとして簡単に表現できるように用意されています。

正規表現意味同義表現
\d0-9までの数字のいずれか1文字[0-9]
\D0-9までの数字以外のいずれか1文字[^0-9]
\s空白文字、タブコード、改行コードのいずれか1文字[\t\n\r\f\v]
\S空白文字以外のいずれか1文字[^\t\n\r\f\v]
\w任意の英数字のうちの1文字[a-zA-Z0-9_]
\W英数字以外のいずれか1文字[^a-zA-Z0-9_]
\A文字列の先頭^
\Z文字列の末尾$

繰り返しパターンの表現方法

正規表現意味指定例マッチする結果
?直前の要素の0回か1回の出現を表すab?a, ab
*直前の要素の0回以上の出現を表すab*a, ab, abb, abbb, …
+直前の要素の1回以上の出現を表すab+ab, abb, abbb, …
{n}直前の要素のn回の出現を表すab{2}abb
{n,}直前の要素のn回以上の出現を表すab{2,}abb, abbb, …
{,m}直前の要素の0-m回の出現を表すab{,2}a, ab, abb
{n,m}直前の要素のn-m回の出現を表すab{2-4}abb, abbb, abbbb

グループ化

正規表現意味指定例マッチする結果
(〇〇〇)()内の要素をグループ化するa(bc)*a, abc, abcbc, …

正規表現(パターン)の指定方法

正規表現におけるパターンの指定方法には、そのまま文字列として指定する方法とコンパイルしてPatternオブジェクトとして指定する方法の2種類あります。パターンを文字列として指定した場合でも内部的にはコンパイルされて実行されるのですが、あらかじめ Patternオブジェクトとして保存しておけばそれを再利用できるので、何度も同じ処理を繰り返すような場合はあらかじめコンパイルしておいた方がよいと思います。

文字列として正規表現(パターン)を直接指定する

文字列型の文章(sentence)に対して文字列型で与えられた正規表現(pattern)で検索する場合は、

result1 = search(pattern, sentence)
result2 = match(pattern, sentence)
result3 = findall(pattern, sentence)

のように、それぞれ第1引数と第2引数に指定するだけでOKです。

何度も処理を繰り返す場合は遅くなってしまう可能性がありますが、関数の引数に指定するだけでよいという簡便さが特徴です。

正規表現(パターン)をPatternオブジェクトとして指定する

文字列型の文章(sentence)に対して、文字列型で与えられた正規表現(pattern)をコンパイルしてPattern型のオブジェクトとすることで、何度も検索する場合はそのオブジェクトを使いまわすことができます。パターンをコンパイルするためには、compile関数にパターンを指定しましょう。

# パターンをコンパイル
pattern_compiled = compile(pattern)

コンパイルして生成されたpattern_compiledを用いて検索する場合も、先ほどと同様で

# 検索を行う
result1 = search(pattern_compiled, sentence)
result2 = match(pattern_compiled, sentence)
result3 = findall(pattern_compiled, sentence)

のように、それぞれ第1引数と第2引数に指定するだけでOKです。

なお、Pattern型のオブジェクト(pattern_compiled)にも、search関数、match関数、findall関数などに対応するメソッドが用意されているので、それを用いて

# 検索を行う
result1 = pattern_compiled.search(sentence)
result2 = pattern_compiled.match(sentence)
result3 = pattern_compiled.findall(sentence)

とすることも可能です。どちらの書き方をするかは好みでよいと思います。

正規表現を用いて検索する

それでは実際に正規表現を用いて具体的な検索を行う例を見ていきましょう。なお、ここではコンパイルせずにそのまま正規表現のパターンを指定する方法をコード例としてお示ししていますが、パターンをコンパイルする方法でも同様です。

基本的な検索方法

正規表現を用いて検索するための関数としては以下のようなものがあります。

search関数finditer関数findall関数
検索対象最初にパターンに一致する部分パターンに一致する部分すべてパターンに一致する部分すべて
戻り値MatchオブジェクトMatchオブジェクトのイテレータ一致した文字列のリスト

例えば、次の文章から電話番号部分を抽出してみましょう。

私の携帯電話の番号は090-1234-5678で、自宅の番号は03(8765)4321です。

ここでは、電話番号部分のパターンとして「\d{1,4}.?\d{1,4}.?\d{1,4}」を指定しています。

import re

s = '私の携帯電話の番号は090-1234-5678で、自宅の番号は03(8765)4321です。'
pattern = r'\d{1,4}.?\d{1,4}.?\d{1,4}'
result1 = re.search(pattern, s)
result2 = re.finditer(pattern, s)
result3 = re.findall(pattern, s)

print('search関数の結果:' + str(result1))
print('finditer関数の結果:' + str(result2))
print('findall関数の結果:' + str(result3))
search関数の結果:<re.Match object; span=(10, 23), match='090-1234-5678'>
finditer関数の結果:<callable_iterator object at 0x00000257D52829C8>
findall関数の結果:['090-1234-5678', '03(8765)4321']

findall関数の結果ではリストで電話番号がすべて取得できていることが分かりますが、search関数とfinditer関数の結果はどう扱えばよいのでしょうか?search関数で得られた結果は文字列で最初にマッチした電話番号である「090-1234-5678」を表すMatchオブジェクトで、finditer関数で得られた結果はマッチしたすべての電話番号を表すMatchオブジェクトのイテレータです。ここから個々のMatchオブジェクトを取り出すにはforループなどを用いましょう。

単純に検索結果を取得するだけならリストで取得できるfindall関数で十分なのですが、取得した結果を用いてさらにそれに対して何らかの処理を行うような場合はfinditer関数を用いてMatchオブジェクトを取得するようにしましょう。例えばパターンでグループ化していればMatchオブジェクトでマッチした部分のグループごとの結果を抽出することも可能になります(Matchオブジェクトの使い方は次で説明します)。また、すべての検索結果は必要ない場合や巨大な文字列の検索でパフォーマンス的にすべてを一度に検索するのは難しいような場合は、search関数で前から1つずつ検索していくと良いかもしれません。

Matchオブジェクトの活用

まずは、finditer関数やsearch関数で検索して得られたMatchオブジェクトから、マッチした文字列を取得してみましょう。

Matchオブジェクトからマッチした文字列を抽出するときには、groupメソッドを用います。では先ほどの例を用いて、finditer関数で電話番号部分をすべて検索し、マッチした電話番号を順番に表示させてみましょう。

import re

s = '私の携帯電話の番号は090-1234-5678で、自宅の番号は03(8765)4321です。'
pattern = r'\d{1,4}.?\d{1,4}.?\d{1,4}'
results = re.finditer(pattern, s)

for result in results:
    print(result.group())
090-1234-5678
03(8765)4321

これで電話番号をすべて検索して、抽出することができました。しかし、「この電話番号をすべて数字で表したい」「「電話番号のうち、市外局番だけを取り出したい」というようなときはもうひと手間必要になってしまいますよね?実は正規表現を用いて検索すると、このようなリクエストにも一発で答えることができるのです。

正規表現で検索するときは、パターンを「( )」で囲ったひと固まりが1つのグループとして認識されます。Matchオブジェクトで結果を取得すれば、パターンで指定したグループごとに結果を得ることができます。例えば先ほどの例で、電話番号の数字部分ごとにグループを3つ設定してみましょう。得られたMatchオブジェクトのgroupsメソッドを用いると、設定したグループごとにタプルの要素として取得することができます。

import re

s = '私の携帯電話の番号は090-1234-5678で、自宅の番号は03(8765)4321です。'
pattern = r'(\d{1,4}).?(\d{1,4}).?(\d{1,4})'
results = re.finditer(pattern, s)

for result in results:
    print(result.groups())
('090', '1234', '5678')
('03', '8765', '4321')

このタプルを使えば、電話番号の数字部分だけを取得することや、市外局番だけを取得することも簡単に可能です。

この他にもMatchオブジェクトは、そのマッチした結果の元の文字列における位置など様々な情報を格納しています。

その他の検索関数

その他の検索関数としては次のようなものがあります。

  • match関数:与えられた文字列の先頭部分に対してパターンを検索し、Matchオブジェクトで取得します。
  • fullmatch関数:与えられた文字列全体がパターンに一致するかを検索し、一致すればMatchオブジェクトを返します(一致しなかった場合はNone)。

正規表現を用いて置換する

正規表現を用いた置換の基本

続いて、正規表現を用いて検索を行い、それを別の文字列で置換する例を見ていきましょう。正規表現を用いた置換にはsub関数を用います。例えば、電話番号などの個人情報が書かれている文字列から、電話番号を匿名化する処理は次のようになります。

import re

s = '私の携帯電話の番号は090-1234-5678で、自宅の番号は03(8765)4321です。'
pattern = r'(\d{1,4}).?(\d{1,4}).?(\d{1,4})'
result = re.sub(pattern, '***-****-****', s)

print(result)
私の携帯電話の番号は***-****-****で、自宅の番号は***-****-****です。

検索でマッチした文字列を用いて置換する文字列を指定する(後方参照)

先ほどの例では置換する文字列は一律に「***-****-****」としていましたが、検索でマッチした文字列によって置換する文字列を変える場合もあります。例えば「電話番号をすべて『(〇〇〇)〇〇〇〇-〇〇〇〇』の形式で置き換える」という場合は、sub関数の第2引数(repl引数)に指定する置き換える文字列は一律に同じものを指定することはできず、検索でマッチした文字列に応じて変える必要があります。

検索でマッチしたものを置換文字列にもう一度用いる方法を後方参照と呼びます。後方参照を用いることで、検索でマッチした文字列に応じて置換する文字列を変えることができます。後方参照を用いるときは、検索するパターン内で「()」を用いてグループ化を行い、そのグループが何番目のグループかを「\1」のようにして「\」と数字で指定します。なおこの時、正規表現におけるグループは1から始まることに注意が必要です。

import re

s = '私の携帯電話の番号は090-1234-5678で、自宅の番号は03(8765)4321です。'
pattern = r'(\d{1,4}).?(\d{1,4}).?(\d{1,4})'
result = re.sub(pattern, r'(\1)\2-\3', s)

print(result)
私の携帯電話の番号は(090)1234-5678で、自宅の番号は(03)8765-4321です。

このようにして検索でマッチした文字列の一部を置換でそのまま使えることが分かりました。

後方参照とは?
後方参照は「一度出てきたものをもう一度参照する」方法です。「後方参照」という名前自体が日本語的にピンとこない方も多いと思いますが、英語ではbackreferenceのことで、プログラムの進む向きとは反対に前に戻って参照することからその名前がついています。
英語なら何となくニュアンスが伝わってきますが、それを日本語で直訳して後方参照という用語にしてしまっているのでいまいちピンとこない用語となっているのです。

関連記事・スポンサーリンク

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)