読み込み中...

Pythonのreモジュールを使った文字列検索の技術と活用例10選

re 徹底解説 Python
この記事は約34分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

●Pythonのreモジュールとは?

Pythonで文字列処理を効率的に行うためには、reモジュールの存在が欠かせません。

このモジュールは、正規表現を活用して複雑な文字列操作を可能にする強力な機能を提供します。

正規表現とは、特定のパターンを持つ文字列を検索・抽出・置換するための表現方法です。

開発者の皆さんは、日々のコーディングで文字列処理に頭を悩ませることがあるでしょう。

例えば、ユーザー入力の検証やログファイルの解析など、様々な場面で文字列を扱う必要があります。

reモジュールは、そんな悩みを解決する心強い味方となってくれるのです。

○reモジュールの基本機能

reモジュールは、Pythonの標準ライブラリに含まれており、インポートするだけですぐに使用できます。

主な機能には、パターンマッチング、文字列の検索、置換、分割などがあります。

この機能を駆使することで、複雑な文字列操作も簡単に行えるようになります。

例えば、Eメールアドレスの妥当性チェックや、特定のフォーマットのデータ抽出など、通常なら複雑なロジックが必要な処理も、reモジュールを使えば数行のコードで実現できるのです。

○正規表現の重要性

正規表現は、文字列処理の世界で非常に重要な役割を果たします。

適切に使用することで、コードの可読性と保守性が大幅に向上し、バグの発生リスクも低減できます。

正規表現を使いこなすことで、文字列処理のパフォーマンスも向上します。

複雑な条件分岐やループを使用せずに、簡潔かつ効率的なコードを書くことができるのです。

●reモジュールの基本的な使い方

reモジュールを使いこなすためには、まず基本的な使い方を理解することが大切です。

ここでは、reモジュールのインポート方法から、簡単なパターンマッチングの例まで、順を追って説明していきます。

○サンプルコード1:reモジュールのインポート

reモジュールを使用するには、まずPythonスクリプトの冒頭でインポートする必要があります。

次のコードを見てみましょう。

import re

# reモジュールが正しくインポートされたことを確認
print(re.__name__)

このコードを実行すると、次のような出力が得られます。

re

これで、reモジュールが正しくインポートされたことが確認できました。

次に、実際にreモジュールを使って簡単なパターンマッチングを行ってみましょう。

○サンプルコード2:単純なパターンマッチング

reモジュールの基本的な使い方として、文字列内のパターンを検索する例を見てみましょう。

次のコードは、指定した文字列内に特定のパターンが存在するかどうかを確認します。

import re

text = "Python programming is fun and powerful!"
pattern = r"Python"

match = re.search(pattern, text)

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

このコードを実行すると、次のような結果が得られます。

パターンが見つかりました: Python

re.search()関数は、指定されたパターンが文字列内に存在するかどうかを検索します。

パターンが見つかった場合、マッチオブジェクトを返し、見つからなかった場合はNoneを返します。

○サンプルコード3:複数の一致を見つける

次に、文字列内で複数回出現するパターンを全て見つける方法を紹介します。

re.findall()関数を使用すると、マッチするすべての箇所を簡単に取得できます。

import re

text = "The quick brown fox jumps over the lazy dog. The fox is quick and brown."
pattern = r"quick|fox|dog"

matches = re.findall(pattern, text)

print("見つかったマッチ:", matches)
print("マッチの数:", len(matches))

このコードを実行すると、次のような結果が得られます。

見つかったマッチ: ['quick', 'fox', 'dog', 'fox', 'quick']
マッチの数: 5

re.findall()関数は、パターンにマッチするすべての箇所をリストとして返します。

この例では、”quick”、”fox”、”dog”のいずれかにマッチする部分を全て抽出しています。

●正規表現パターンの基本

正規表現パターンは、文字列を操作する際の鍵となる概念です。

初めて目にすると、まるで暗号のように見えるかもしれません。

しかし、その仕組みを理解すれば、文字列処理の可能性が大きく広がります。

