第13章 正規表現

正規表現とは?

正規表現(regular expression、略してregexやregexp)は、文字列のパターンを表現するための強力な言語ツールです。これにより、文字列の検索、置換、検証などの操作を行うことができます。

Pythonでは、reという標準ライブラリを使用して正規表現の操作を行います。

正規表現の基本的な構成要素
  1. リテラル: 通常の文字として解釈される文字です。例:aや1。
  2. メタキャラクタ: 正規表現の動作を制御する特別な文字。例:.や*。
  3. エスケープ: メタキャラクタをリテラルとして解釈するために、\を使用します。例:\.は実際の.文字を意味します。


Pythonでの正規表現の基本的な使い方

re.search()関数: 文字列内で正規表現に一致する部分を検索します。

import re

result = re.search("abc", "abcdef")
if result:
    print("一致が見つかりました!")
else:
    print("一致するものが見つかりませんでした。")

re.match()関数: 文字列の先頭が正規表現に一致するかを確認します。

import re

result = re.match("abc", "abcdef")
if result:
    print("一致が見つかりました!")
else:
    print("一致するものが見つかりませんでした。")

re.findall()関数: 文字列内で正規表現に一致するすべての部分をリストとして返します。

import re

results = re.findall("a.", "abcacdadea")
print(results)  # ['ab', 'ac', 'ad', 'ae']


基本的なパターン

メタキャラクタは正規表現内で特別な意味を持つ文字のことを指します。

import re

# '.': 任意の1文字を表します。
print(re.search("c.t", "cat"))  # 

# '^': 文字列の開始を意味します。
print(re.search("^c", "cat"))  # 

# '$': 文字列の終了を意味します。
print(re.search("t$", "cat"))  # 

# '*': 直前の文字が0回以上繰り返す場合に一致します。
print(re.search("ca*t", "caaat"))  # 

# '+': 直前の文字が1回以上繰り返す場合に一致します。
print(re.search("ca+t", "caaat"))  # 

# '?': 直前の文字が0回または1回存在する場合に一致します。
print(re.search("ca?t", "cat"))  # 

[]を使用してキャラクタクラスを定義します。これにより、指定された範囲の文字のいずれか1文字と一致します。

import re

# '[abc]': a, b, cのいずれかの文字と一致します。
print(re.search("[abc]", "dog"))  # None
print(re.search("[abc]", "cat"))  # 

# '[^abc]': a, b, c以外の任意の文字と一致します。
print(re.search("[^abc]", "cat"))  # 

# '[a-z]': 小文字のアルファベット全体と一致します。
print(re.search("[a-z]", "DOG"))  # None

# '[A-Z]': 大文字のアルファベット全体と一致します。
print(re.search("[A-Z]", "Dog"))  # 

以下は特別なシーケンスの例です。

import re

# '\d': 任意の数字と一致します。[0-9]と等価です。
print(re.search("\d", "abc4def"))  # 

# '\D': 数字以外の任意の文字と一致します。[^0-9]と等価です。
print(re.search("\D", "1234a5678"))  # 

# '\s': 任意の空白文字(スペース、タブ、改行など)と一致します。
print(re.search("\s", "abc def"))  # 

# '\S': 空白文字以外の任意の文字と一致します。
print(re.search("\S", "  a  "))  # 

# '\w': 任意の英数字およびアンダースコアと一致します。
print(re.search("\w", "!@#abc*"))  # 

# '\W': \wの反対となる文字と一致します。
print(re.search("\W", "abc_def!ghi"))  # 


量指定子

量指定子は、直前の文字やグループがどれだけの回数繰り返すかを指定するためのメタ文字です。以下に、Pythonの正規表現で使用される主な量指定子を示します。

import re

# "a"が1回から3回繰り返す場合に一致します。
print(re.search("a{1,3}", "caaaaat"))  # 

# "a"が2回以上繰り返す場合に一致します。
print(re.search("a{2,}", "caaaaat"))  # 

# "a"が3回繰り返す場合に一致します。
print(re.search("a{3}", "caaaaat"))  # 

# "a"が0回または1回繰り返す場合に一致します。
print(re.search("ca?t", "ct"))  # 

# "a"が0回以上繰り返す場合に一致します。
print(re.search("ca*t", "caaaaat"))  # 

# "a"が1回以上繰り返す場合に一致します。
print(re.search("ca+t", "caaaaat"))  # 


位置指定

正規表現で位置を指定するメタ文字やシーケンスを使用すると、特定の位置にマッチするパターンを作成することができます。以下に、Pythonの正規表現で使用される主な位置指定のメタ文字やシーケンスについて詳しく説明します。

import re

# "cat"で始まる文字列に一致します。
print(re.search("^cat", "caterpillar"))  # 

