Pythonで正規表現(regular expression)を抽出する際に使用するreモジュールの基本的な使い方について解説します。
Contents
正規表現(Regular Expression)
正規表現(Regular Expression)は、あいまいな文字列パターンを表現するための表現方法です。Pythonでは、正規表現を扱うためにreモジュールが用意されており、文字列に含まれる正規表現パターンを抽出することができます。
任意にパターンに一致するものを探す方法としてワイルドカード(「*」(複数文字)や「?」(任意1文字)等)の表現を使って、「*.txt」のようにWindowsエクスプローラーでファイル検索する人もいるかと思います。正規表現はワイルドカードよりも、さらに複雑なパターンを表現することが可能です。
例えば、電話番号であれば、「\d{2,4}-\d{2,4}-\d{4}」のように正規表現であらわせます。\dが10進数数値を表しており、{2,4}は直前の文字が2~4回出現するパターンにマッチします。{4}は直前の文字が4回出現するパターンということを意味します。この例で直前文字と言っているのは\dの10進数数値の事です。
正規表現は、色々な業務システムで利用されます。テキストマイニング等の自然言語処理システムでも必要となってくる技術かと思いますので基本を理解しておくとよいでしょう。
正規表現は、それだけで本があるぐらい奥が深いので細かな説明は割愛します。興味がある方は、関連書籍や関連Webページを検索して調べていただければと思います。また、自分が記載した正規表現で対象文字列を抽出できるかを確認できるサイトも結構あり、「正規表現チェッカー」などで検索すれば出てきますのでうまく活用してみてください。
この記事では、reモジュールの基本的な使い方を説明します。
reモジュール
正規表現をPythonのreモジュールで抽出する方法について例を用いて紹介していきます。
正規表現パターン抽出の基本
正規表現モジュールのreの基本的な使い方は以下の手順になります。
- compile関数で正規表現パターンを作成する。
- 正規表現パターンに文字列を指定して判定。判定にはsearchメソッドやmatchメソッドを使用する。
searchメソッドを使用するか、matchメソッドを使用するかで少し挙動が違います。簡単に記載すると以下の通りです。
- search:任意位置で一致する場合を抽出したい
- match:先頭から一致する場合を抽出したい
以下で例を見ながらsearch、matchそれぞれの使用方法を紹介します。
searchメソッドを用いた正規表現の抽出
searchメソッドを用いた正規表現の抽出の例を紹介します。
import re text1 = "電話番号は、090-1111-2222です。" text2 = "電話番号は、0123-45-6789です。" # 正規表現をcompileで準備する # \を使うのでrのraw文字列として引数に設定する ptrn = re.compile(r"(\d{2,4})-(\d{2,4})-(\d{4})") # 文字列を検索し、結果を表示する result1 = ptrn.search(text1) print(type(result1)) if result1: # 引数なしはヒットした全体を表示。result.group(0)でも同じ print(result1.group()) # 1番目の部分文字列 print(result1.group(1)) # 2番目の部分文字列 print(result1.group(2)) # 3番目の部分文字列 print(result1.group(3)) else: print("一致なし") print("=====") # Python 3.8以降であればセイウチ演算子を使ってもよい if result2 := ptrn.search(text2): print(result2.group()) print(result2.group(1)) print(result2.group(2)) print(result2.group(3)) else: print("一致なし")
【実行結果】 <class 're.Match'> 090-1111-2222 090 1111 2222 ===== 0123-45-6789 0123 45 6789
以降でポイントを順に説明していきます。
まず、正規表現の抽出を行うためにはreモジュールのインポートする必要があります。次に、compile関数で抽出したい正規表現パターンを以下のように設定します。
# 正規表現をcompileで準備する # \を使うのでrのraw文字列として引数に設定する ptrn = re.compile(r"(\d{2,4})-(\d{2,4})-(\d{4})")
正規表現パターンは性質上「\」を多く使います。文字列を使うときのエスケープシーケンスとぶつかってしまうことを避けるため、raw文字列(r’~’)を使用するとよいでしょう。
result1 = ptrn.search(text1)
次に、用意したパターンのserchメソッドに抽出対象としたい文字列を渡します。抽出した文字列を表示したい場合には、groupメソッドを使用して抽出文字列を取得することができます。
if result1: # 引数なしはヒットした全体を表示。result.group(0)でも同じ print(result1.group()) # 1番目の部分文字列 print(result1.group(1)) # 2番目の部分文字列 print(result1.group(2)) # 3番目の部分文字列 print(result1.group(3)) else: print("一致なし")
print(type(result1))で、searchメソッドの返却値の型を表示していますが、「re.Match」クラスのオブジェクトになっています。もし、検索結果で正規表現に一致するものがなかった場合はNoneが返ってきますので、抽出結果を確認する前にif文で判定をしています。
re.Matchのgroupメソッドを使うことで抽出された文字列を参照できます。引数がなし又は0の場合は、正規表現に一致した文字列全体を取得できます。
引数に1以上を指定すると、正規表現中のグループであるサブマッチ文字列を取得できます。ここでサブマッチ文字列というのは、正規表現のcompileの際に()でくくった単位の事です。今回の例では「r'(\d{2,4})-(\d{2,4})-(\d{4})’」と()の単位が3つあるのでグループを1, 2, 3という引数で各グループに該当した文字列を取得できます。
なお、if文の部分ですが、Python3.8以降を使用している場合であれば、セイウチ演算子用いて以下のようにまとめてしまうのがシンプルです。text2の抽出の方はセイウチ演算子を使った例で記載しています。
if result2 := ptrn.search(text2):
その他にもstartやend、spanといったメソッドで抽出されたグループごとの開始、終了位置を取得するといったこともできます。reの公式ドキュメントのこちらを参照してください。
次は、searchに似たmatchメソッドについて紹介します。
matchメソッドを使用した正規表現の抽出
searchによく似たメソッドとして、matchメソッドがあります。
このmatchメソッドの特徴は、「文字列の先頭からのみをマッチ対象とする」という点です。以下の例で見てみましょう。
import re text1 = "090-1111-2222が電話番号です。" text2 = "電話番号は、0123-45-6789です。" ptrn = re.compile(r"(\d{2,4})-(\d{2,4})-(\d{4})") result1 = ptrn.match(text1) if result1: print(result1.group()) print(result1.group(1)) print(result1.group(2)) print(result1.group(3)) else: print("一致なし") print("=====") if result2 := ptrn.match(text2): print(result2.group()) print(result2.group(1)) print(result2.group(2)) print(result2.group(3)) else: print("一致なし")
【実行結果】 090-1111-2222 090 1111 2222 ===== 一致なし
内容は上記のsearchと同様の例をmatchに置き換えた例です。text1は電話番号から文字列が始まっており、text2は文字列の途中に電話番号が出てきています。
searchメソッドであればいずれも抽出できますが、matchメソッドだと文字列中に電話番号が出現するtext2は抽出することができません。
先頭から抽出したい場合は、matchを使用すると覚えておきましょう。
正規表現に一致する全ての文字列を取得する
search/matchメソッドは、どちらも最初にマッチした文字列を一つ返すだけです。しかし、多くの場合は対象文字列に含まれる正規表現に一致する文字列を全て取得したいケースが多いかと思います。
対象の文字列から正規表現に一致する文字列を全て取得したい場合は、findallメソッドまたはfinditerメソッドを使用します。どちらも正規表現に一致する文字列をすべて取得する点で同じですが、以下のような違いがあります。
- findall:正規表現に一致する文字列をリストで取得
- finditer:正規表現に一致する文字列をre.Matchオブジェクトで取得
以下で例を見ながらfindall、finditerそれぞれの使用方法を紹介します。
findallメソッドを使用する方法
findallメソッドを使用した正規表現の取得方法について、以下例を用いて見ていきましょう。
import re text = ( "私の電話は0123-45-6789で、携帯は090-1111-2222です。" "Aさんの電話は9876-54-3210で、携帯は080-3333-4444です。" ) # パターンを設定(グループ設定) ptrn1 = re.compile(r"(\d{2,4})-(\d{2,4})-(\d{4})") # マッチする文字列を全て検索 match_texts = ptrn1.findall(text) print(type(match_texts)) for match_text in match_texts: print(match_text) print("=====") # パターンを設定(グループなし) ptrn2 = re.compile(r"\d{2,4}-\d{2,4}-\d{4}") # マッチする文字列を全て検索 match_texts = ptrn2.findall(text) print(type(match_texts)) for match_text in match_texts: print(match_text)
【実行結果】 <class 'list'> ('0123', '45', '6789') ('090', '1111', '2222') ('9876', '54', '3210') ('080', '3333', '4444') ===== <class 'list'> 0123-45-6789 090-1111-2222 9876-54-3210 080-3333-4444
正規表現のパターンをcompile関数で準備するところは、search/matchメソッド使用時と同じです。
作成したパターンでfindallメソッドを以下のように実行することで、正規表現に一致するものをリスト(list)形式で取得することができます。
match_texts = ptrn1.findall(text)
findallメソッドは、正規表現パターンでグループを表す()を使用した場合には、グループ毎に抽出した文字列をタプル(tuple)にまとめた形で返却します。
グループを表す()がない場合は、正規表現に一致する文字列全体がリストとして取得できます。
finditerメソッドを使用する方法
上記で見てきたfindallメソッドで取得されるのはリスト(list)形式です。
searchメソッドやmatchメソッドでは、re.Matchのオブジェクトが返却されたためgroupメソッドによりサブマッチ文字列を取得できました。finditerメソッドを用いると正規表現に一致する全ての文字列をre.Matchオブジェクトで取得することができます。
正確にはre.Matchオブジェクトのイテレータを返すので、for文で順に処理することができます。
finditerメソッドを使用した正規表現の取得方法について、以下例を用いて見ていきましょう。
import re text = ( "私の電話は0123-45-6789で、携帯は090-1111-2222です。" "Aさんの電話は9876-54-3210で、携帯は080-3333-4444です。" ) # パターンを設定 ptrn1 = re.compile(r"(\d{2,4})-(\d{2,4})-(\d{4})") # マッチする文字列を全て検索 match_texts = ptrn1.finditer(text) print(type(match_texts)) for match_text in match_texts: print("=====") print(match_text.group()) print(match_text.group(1)) print(match_text.group(2)) print(match_text.group(3))
【実行結果】 <class 'callable_iterator'> ===== 0123-45-6789 0123 45 6789 ===== 090-1111-2222 090 1111 2222 ===== 9876-54-3210 9876 54 3210 ===== 080-3333-4444 080 3333 4444
以下の部分のようにfindallと使用方法はほとんど同じです。
match_texts = ptrn1.finditer(text)
finditerの返却値はtype関数で確認すると「<class ‘callable_iterator’>」となっており、イテレータとなっていることが分かります。そのため以下のようにfor文等で順番に取り出すことで処理をすることが可能です。
for match_text in match_texts: print('=====') print(match_text.group()) print(match_text.group(1)) print(match_text.group(2)) print(match_text.group(3))
forでイテレータから取り出したものはre.Matchオブジェクトのため、groupメソッドを使うことで、正規表現に一致する文字列全体やサブマッチ文字列を取得して利用することができます。
文字列を分割・置換する
正規表現に一致した部分を基準に文字列を分割するにはsplitメソッドを、正規表現に一致した文字列を置換するにはsubメソッドが使用できます。
splitメソッドで正規表現で文字列を分割する
正規表現で一致した部分を基準に文字列を分割する場合は、以下のようにsplitメソッドを使用します。
import re text = ( "私の電話は0123-45-6789で、携帯は090-1111-2222です。" "Aさんの電話は9876-54-3210で、携帯は080-3333-4444です。" ) # パターンを設定 ptrn1 = re.compile(r"\d{2,4}-\d{2,4}-\d{4}") # 正規表現で一致した位置を基準に文字列を分割する split_result = ptrn1.split(text) print(split_result)
【実行結果】 ['私の電話は', 'で、携帯は', 'です。Aさんの電話は', 'で、携帯は', 'です。']
この例は、携帯電話に一致する正規表現を用いて文字列を分割している例です。
まずはreの一般的な使い方の通り、compile関数にて正規表現を定義します。文字列を分割する際にはsplitメソッドに対象となる文字列全体を渡すだけです。分割された文字列はリスト(list)で取得できます。
splitメソッドの公式ドキュメントの記載はこちらを参照してください。なお、正規表現ではなく区切り文字で文字列を分割する場合は、strのsplitメソッドを使用するようにしましょう。
subメソッドで正規表現に一致した文字列を置換する
正規表現に一致した文字列を置換したい場合は、以下のようにsubメソッドを使用します。
import re text = ( "私の電話は0123-45-6789で、携帯は090-1111-2222です。" "Aさんの電話は9876-54-3210で、携帯は080-3333-4444です。" ) # パターンを設定 ptrn1 = re.compile(r"\d{2,4}-\d{2,4}-\d{4}") # 一致する文字列を置換する replaced_text1 = ptrn1.sub("XXX-XXXX-XXXX", text) print(replaced_text1) print("=====") text = ( "私の電話は0123-45-6789で、携帯は090-1111-2222です。" "Aさんの電話は9876-54-3210で、携帯は080-3333-4444です。" ) # 置換する数を指定する replaced_text2 = ptrn1.sub("XXX-XXXX-XXXX", text, 2) print(replaced_text2)
【実行結果】 私の電話はXXX-XXXX-XXXXで、携帯はXXX-XXXX-XXXXです。Aさんの電話はXXX-XXXX-XXXXで、携帯はXXX-XXXX-XXXXです。 ===== 私の電話はXXX-XXXX-XXXXで、携帯はXXX-XXXX-XXXXです。Aさんの電話は9876-54-3210で、携帯は080-3333-4444です。
この例は、電話番号を’XXX-XXX-XXXX’ という文字列に置換している例です。
まずはreの一般的な使い方の通り、compile関数にて正規表現を定義します。正規表現に一致した文字列を置換するには、subメソッドに置換する文字と対象の文字列全体を渡すことで、正規表現に一致した部分を置換することができます。
また、3つ目の引数の数字(replaced_text2を作成している上記例では2) は置き換えの最大個数を指定できます。例では2となっているので最初の2つは置換がされていますが、それ以降で正規表現に一致する文字列はそのままになっていることが分かります。
subメソッドの公式ドキュメントの記載はこちらを参照してください。
オプションで正規表現抽出の挙動を制御する
正規表現モジュールreでは、各種動作の制御に用いるオプションがあります。以降では、IGNORECASE, MULTILINE, DOTALL, VERBOSEについて使い方を紹介します。
大文字/小文字を区別しない(IGNORECASE)
正規表現で大文字/小文字を区別せずに抽出したい場合は、re.IGNORECASEをオプションとして指定します。
import re text = "メールアドレスは、user_01@test.comとプライベート用のUSER_02@test.co.jpを使用しています。" ptrn = re.compile( r"([a-z0-9_.+-]+)@([a-z0-9][a-z0-9-]*[a-z0-9]*\.)+[a-z]{2,}", re.IGNORECASE, ) results = ptrn.finditer(text) for result in results: print(result.group())
【実行結果】 user_01@test.com USER_02@test.co.jp
この例では、メールアドレスを正規表現で抽出しています。
大文字/小文字に関わらずにメールアドレスの抽出ができていることが分かります。なお、re.IGNORECASEの指定をなくした場合には、二つ目の「USER_02@test.co.jp」については抽出されなくなります。
上記の例であれば、例えば@前までの部分を「[a-zA-Z0-9_.+-]+」というように大文字のパターンも追加することでre.IGNORECASEの指定なしでも抽出することができます。しかし、記述が長くなってしまいますし、記載ミスをしてしまう場合もあります。そのため、オプションをうまく使って対処するのがよいかと思います。
複数行モードを有効にする(MULTILINE)
複数行の文字列が\nで改行されているような文字列の場合に、先頭「^」と文字列の末尾「$」の抽出を各行それぞれで行いたい場合は、re.MULTILINEをオプションとして使用します。複数行モードと言ったり、マルチラインモードを言ったりします。
import re text = "0123-45-6789はAさんの電話番号\n090-1111-2222はBさんの電話番号" # マルチラインモードではない場合 ptrn1 = re.compile(r"^(\d{2,4})-(\d{2,4})-(\d{4})") results1 = ptrn1.finditer(text) for result1 in results1: print(result1.group()) print("=====") # マルチラインモードを使用する場合 ptrn2 = re.compile(r"^(\d{2,4})-(\d{2,4})-(\d{4})", re.MULTILINE) results2 = ptrn2.finditer(text) for result2 in results2: print(result2.group())
【実行結果】 0123-45-6789 ===== 0123-45-6789 090-1111-2222
上記例では分かりやすくするために、re.MULTILINEを指定しない場合の例も示しています。
re.MULTILINEを指定しない場合は、1行目は正規表現に一致して抽出されるのですが、改行された2行目では正規表現を抽出できていません。一方で、re.MULTILINEを指定すると2行目についても抽出できていることが分かります。
今回の例では、文字列の先頭である「^」の例となっていますが、文字列の末尾を表す「$」についても同様に挙動の制御を行うことができます。
単一行モードを有効にする(DOTALL)
正規表現では「.」は、改行「\n」を除く任意の文字にマッチする表現です。改行も含めてすべての文字にマッチするようにしたい場合は、re.DOTALLをオプションとして指定します。単一行モードと言ったり、シングルラインモードと言ったりします。
import re text = "単一行モードを使用すると\n改行コードを含めて抽出できる" # 単一行モードを有効にしない場合 ptrn1 = re.compile(r"^.+") if result1 := ptrn1.search(text): print(result1.group()) print("=====") # 単一行モードを有効にした場合 ptrn2 = re.compile(r"^.+", re.DOTALL) if result2 := ptrn2.search(text): print(result2.group())
【実行結果】 単一行モードを使用すると ===== 単一行モードを使用すると 改行コードを含めて抽出できる
上記例では分かりやすくするために、re.DOTALLを指定しない場合の例も示しています。
re.DOTALLを指定しない場合は、改行の手前までは抽出されるのですが、改行された2行目からは抽出できていません。一方で、re.DOTALLを指定すると改行を含んで2行目についても抽出できていることが分かります。
空白/コメントを付与して正規表現を見やすくする(VERBOSE)
正規表現の表現は、最初見た人にはなかなか難しいものです。私も正規表現については、常に使用するわけではないので使うときにはいつも調べながら使用します。
re.VERBOSEオプションを使うと、空白/コメントを正規表現文字列の中に加えることで正規表現に関する説明を付与するといったことができます。
import re text = "私のメールアドレスは、user_01@test.comです。" ptrn = re.compile( r""" ([a-z0-9_.+-]+) #local @ #delimiter ([a-z0-9][a-z0-9-]*[a-z0-9]*\.)+[a-z]{2,} #domain """, re.VERBOSE, ) if result := ptrn.search(text): print(result.group())
【実行結果】 user_01@test.com
上記の例のように、re.VERBOSEを有効にすると、正規表現内の空白や改行は無視されます。また、「#」でコメントを加えることができるようになります。
正規表現の各部分の説明などをコードに記載しておくと後で内容を確認しやすかったり、他の人に内容を伝えやすくなったりと、可読性を向上させることが可能にります。
複数のオプション値を同時に使用する
上記でいくつかの正規表現のオプションを見てきました。複数のオプションを同時に適用したい場合には、または(OR)を意味する「|」を使用して以下のように連結してあげることで適用することができます。
import re text = "user_01@test.com: 会社メールアドレス\nUSER_02@test.co.jp: プライベート用メールアドレス" ptrn = re.compile( r"^([a-z0-9_.+-]+)@([a-z0-9][a-z0-9-]*[a-z0-9]*\.)+[a-z]{2,}", re.IGNORECASE | re.MULTILINE, ) results = ptrn.finditer(text) for result in results: print(result.group())
【実行結果】 user_01@test.com USER_02@test.co.jp
以上のように、オプションもうまく使うと正規表現の扱いが容易になります。
まとめ
Pythonで正規表現(regular expression)を抽出する際に使用するreモジュールの基本的な使い方について解説しました。
compileしてserchやmatchで抽出する基本的な方法から、findall/finditerによる正規表現に一致する全ての文字列を取得する方法、split/subを用いた分割・置換の方法、各種オプション(IGNORECASE, MULTILINE, DOTALL, VERBOSE)の使い方について例を使って紹介してきました。
正規表現は、色々な業務システムで利用されます。テキストマイニング等の自然言語処理システムでも必要となってくる技術かと思います。是非、基本的な使い方を理解しておいてもらえるとよいかと思います。
reモジュールの公式ドキュメントの記載はこちらを参照してください。
上記で紹介しているソースコードについてはgithubにて公開しています。参考にしていただければと思います。