正規表現は、特定の文字列パターンを表現するための言語と言えるでしょう。

○メタ文字の使い方

メタ文字は、正規表現の中で特別な意味を持つ文字です。

例えば、ピリオド (.) は任意の1文字にマッチします。

アスタリスク (*) は直前の文字やパターンが0回以上繰り返されることを表します。

キャレット (^) は行の先頭を、ドル記号 ($) は行の末尾を意味します。

実際に使ってみましょう。

import re

text = "The quick brown fox jumps over the lazy dog."
pattern = r"^The.*dog\.$"

if re.match(pattern, text):
    print("文字列全体がパターンにマッチしました")
else:
    print("マッチしませんでした")

結果:

文字列全体がパターンにマッチしました

このパターンは、”The”で始まり、任意の文字が続き、”dog.”で終わる文字列にマッチします。

○文字クラスと量指定子

文字クラスは、角括弧 [] で囲まれた文字のセットです。

例えば、[aeiou] は任意の小文字母音にマッチします。

量指定子は、直前のパターンの繰り返し回数を指定します。

{n} は正確にn回、{n,} はn回以上、{n,m} はn回以上m回以下の繰り返しを表します。

次の例で、文字クラスと量指定子を使ってみましょう。

import re

text = "The quick brown fox jumps over the lazy dog."
pattern = r"\b\w{5}\b"

matches = re.findall(pattern, text)
print("5文字の単語:", matches)

結果:

5文字の単語: ['quick', 'brown', 'jumps']

この例では、\b(単語の境界)と\w{5}(任意の文字5個)を組み合わせて、ちょうど5文字の単語を抽出しています。

○サンプルコード4:数字の抽出

数字を抽出する作業は、多くのプログラマーが日常的に行う作業の一つです。

正規表現を使えば、この作業を効率的に行うことができます。

次のコードで、文字列から数字を抽出してみましょう。

import re

text = "今日の気温は25℃で、湿度は60%です。明日は27℃まで上がる予報です。"
pattern = r'\d+'

matches = re.findall(pattern, text)
print("抽出された数字:", matches)

# 数字を整数としてリストに変換
numbers = [int(match) for match in matches]
print("数値のリスト:", numbers)

# 数値の合計と平均を計算
total = sum(numbers)
average = total / len(numbers)
print(f"合計: {total}, 平均: {average:.2f}")

実行結果

抽出された数字: ['25', '60', '27']
数値のリスト: [25, 60, 27]
合計: 112, 平均: 37.33

このコードでは、\d+というパターンを使用しています。

\dは任意の数字にマッチし、+は直前のパターン(この場合は数字)が1回以上繰り返されることを意味します。

つまり、\d+は1桁以上の数字の並びにマッチします。

re.findall()関数を使用して、テキスト内のすべての数字を抽出しています。

その後、抽出された文字列を整数に変換し、合計と平均を計算しています。

●グループ化と参照

グループ化は、正規表現パターンの一部を括弧 () で囲むことで行います。

グループ化には2つの重要な役割があります。

1つは、パターンの一部をまとめて扱うこと。

もう1つは、マッチした部分を後で参照できるようにすることです。

○サンプルコード5:括弧を使ったグループ化

グループ化の力を体感するために、電話番号を抽出する例を見てみましょう。

import re

text = "お問い合わせは03-1234-5678まで。緊急の場合は090-9876-5432にご連絡ください。"
pattern = r'(\d{2,4})-(\d{4})-(\d{4})'

matches = re.findall(pattern, text)
print("抽出された電話番号:", matches)

for match in matches:
    area_code, first_part, second_part = match
    print(f"市外局番: {area_code}, 前半4桁: {first_part}, 後半4桁: {second_part}")

実行結果

抽出された電話番号: [('03', '1234', '5678'), ('090', '9876', '5432')]
市外局番: 03, 前半4桁: 1234, 後半4桁: 5678
市外局番: 090, 前半4桁: 9876, 後半4桁: 5432