# "pillar"で終わる文字列に一致します。
print(re.search("pillar$", "caterpillar"))  # 

# "cat"という単語全体に一致します。
print(re.search(r"\bcat\b", "I have a cat."))  # 

# "cat"の文字列が単語の境界以外の場所に存在する場合に一致します。
print(re.search(r"\Bcat\B", "concatenate"))  # 

# "cat"の直前に"wild"が存在する場合に一致します。
print(re.search(r"(?=cat)wild", "wildcat"))  # None, because it's looking for "wild" immediately before "cat", which is not the case here.

# "cat"の直前に"wild"が存在しない場合に一致します。
print(re.search(r"(?!cat)wild", "wildcat"))  # 


グルーピングとキャプチャ

正規表現の中で部分的なマッチを取り出したり、その部分に対して特定の操作を適用する場合、グルーピングとキャプチャが非常に便利です。以下に、Pythonの正規表現におけるグルーピングとキャプチャに関する詳細を説明します。

グルーピング

グルーピングは、正規表現内で括弧()を使用して行います。括弧内の部分表現は1つのグループとして扱われ、このグループ内でのマッチは後から個別に取り出すことができます。

import re

pattern = r"(\d{2})-(\d{2})-(\d{4})"
match = re.search(pattern, "生年月日: 25-12-1990")

if match:
    day = match.group(1)
    month = match.group(2)
    year = match.group(3)
    print(f"日: {day}, 月: {month}, 年: {year}")


キャプチャ

グルーピングによって作成されたグループは、マッチの結果から個別に取り出すことができます。この取り出しを「キャプチャ」と呼びます。上の例で見たように、group()メソッドを使用して特定のグループをキャプチャできます。


キャプチャの省略

時には、グルーピングを行いたくてもその結果をキャプチャしたくないことがあります。このような場合、?:を括弧の先頭に追加することでキャプチャを無効にできます。

import re

pattern = r"(?:\d{2})-(\d{2})-(\d{4})"
match = re.search(pattern, "生年月日: 25-12-1990")

if match:
    month = match.group(1)
    year = match.group(2)
    print(f"月: {month}, 年: {year}")


名前付きグループ

マッチの結果を取り出すときに、インデックス番号ではなく名前を使用して結果を取得することができます。これは、特に複雑な正規表現や多数のグループが存在する場合に役立ちます。

import re

pattern = r"(?P\d{2})-(?P\d{2})-(?P\d{4})"
match = re.search(pattern, "生年月日: 25-12-1990")

if match:
    day = match.group("day")
    month = match.group("month")
    year = match.group("year")
    print(f"日: {day}, 月: {month}, 年: {year}")


前方/後方の否定

正規表現における前方/後方の否定は、あるパターンの前や後に特定の文字やシーケンスが存在しないことを確認する際に使用されます。Pythonのreモジュールでは、前方/後方の否定のアサーションをサポートしています。

前方の否定 (Negative Lookbehind)

前方の否定は、現在の位置の前にあるテキストが指定したパターンに一致しないことを確認します。この否定の構文は(?<!pattern)で、patternは一致させたくないパターンです。

次の正規表現は、"ing" で終わる単語を検索しますが、"ing" の前に "str" が来てはならないという条件を持っています。

import re

text = "I am singing and stringing."
pattern = r"(?<!str)ing\b"

matches = re.findall(pattern, text)
print(matches)  # ['ing']

この例では、"singing" はマッチしますが、"stringing" はマッチしません。


後方の否定 (Negative Lookahead)

後方の否定は、現在の位置の後にあるテキストが指定したパターンに一致しないことを確認します。この否定の構文は(?![pattern])で、patternは一致させたくないパターンです。

次の正規表現は、"price" の後に "high" が続かないテキストを検索します。

import re

text = "The price is very high. What is the price?"
pattern = r"price(?![ ]high)"

matches = re.findall(pattern, text)
print(matches)  # ['price']

この例では、最後の "price" はマッチしますが、"price high" の部分はマッチしません。


フラグとオプション

正規表現を使う際には、振る舞いを変更するためのさまざまなフラグやオプションを使用できます。これによって、大文字小文字を無視したマッチングや、複数行モードなどの特定の動作を簡単に実現することができます。

re.IGNORECASE (または re.I)

大文字と小文字を区別せずにマッチングを行います。

import re

pattern = re.compile(r'python', re.IGNORECASE)
result = pattern.findall('I love Python and python!')
print(result)  # ['Python', 'python']


re.MULTILINE (または re.M)

文字列が複数行にわたる場合に、^$がそれぞれ各行の開始と終了にマッチするようになります。

import re

text = '''apple
banana
APPLE'''

pattern = re.compile(r'^apple', re.MULTILINE)
result = pattern.findall(text)
print(result)  # ['apple']