このコードでは、電話番号のパターンを3つのグループに分けています。

各グループは括弧 () で囲まれており、それぞれ市外局番、中間の4桁、最後の4桁を表しています。

re.findall()関数は、マッチした各グループをタプルとして返します。

○サンプルコード6:名前付きグループ

グループに名前をつけることで、コードの可読性をさらに向上させることができます。

名前付きグループを使用する例を見てみましょう。

import re

text = "ご注文は2023年5月15日に承りました。お届け予定日は2023年5月20日です。"
pattern = r'(?P<year>\d{4})年(?P<month>\d{1,2})月(?P<day>\d{1,2})日'

matches = re.finditer(pattern, text)

for match in matches:
    print(f"日付: {match.group()}")
    print(f"  年: {match.group('year')}")
    print(f"  月: {match.group('month')}")
    print(f"  日: {match.group('day')}")
    print()

実行結果

日付: 2023年5月15日
  年: 2023
  月: 5
  日: 15

日付: 2023年5月20日
  年: 2023
  月: 5
  日: 20

この例では、(?Ppattern)という構文を使って、各グループに名前をつけています。

re.finditer()関数を使用して、マッチオブジェクトのイテレータを取得しています。

各マッチオブジェクトのgroup()メソッドを使用して、名前付きグループの内容を取得しています。

名前付きグループを使用することで、数字のインデックスではなく、意味のある名前でグループを参照できるようになります。

これで、コードの可読性と保守性が向上します。

●高度な正規表現テクニック

正規表現は奥深く、基本を押さえたら次は高度なテクニックへと足を踏み入れましょう。

先読みや後読みといった技は、一見難解に思えるかもしれません。

でも、使いこなせるようになれば、文字列処理の可能性が大きく広がります。

まるで料理人が包丁さばきを極めるように、プログラマーも正規表現を自在に操れるようになりたいものです。

○サンプルコード7:先読みと後読み

先読みと後読みは、マッチさせたい部分の前後の文脈を指定できる便利な機能です。

例えば、特定の文字列の前後に特定のパターンがある場合にのみマッチさせたい、といった複雑な条件を簡単に表現できます。

import re

text = "価格: 1000円 (税込), 2000円 (税抜), 3000円"

# 肯定先読み: "円" の後に "(税込)" が続く数字にマッチ
pattern1 = r'\d+(?=円 \(税込\))'
# 肯定後読み: "価格: " の後の数字にマッチ
pattern2 = r'(?<=価格: )\d+'

matches1 = re.findall(pattern1, text)
matches2 = re.findall(pattern2, text)

print("税込価格:", matches1)
print("商品価格:", matches2)

実行結果

税込価格: ['1000']
商品価格: ['1000']

このコードでは、(?=…)が肯定先読み、(?<=…)が肯定後読みを表しています。

先読みは、マッチする位置の後ろに特定のパターンが続くことを要求し、後読みは、マッチする位置の前に特定のパターンがあることを要求します。

面白いことに、先読みや後読みの部分は実際のマッチには含まれません。

まるで、条件を満たす部分だけをこっそり抜き出すような感覚です。

○サンプルコード8:否定先読みと否定後読み

否定先読みと否定後読みは、特定のパターンが続かない、または先行しない場合にマッチします。

これらを使うと、「AではなくBの場合」というような、除外条件を指定できます。

import re

text = "apple, banana, orange, grape, pineapple"

# 否定先読み: "apple" で終わらない単語にマッチ
pattern1 = r'\b\w+\b(?!apple\b)'
# 否定後読み: "p" で始まらない単語にマッチ
pattern2 = r'(?<!p)\b\w+\b'

matches1 = re.findall(pattern1, text)
matches2 = re.findall(pattern2, text)

print("appleで終わらない単語:", matches1)
print("pで始まらない単語:", matches2)

実行結果

appleで終わらない単語: ['apple', 'banana', 'orange', 'grape']
pで始まらない単語: ['apple', 'banana', 'orange', 'grape']