re.DOTALL (または re.S)

このフラグを使用すると、.(ドット)が改行文字\nにもマッチします。

import re

text = 'apple\nbanana'
pattern = re.compile(r'apple.banana', re.DOTALL)
result = pattern.search(text)
print(result.group())  # apple\nbanana


re.VERBOSE (または re.X)

このフラグを使うと、正規表現の中に空白やコメントを入れることができ、読みやすい正規表現を書くことができます。

import re

pattern = re.compile(r"""
\d{3}   # area code
-       # dash separator
\d{4}   # local number
""", re.VERBOSE)

result = pattern.search('123-4567')
print(result.group())  # 123-4567


re.ASCII (または re.A)

\w,\b,\s,\dなどのエスケープシーケンスがASCII文字のみにマッチするようになります。デフォルトでは、これらはユニコード文字にもマッチします。

これらのフラグは、コンパイル時や検索時に使うことができます。複数のフラグを組み合わせて使用することもできます。例えば、re.compile(pattern, re.IGNORECASE | re.MULTILINE)のようにビットORで連結して使用します。


応用的なテクニック

正規表現はテキストの検索や操作において非常に強力なツールであり、その機能性は基本的なものから非常に高度なものまで様々です。ここでは、Pythonでの正規表現の応用的なテクニックについて説明します。

ルックアヘッドとルックビハインド

正のルックアヘッド:先読みとも呼ばれ、あるパターンの後に続く別のパターンを検出しますが、そのパターン自体は結果には含まれません。

import re

text = "100USD, 200EUR, 300JPY"
pattern = r"\d+(?=USD)"
matches = re.findall(pattern, text)
print(matches)  # ['100']

負のルックアヘッド:あるパターンの後に続かない別のパターンを検出します。

import re

pattern = r"\d+(?!USD)"
matches = re.findall(pattern, text)
print(matches)  # ['200', '300']

正のルックビハインド:後ろ読みとも呼ばれ、あるパターンの前に存在する別のパターンを検出します。

import re

pattern = r"(?<=EUR)\d+"
matches = re.findall(pattern, text)
print(matches)  # []

負のルックビハインド:あるパターンの前に存在しない別のパターンを検出します。

import re

pattern = r"(?<!EUR)\d+"
matches = re.findall(pattern, text)
print(matches)  # ['100', '200', '300']


条件付き正規表現

特定の条件下でのみマッチするパターンを作成できます。

import re

pattern = r"(?(?=USD)\d{3}|\d{2})"
# USDの後には3桁の数字、そうでなければ2桁の数字にマッチ
matches = re.findall(pattern, text)
print(matches)  # ['100', '20', '30']


名前付きキャプチャ

グルーピングは便利ですが、インデックス番号でアクセスするのではなく、名前でアクセスするとさらにわかりやすくなります。

import re

text = "John: 28, Emily: 22"
pattern = r"(?P\w+): (?P\d+)"
matches = re.finditer(pattern, text)

for match in matches:
    print(match.group('name'), match.group('age'))


フラグを使用した複数行のマッチ

re.MULTILINEre.DOTALLなどのフラグを使用すると、複数行のテキストにまたがるマッチを行うことができます。

import re

text = """
This is a sample text.
Another line here.
"""
pattern = r"^Another"
matches = re.findall(pattern, text, re.MULTILINE)
print(matches)  # ['Another']


実践例

正規表現は非常に強力で、さまざまなテキスト処理のタスクにおいて役立ちます。以下に、Pythonのreモジュールを使用した実践的な正規表現の例をいくつか紹介します。

Eメールアドレスの検出

Eメールアドレスをテキストから検出するための簡単な正規表現です。

import re

text = "Please contact support@example.com for assistance."
pattern = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,3}"
match = re.search(pattern, text)

if match:
    print("Eメールアドレスが見つかりました:", match.group())
else:
    print("Eメールアドレスは見つかりませんでした。")


URLの検出

URLを検出する正規表現の一例です。

import re

text = "Visit our website at https://www.example.com!"
pattern = r"https?://\S+"
match = re.search(pattern, text)

if match:
    print("URLが見つかりました:", match.group())
else:
    print("URLは見つかりませんでした。")


日付の検出

以下は、YYYY-MM-DD形式の日付を検出する正規表現の例です。

import re

text = "The event is scheduled for 2023-07-10."
pattern = r"\d{4}-\d{2}-\d{2}"
match = re.search(pattern, text)

if match:
    print("日付が見つかりました:", match.group())
else:
    print("日付は見つかりませんでした。")


HTMLタグの除去

HTMLタグを除去してプレーンテキストのみを取得するための正規表現です。

import re

text = "

This is a heading

This is a paragraph.

" clean_text = re.sub(r"<.*?>", "", text) print(clean_text)


電話番号の検出

以下は、様々なフォーマットの電話番号を検出する正規表現の例です。

import re

text = "You can reach me at 123-456-7890 or (123) 456-7890."
pattern = r"\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}"
matches = re.findall(pattern, text)

for match in matches:
    print("電話番号:", match)


最適化とパフォーマンス

正規表現はテキスト処理に非常に強力ですが、複雑なパターンや大量のテキストデータを扱う際にはパフォーマンスの問題が発生する可能性があります。以下は、Pythonの正規表現を効果的に利用するための最適化とパフォーマンスに関するヒントです。

コンパイル済み正規表現

re.compile()を使用することで、同じ正規表現を繰り返し使用する場合の実行時間を短縮できます。この関数は、正規表現のパターンを事前にコンパイルして、後で再利用できる正規表現オブジェクトを返します。

import re

pattern = re.compile(r'\d+')
result = pattern.findall('123 abc 456 def')
print(result)  # ['123', '456']


非キャプチャグループ

キャプチャ不要な場合は、非キャプチャグループを使用してメモリ使用量を削減することができます。

import re

# 通常のグルーピング
pattern1 = r"(\d+)-(\d+)"
# 非キャプチャグルーピング
pattern2 = r"(?:\d+)-(?:\d+)"


貪欲マッチングと非貪欲マッチング

デフォルトで、量指定子は「貪欲」です。可能な限り最長のマッチを試みます。非貪欲マッチ(最短マッチ)を行いたい場合は、量指定子の後に?を追加します。

import re

text = '<div>content1</div><div>content2</div>'
# 貪欲マッチ
pattern_greedy = r'<div>.*</div>'
print(re.search(pattern_greedy, text).group())  # <div>content1</div><div>content2</div>

# 非貪欲マッチ
pattern_lazy = r'<div>.*?</div>'
print(re.search(pattern_lazy, text).group())  # <div>content1</div>


固定文字列の検索

正規表現の機能が不要な場合(単なる部分文字列の検索など)、inキーワードやstr.find()str.startswith()などの組み込み関数を使用した方が高速に動作します。


パターンの最適化

[^...]よりも[...]を使用する方が高速です。可能であれば、^(行の先頭)や$(行の終端)を利用して、検索範囲を制限します。


パフォーマンスの計測

実際のコードでパフォーマンス問題が発生しているかどうかを確認するために、timeitモジュールを使用して実行時間を計測します。

import timeit

print(timeit.timeit('re.search(pattern, text)', setup='import re; pattern="..."; text="..."', number=10000))


注意点とトラブルシューティング

正規表現は非常に強力なツールですが、使い方によっては予期しない結果やパフォーマンスの問題が発生することもあります。以下は、Pythonでの正規表現使用時の注意点とトラブルシューティングについての概要です。

バックスラッシュ \ の使用

Pythonの文字列リテラル内でバックスラッシュを使用する際は、エスケープが必要です。しかし、正規表現にもバックスラッシュを使用することが多いため、これが混同を招く原因となります。


貪欲マッチ

正規表現の量指定子はデフォルトで貪欲に動作します。これが原因で意図しない長いマッチを取得することがあります。


カタストロフィックバックトラッキング

複雑な正規表現や矛盾したパターンを使用すると、非常に遅い実行時間となることがあります。


マルチラインモード

^$はデフォルトで文字列の先頭や末尾にマッチしますが、マルチラインのテキストを処理する際には、各行の先頭や末尾にマッチさせたい場合があります。


ユニコードとの互換性

\w\dなどのショートカットシーケンスは、ASCII文字だけでなくユニコード文字にもマッチすることがあります。


マッチしない場合のデバッグ

正規表現が期待通りにマッチしない場合、どこに問題があるのかを特定するのが難しいことがあります。


練習問題1.

以下のテキストから日本の電話番号を抜き出してください。日本の電話番号は、市外局番が3桁-4桁または2桁-4桁、市内局番が4桁、加えて、伸ばし棒がある場合も考慮してください。

テキスト:

私の電話番号は012-3456-7890です。事務所の電話番号は98-7654-4321です。ただし、01234567890や987654321は電話番号ではありません。また、012-3456-7890-1234も電話番号の形式とは異なります。


練習問題2.

以下のテキストから正しい形式のメールアドレスだけを抜き出してください。

テキスト:

私のメールアドレスはsample@example.comです。ただし、sample@.comやsample@example..comは無効なアドレスです。


練習問題3.

以下のテキストからhttpまたはhttpsを使用したURLを抜き出してください。

テキスト:

私のウェブサイトはhttp://www.example.comです。セキュアなページはhttps://secure.example.comにあります。ただし、www.example.comやftp://files.example.comはURLとして抜き出さないでください。