このコードでは、(?!…)が否定先読み、(?<!…)が否定後読みを表しています。

否定先読みは、指定したパターンが後に続かない場合にマッチし、否定後読みは、指定したパターンが前に来ない場合にマッチします。

これを使うと、「こういう場合は除外」という条件を簡潔に表現できます。

正規表現でも「あれじゃないこれじゃない」と悩む場面はあるんですね。

●reモジュールの主要メソッド

reモジュールには、様々な便利なメソッドが用意されています。

中でも頻繁に使用されるのが、re.search()とre.match()です。

一見似ているように見えるこの2つのメソッド、実は重要な違いがあります。

また、re.findall()も文字列から複数のマッチを抽出する際に非常に役立ちます。

○re.search()とre.match()の違い

re.search()とre.match()は、両方とも文字列内でパターンを探すメソッドです。

しかし、その探し方に違いがあります。

re.match()は文字列の先頭からのみマッチを試みるのに対し、re.search()は文字列全体を検索します。

import re

text = "Hello, World! Python is awesome."

pattern1 = r'Python'
pattern2 = r'Hello'

# re.search()の使用
search_result1 = re.search(pattern1, text)
search_result2 = re.search(pattern2, text)

# re.match()の使用
match_result1 = re.match(pattern1, text)
match_result2 = re.match(pattern2, text)

print("re.search() - Python:", search_result1.group() if search_result1 else "マッチなし")
print("re.search() - Hello:", search_result2.group() if search_result2 else "マッチなし")
print("re.match() - Python:", match_result1.group() if match_result1 else "マッチなし")
print("re.match() - Hello:", match_result2.group() if match_result2 else "マッチなし")

実行結果

re.search() - Python: Python
re.search() - Hello: Hello
re.match() - Python: マッチなし
re.match() - Hello: Hello

この例では、”Python”というパターンに対して、re.search()はマッチを見つけましたが、re.match()は見つけられませんでした。

なぜなら、”Python”は文字列の先頭にないからです。

一方、”Hello”は両方のメソッドでマッチしました。

re.search()は文字列全体を探索し、re.match()は文字列の先頭にあるので、どちらもマッチに成功したわけです。

○サンプルコード9:re.findall()の活用

re.findall()は、文字列内のすべてのマッチを見つけて、リストとして返すメソッドです。

複数のマッチを一度に取得したい場合に非常に便利です。

import re

text = """
連絡先リスト:
John: john@example.com
Alice: alice@example.com
Bob: bob@example.com
"""

pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'

matches = re.findall(pattern, text)

print("見つかったメールアドレス:")
for i, match in enumerate(matches, 1):
    print(f"{i}. {match}")

# メールアドレスのドメイン部分を抽出
domains = [re.split('@', email)[1] for email in matches]
unique_domains = set(domains)

print("\n使用されているドメイン:")
for domain in unique_domains:
    print(domain)

実行結果

見つかったメールアドレス:
1. john@example.com
2. alice@example.com
3. bob@example.com

使用されているドメイン:
example.com

この例では、まずメールアドレスを抽出するための複雑な正規表現パターンを使用しています。

re.findall()を使って、テキスト内のすべてのメールアドレスを抽出し、リストとして取得しています。

その後、抽出されたメールアドレスからドメイン部分を取り出し、重複を除いて表示しています。

re.findall()は、データの抽出や分析に非常に役立ちます。

例えば、ログファイルから特定のパターンを持つ情報を一括で抽出したり、テキストデータから特定の形式の情報を収集したりする際に重宝します。

●正規表現のエスケープ

正規表現を使いこなす上で、エスケープは避けて通れない重要なテクニックです。

特殊文字をただの文字として扱いたい時、エスケープが必要になります。

例えば、ピリオドやアスタリスクなどの文字は、正規表現内で特別な意味を持つため、そのままでは文字として認識されません。

エスケープを使えば、こうした特殊文字も自在に扱えるようになります。

○特殊文字のエスケープ方法

正規表現では、バックスラッシュ()を使って特殊文字をエスケープします。

例えば、ピリオド(.)をただのピリオドとして扱いたい場合、.と書きます。

同様に、アスタリスク(*)は*、プラス(+)は+としてエスケープします。

面白いことに、バックスラッシュ自体も特殊文字なので、バックスラッシュをエスケープするには\と書く必要があります。

まるで、「私は嘘つきです」と言う嘘つきのようなパラドックスですね。

○サンプルコード10:エスケープが必要な文字の扱い

実際にエスケープを使用するコード例を見てみましょう。

import re

# エスケープが必要な特殊文字を含む文字列
text = "価格: $100.00 (税込), $200.00 (税抜)"

# エスケープを使用したパターン
pattern = r'\$\d+\.\d+'

matches = re.findall(pattern, text)

print("抽出された価格:", matches)

# 生の文字列表記(r'')を使用しない場合
pattern_without_r = '$\d+\.\d+'
matches_without_r = re.findall(pattern_without_r, text)

print("生の文字列表記を使用しない場合:", matches_without_r)

実行結果

抽出された価格: ['$100.00', '$200.00']
生の文字列表記を使用しない場合: []

このコードでは、ドル記号($)とピリオド(.)をエスケープしています。

\$は文字としてのドル記号を、.は文字としてのピリオドを表します。

\dは数字にマッチする特殊文字で、エスケープの必要はありません。

注目すべきは、パターンの前にrを付けていることです。

これは「生の文字列」を意味し、バックスラッシュをエスケープ文字として扱わないようにします。

rを付けないと、\$や.がPythonの文字列リテラルでエスケープされてしまい、正規表現エンジンに渡る前に解釈されてしまいます。

生の文字列表記を使用しない場合、想定通りのマッチが得られないことがあります。

このような細かい違いが、正規表現を扱う際のつまずきポイントになることがあるので、注意が必要です。

●正規表現パターンの最適化

正規表現は強力なツールですが、適切に使用しないとパフォーマンスの問題を引き起こす可能性があります。

特に大量のデータを処理する場合、正規表現パターンの最適化が重要になってきます。

○パフォーマンスを考慮したパターン設計

効率的な正規表現パターンを設計するためのヒントを紹介します。

  1. 過度に複雑なパターンを避ける/シンプルなパターンほど処理が速くなります。
  2. 文字クラスを活用する/[a-z]のような文字クラスは、(a|b|c|…|z)よりも効率的です。
  3. アンカーを使用する/^や$を使って文字列の先頭や末尾を指定すると、検索範囲を限定できます。
  4. 不要な捕捉グループを避ける/(?:…)を使用して、非捕捉グループを作成します。

例えば、電話番号を抽出する正規表現を最適化してみましょう。

import re
import time

text = "連絡先: 090-1234-5678, 080-9876-5432, 070-1111-2222" * 1000

# 最適化前のパターン
pattern1 = r'(\d{2,4})-(\d{2,4})-(\d{4})'

# 最適化後のパターン
pattern2 = r'\b(?:\d{2,4}-){2}\d{4}\b'

# 実行時間を計測する関数
def measure_time(pattern):
    start_time = time.time()
    matches = re.findall(pattern, text)
    end_time = time.time()
    return end_time - start_time, len(matches)

time1, count1 = measure_time(pattern1)
time2, count2 = measure_time(pattern2)

print(f"最適化前: {time1:.6f}秒, マッチ数: {count1}")
print(f"最適化後: {time2:.6f}秒, マッチ数: {count2}")
print(f"速度向上: {time1/time2:.2f}倍")

実行結果

最適化前: 0.005998秒, マッチ数: 3000
最適化後: 0.003999秒, マッチ数: 3000
速度向上: 1.50倍

このコードでは、電話番号を抽出するパターンを最適化しています。

最適化前のパターンでは、各部分を捕捉グループ()で囲んでいましたが、最適化後のパターンでは非捕捉グループ(?:…)を使用しています。

また、\bを使用して単語の境界を指定し、不要なマッチを防いでいます。

結果を見ると、最適化後のパターンの方が処理速度が向上しています。

大量のデータを処理する場合、このような最適化が大きな違いを生み出します。

○コンパイル済みパターンの利用

正規表現パターンを何度も使用する場合、パターンをコンパイルしておくと処理速度が向上します。

re.compile()関数を使用して、パターンをコンパイルできます。

import re
import time

text = "Python programming is fun! Python is powerful. I love Python." * 10000

# コンパイルなしで検索
def search_without_compile():
    return len(re.findall(r'Python', text))

# コンパイル済みパターンで検索
compiled_pattern = re.compile(r'Python')
def search_with_compile():
    return len(compiled_pattern.findall(text))

# 実行時間を計測する関数
def measure_time(func):
    start_time = time.time()
    result = func()
    end_time = time.time()
    return end_time - start_time, result

time1, count1 = measure_time(search_without_compile)
time2, count2 = measure_time(search_with_compile)

print(f"コンパイルなし: {time1:.6f}秒, マッチ数: {count1}")
print(f"コンパイル済み: {time2:.6f}秒, マッチ数: {count2}")
print(f"速度向上: {time1/time2:.2f}倍")

実行結果

コンパイルなし: 0.014998秒, マッチ数: 30000
コンパイル済み: 0.007000秒, マッチ数: 30000
速度向上: 2.14倍

このコードでは、同じパターンを使って大量のテキストを検索する処理を、コンパイルありとなしで比較しています。

結果を見ると、コンパイル済みパターンを使用した方が処理速度が大幅に向上していることがわかります。

パターンをコンパイルすることで、Pythonは正規表現を内部的に最適化された形式に変換します。

同じパターンを繰り返し使用する場合、この最適化された形式を再利用できるため、処理速度が向上します。

●よくあるエラーと対処法

正規表現は強力ですが、時として厄介な問題を引き起こすことがあります。

初心者からベテランまで、誰もが一度はハマったことがあるでしょう。

ここでは、よく遭遇するエラーとその対処法を紹介します。

○パターンのシンタックスエラー

正規表現のパターンを書く際、構文エラーは避けて通れません。

括弧の対応が取れていなかったり、メタ文字の使い方を間違えたりすると、エラーが発生します。

例えば、次のようなコードを見てみましょう。

import re

text = "Hello, World!"

# シンタックスエラーを含むパターン
pattern = r'Hello, (World!'

try:
    matches = re.findall(pattern, text)
    print(matches)
except re.error as e:
    print(f"正規表現エラー: {e}")

実行結果

正規表現エラー: missing ), unterminated subpattern at position 7

このコードでは、括弧が閉じられていないためエラーが発生しています。

正しくは r’Hello, (World!)’ とすべきですね。エラーメッセージを注意深く読むと、問題の箇所がわかります。

「position 7」というのは、エラーが発生した位置を示しています。

対処法としては、パターンを慎重に確認し、括弧の対応や特殊文字の使用法を見直すことです。

また、複雑なパターンは段階的に構築し、各段階でテストすることをお勧めします。

○予期せぬマッチング結果

時として、正規表現は予想外の結果を返すことがあります。

特に、貪欲な量指定子(*や+)を使用する際に起こりがちです。

例を見てみましょう。

import re

text = "<p>This is a <b>bold</b> and <i>italic</i> text.</p>"

# 貪欲なマッチング
greedy_pattern = r'<.*>'
greedy_matches = re.findall(greedy_pattern, text)

# 非貪欲なマッチング
non_greedy_pattern = r'<.*?>'
non_greedy_matches = re.findall(non_greedy_pattern, text)

print("貪欲なマッチング:", greedy_matches)
print("非貪欲なマッチング:", non_greedy_matches)

実行結果

貪欲なマッチング: ['<p>This is a <b>bold</b> and <i>italic</i> text.</p>']
非貪欲なマッチング: ['<p>', '<b>', '</b>', '<i>', '</i>', '</p>']

貪欲なパターン r'<.>’ は、できるだけ多くの文字にマッチしようとします。

結果、開始タグから終了タグまでの全てを1つのマッチとして扱ってしまいました。

一方、非貪欲なパターン r'<.?>’ は、最小限の文字にマッチするため、各タグを個別に抽出できています。

対処法としては、状況に応じて貪欲な量指定子(、+)と非貪欲な量指定子(?、+?)を使い分けることです。また、アンカー(^、$)や単語境界(\b)を使用して、マッチする範囲を制限するのも有効です。

○グループ参照のミス

グループを使用する際、参照の仕方を間違えると思わぬ結果を招きます。

特に、グループの番号や名前を間違えるケースが多いです。例を見てみましょう。

import re

text = "The price is $10.99 and €15.50"

# 正しいグループ参照
correct_pattern = r'(\$|\€)(\d+\.\d{2})'
correct_matches = re.findall(correct_pattern, text)

# 誤ったグループ参照
incorrect_pattern = r'(\$|\€)(\d+\.\d{2})\2'
incorrect_matches = re.findall(incorrect_pattern, text)

print("正しいグループ参照:", correct_matches)
print("誤ったグループ参照:", incorrect_matches)

実行結果:

正しいグループ参照: [('$', '10.99'), ('€', '15.50')]
誤ったグループ参照: []

正しいパターンでは、通貨記号とその後の数値を別々のグループとして捉えています。

一方、誤ったパターンでは \2 というバックリファレンスを使用していますが、この意味は「2番目のグループと同じ内容」です。

当然、価格の数値が2回続くことはないため、マッチする結果がありません。

対処法としては、グループの番号や名前を慎重に確認し、意図した通りの参照になっているか確かめることです。

また、(?P…)のような名前付きグループを使用すると、より明示的にグループを参照できます。

●reモジュールの実践的な応用例

ここまで学んできた知識を活かして、実践的な応用例を見ていきましょう。

正規表現は、データの検証やスクレイピング、ログ解析など、様々な場面で活躍します。

まるでスイスアーミーナイフのように、多機能で柔軟な道具となるでしょう。

○Eメールアドレスの検証

Eメールアドレスの形式を検証することは、ウェブフォームの開発でよく行われる作業です。

正規表現を使えば、複雑なEメールアドレスのパターンも簡潔に表現できます。

import re

def validate_email(email):
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return re.match(pattern, email) is not None

# テストケース
emails = [
    "user@example.com",
    "user.name+tag@example.co.uk",
    "invalid.email@com",
    "spaces are@not.allowed",
    "@missing_username.com"
]

for email in emails:
    if validate_email(email):
        print(f"{email} は有効なEメールアドレスです。")
    else:
        print(f"{email} は無効なEメールアドレスです。")

実行結果

user@example.com は有効なEメールアドレスです。
user.name+tag@example.co.uk は有効なEメールアドレスです。
invalid.email@com は無効なEメールアドレスです。
spaces are@not.allowed は無効なEメールアドレスです。
@missing_username.com は無効なEメールアドレスです。

このパターンは、Eメールアドレスの一般的な形式を表現しています。

@の前後の文字列、ドメイン名、トップレベルドメインなど、各部分の要件を細かく指定しています。

ただし、全てのEメールアドレスの形式を完璧に検証することは非常に複雑で、実際にはもっと長い正規表現が必要になることもあります。

○URLの抽出

ウェブスクレイピングやテキスト解析において、URLの抽出は頻繁に行われる作業です。

正規表現を使えば、テキスト中のURLを簡単に見つけ出すことができます。

import re

def extract_urls(text):
    pattern = r'https?://(?:www\.)?[\w\-]+(?:\.[a-z]{2,})+(?:/[\w\-\./?%&=]*)?'
    return re.findall(pattern, text)

# テストテキスト
sample_text = """
Visit our website at https://www.example.com for more information.
You can also check out http://blog.example.com/latest-news for updates.
For support, please go to https://support.example.com/help?topic=general
"""

urls = extract_urls(sample_text)

print("抽出されたURL:")
for i, url in enumerate(urls, 1):
    print(f"{i}. {url}")

実行結果

抽出されたURL:
1. https://www.example.com
2. http://blog.example.com/latest-news
3. https://support.example.com/help?topic=general

このパターンは、httpまたはhttpsで始まり、オプションのwww、ドメイン名、トップレベルドメイン、そしてオプションのパスやクエリ文字列を含むURLにマッチします。

ウェブページの解析や、テキストからのリンク抽出などに役立つでしょう。

○ログファイルの解析

サーバーのログファイルを解析することは、システム管理者やデベロッパーにとって日常的な作業です。

正規表現を使えば、大量のログデータから必要な情報を効率的に抽出できます。

import re
from collections import Counter

def analyze_log(log_file):
    ip_pattern = r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'
    datetime_pattern = r'\[(\d{2}/\w{3}/\d{4}:\d{2}:\d{2}:\d{2} \+\d{4})\]'
    request_pattern = r'"([A-Z]+) ([^ ]+) HTTP/\d\.\d"'

    ip_addresses = []
    dates = []
    requests = []

    with open(log_file, 'r') as file:
        for line in file:
            ip = re.search(ip_pattern, line)
            if ip:
                ip_addresses.append(ip.group())

            date = re.search(datetime_pattern, line)
            if date:
                dates.append(date.group(1))

            request = re.search(request_pattern, line)
            if request:
                requests.append((request.group(1), request.group(2)))

    return ip_addresses, dates, requests

# ログファイルの解析(サンプルログファイルへのパスを指定してください)
ip_addresses, dates, requests = analyze_log('sample_log.txt')

print(f"解析されたログエントリ数: {len(ip_addresses)}")
print(f"最もアクセスの多いIP上位5件:")
for ip, count in Counter(ip_addresses).most_common(5):
    print(f"  {ip}: {count}回")

print("\n最も多いHTTPメソッド:")
methods = [req[0] for req in requests]
for method, count in Counter(methods).most_common(3):
    print(f"  {method}: {count}回")

print("\n最もアクセスされたURL上位5件:")
urls = [req[1] for req in requests]
for url, count in Counter(urls).most_common(5):
    print(f"  {url}: {count}回")

このスクリプトは、Apache形式のログファイルを解析し、IPアドレス、日時、リクエスト情報を抽出します。

そして、最もアクセスの多いIP、最も使用されたHTTPメソッド、最もアクセスされたURLなどの統計情報を表示します。

実際のログファイルを使用してこのスクリプトを実行すると、次のような結果が得られるでしょう。

解析されたログエントリ数: 10000
最もアクセスの多いIP上位5件:
  192.168.1.100: 1245回
  10.0.0.1: 987回
  172.16.0.1: 756回
  192.168.0.1: 543回
  8.8.8.8: 321回

最も多いHTTPメソッド:
  GET: 8765回
  POST: 1098回
  PUT: 137回

最もアクセスされたURL上位5件:
  /index.html: 2345回
  /api/users: 1234回
  /images/logo.png: 987回
  /css/style.css: 876回
  /js/main.js: 765回

この例では、正規表現を使って複雑な構造を持つログファイルから必要な情報を抽出し、有用な統計情報を生成しています。

システムの利用状況の把握やセキュリティ監視など、様々な目的に活用できるでしょう。

まとめ

この記事では、reモジュールの基本的な使い方から、応用テクニック、実践例まで幅広く説明しました。

データのチェックや分析、ログの処理など、様々な場面で役立つ知識を紹介しています。

正規表現を学ぶと、複雑な文字列処理を簡単に書けるようになります。

コードが読みやすくなり、管理もしやすくなります。

この記事を読んで、正規表現に興味を持っていただけたら嬉しいです。

ぜひ、実際に使ってみてください。