Python正規表現とは?基本と使い方を紹介

Python正規表現(reモジュール)完全ガイド:基本から実践的な使い方まで詳細解説

はじめに:正規表現とは何か?なぜPythonで使うのか?

現代のプログラミングにおいて、文字列の操作は非常に重要な要素です。テキストデータの中から特定のパターンを持つ部分を探したり、抜き出したり、あるいは別の文字列に置換したりといった処理は頻繁に発生します。例えば、ログファイルから特定のエラーメッセージを探す、ユーザーが入力したメールアドレスの形式が正しいか検証する、WebサイトのHTMLから特定の情報を抽出するといった場面です。

このような文字列処理を効率的かつ柔軟に行うための強力なツールが「正規表現(Regular Expression)」です。正規表現を使うことで、複雑なパターンを短い文字列で表現し、文字列の検索、置換、分割などを高度に行うことができます。

Pythonには、標準ライブラリとして正規表現を扱うための re モジュールが用意されています。この re モジュールを使うことで、Pythonプログラムの中で正規表現の強力な機能を簡単に利用できます。Pythonの re モジュールは、多くの一般的な正規表現エンジン(Perl互換の正規表現など)と似た構文を持っており、非常に柔軟で高機能です。

この記事では、Pythonの re モジュールを使った正規表現の基本から応用的な使い方までを、豊富なコード例とともに詳細に解説します。約5000語というボリュームで、正規表現の概念からPythonでの具体的な実装方法、主要な関数やクラス、Matchオブジェクトの活用方法、さらには実用的な応用例までを網羅します。この記事を読めば、Pythonで自信を持って正規表現を扱えるようになるでしょう。

1. 正規表現の基本概念:パターンとマッチング

正規表現を使う際の最も基本的な考え方は、「パターン」を定義し、そのパターンが対象の「文字列」の中に「マッチ」するかどうかを調べるというものです。

  • パターン (Pattern): 検索したい文字列の特徴を記述したものです。アルファベットや数字といったリテラル文字だけでなく、後述する特殊な意味を持つ「メタ文字」を組み合わせて表現します。
  • 文字列 (String): パターンを適用して検索や置換を行う対象のテキストデータです。
  • マッチング (Matching): パターンが文字列中のどこかに存在するかどうかを判定する処理です。マッチが見つかった場合、その位置やマッチした部分の情報が得られます。

例えば、「cat」という文字列を探したい場合、パターンは単純に "cat" となります。しかし、「数字が3つ連続している部分」を探したい場合は、メタ文字を使ってパターンを定義する必要があります。

Pythonの re モジュールでは、定義したパターンを使って、文字列全体がパターンに一致するか、文字列のどこかにパターンが含まれるか、といった様々なマッチング操作を行うことができます。

2. 正規表現の基本的なメタ文字(特殊文字)

正規表現の強力さは、これらの「メタ文字」にあると言えます。メタ文字は、文字そのものではなく、特定の意味や機能を持っています。ここでは、最も基本的でよく使われるメタ文字を紹介します。

メタ文字 意味 説明
. 任意の一文字 (改行文字 \n を除く) a.c “abc”, “aac”, “axc” などにマッチ。改行を含む文字列で . が改行にマッチしない場合が多いが、フラグで変更可能。
^ 文字列または行の開始位置 ^abc 文字列の先頭が “abc” である場合にマッチ。複数行モード (re.M) では各行の先頭にマッチ。
$ 文字列または行の終了位置 abc$ 文字列の末尾が “abc” である場合にマッチ。複数行モード (re.M) では各行の末尾にマッチ。
* 直前の要素の0回以上の繰り返し a* “”, “a”, “aa”, “aaa”… にマッチ。
+ 直前の要素の1回以上の繰り返し a+ “a”, “aa”, “aaa”… にマッチ(””にはマッチしない)。
? 直前の要素の0回または1回の繰り返し (省略可能) a? “”, “a” にマッチ。
{m} 直前の要素のm回の繰り返し a{3} “aaa” にのみマッチ。
{m,} 直前の要素のm回以上の繰り返し a{3,} “aaa”, “aaaa”… にマッチ。
{m,n} 直前の要素のm回からn回までの繰り返し a{2,4} “aa”, “aaa”, “aaaa” にマッチ。
[] 文字クラス (カッコ内のいずれか一文字) [abc] “a”, “b”, “c” のいずれか一文字にマッチ。
| または (選択肢) cat|dog “cat” または “dog” にマッチ。
\ エスケープ (メタ文字をリテラル文字として扱う) \. ピリオド. そのものにマッチ。

これらのメタ文字を組み合わせることで、より複雑なパターンを表現できます。

コード例:

“`python
import re

. の例

print(re.search(r”a.c”, “abc”))
print(re.search(r”a.c”, “axc”))
print(re.search(r”a.c”, “ayyc”)) # マッチしない

^ と $ の例

print(re.search(r”^abc”, “abcdef”))
print(re.search(r”^abc”, “xabc”)) # マッチしない
print(re.search(r”def$”, “abcdef”))
print(re.search(r”def$”, “abcdefg”)) # マッチしない

* の例

print(re.search(r”ab”, “b”)) # 0回の繰り返しにもマッチ
print(re.search(r”a
b”, “ab”))
print(re.search(r”a*b”, “aaab”))

+ の例

print(re.search(r”a+b”, “b”)) # 0回の繰り返しにはマッチしない
print(re.search(r”a+b”, “ab”))
print(re.search(r”a+b”, “aaab”))

? の例

print(re.search(r”ab?c”, “ac”)) # bが0回
print(re.search(r”ab?c”, “abc”)) # bが1回
print(re.search(r”ab?c”, “abbc”)) # マッチしない

{m,n} の例

print(re.search(r”a{2,4}b”, “aab”))
print(re.search(r”a{2,4}b”, “aaab”))
print(re.search(r”a{2,4}b”, “aaaab”))
print(re.search(r”a{2,4}b”, “ab”)) # 2回未満なのでマッチしない
print(re.search(r”a{2,4}b”, “aaaaab”)) # 4回超過なのでマッチしない

[] の例

print(re.search(r”[aeiou]”, “hello”)) # oにマッチ
print(re.search(r”[0-9]+”, “price is 12345”)) # 12345にマッチ

| の例

print(re.search(r”cat|dog”, “I have a cat”))
print(re.search(r”cat|dog”, “I have a dog”))
print(re.search(r”cat|dog”, “I have a bird”)) # マッチしない

\ の例 (ピリオドそのものにマッチ)

print(re.search(r”a.c”, “a.c”))
print(re.search(r”a.c”, “abc”)) # マッチしない
“`

r"" (Raw String) について:

Pythonの文字列リテラルでは、バックスラッシュ (\) はエスケープ文字として特別な意味を持ちます (\n は改行、\t はタブなど)。正規表現でもバックスラッシュはメタ文字として使われるため (\d, \s など)、Pythonの文字列リテラルと正規表現の両方でエスケープが必要になり、非常に分かりにくくなる場合があります(例: 正規表現でバックスラッシュそのものにマッチさせたい場合、Python文字列では "\\"、正規表現では \\ となるため、合わせて "\\\\" と書く必要が出てきます)。

これを避けるために、正規表現パターンを記述する際には、文字列の前に r を付けた Raw String (生文字列) を使うことが強く推奨されます。Raw String では、バックスラッシュはエスケープ文字として解釈されず、文字通りのバックスラッシュとして扱われます。これにより、正規表現パターンをそのまま記述できます。

“`python

Raw String を使わない場合

print(re.search(“\\”, “a\b”)) # バックスラッシュそのものにマッチさせたい場合

Raw String を使う場合

print(re.search(r”\”, “a\b”)) # バックスラッシュそのものにマッチさせたい場合
“`

以降の例では、特に断りがない限り、正規表現パターンには Raw String を使用します。

3. 文字クラスの詳細

文字クラス [] は、特定の文字の集合のうち、いずれか一文字にマッチさせたい場合に非常に便利です。

  • [abc]: ‘a’, ‘b’, ‘c’ のいずれか一文字にマッチします。
  • [a-z]: アルファベット小文字 ‘a’ から ‘z’ までのいずれか一文字にマッチします。範囲指定は数字 ([0-9]) や大文字 ([A-Z]) にも使えます。[a-zA-Z0-9] とすれば、英数字のいずれか一文字にマッチします。
  • [^abc]: キャレット ^ をカッコ内の先頭に置くと、「否定」の意味になります。つまり、「’a’, ‘b’, ‘c’ 以外のいずれか一文字」にマッチします。[^0-9] は数字以外の文字にマッチします。

文字クラス内での特殊文字:

文字クラス [] の内側では、ほとんどのメタ文字は特殊な意味を失い、リテラル文字として扱われます。ただし、以下の文字は例外的に特殊な意味を持つことがあります。

  • -: 範囲指定に使われます (a-z)。リテラルとして扱う場合は、エスケープするか、カッコの先頭または末尾に置きます ([-az] または [az-])。
  • ^: カッコの先頭に置くと否定の意味になります ([^abc])。それ以外の場所に置くとリテラルとして扱われます ([a^bc])。
  • ]: 文字クラスの終了を表します。リテラルとして扱う場合はエスケープが必要です ([]])。
  • \: エスケープ文字です ([\-\]\\])。

よく使われる特殊シーケンス(ショートハンド文字クラス)

正規表現には、特定のよく使われる文字クラスを簡単に記述するためのショートハンドが用意されています。これらもバックスラッシュから始まります。

ショートハンド 意味 同等の文字クラス 説明
\d 数字 [0-9] 任意の数字 (digit)。
\D 数字以外 [^0-9] 数字以外の文字。
\s 空白文字 [ \t\n\r\f\v] スペース、タブ、改行、キャリッジリターン、フォームフィード、垂直タブ。
\S 空白文字以外 [^ \t\n\r\f\v] 空白文字以外の文字。
\w 単語構成文字 [a-zA-Z0-9_] アルファベット、数字、アンダースコア (word character)。
\W 単語構成文字以外 [^a-zA-Z0-9_] 単語構成文字以外の文字。
\b 単語境界 (アサーション) 単語文字と非単語文字の間、または文字列の先頭/末尾と単語文字の間。文字自体にはマッチしない。
\B 非単語境界 (アサーション) 単語境界ではない位置。

これらのショートハンドを使うことで、パターンをより簡潔に記述できます。

コード例:

“`python
import re

文字クラスの例

print(re.search(r”[aeiou]”, “Python”))
print(re.search(r”[0-9]+”, “Version 3.9″))
print(re.search(r”[^0-9]”, “12345abc”)) # 12345 の後の ‘a’ にマッチ

ショートハンドの例

print(re.search(r”\d{3}”, “Phone: 123-456″)) # 123 にマッチ
print(re.search(r”\s+”, “Multiple spaces”)) # 複数のスペースにマッチ
print(re.search(r”\w+”, “First_Name”)) # First_Name 全体にマッチ
print(re.findall(r”\w+”, “Hello World!”)) # [‘Hello’, ‘World’] をリストで返す

\b (単語境界) の例

“cat” という単語全体にマッチさせたい場合

print(re.search(r”\bcat\b”, “The cat sat on the mat.”)) # cat にマッチ
print(re.search(r”\bcat\b”, “category is here.”)) # マッチしない (category は単語)
print(re.search(r”cat”, “category is here.”)) # cat にマッチ (単語境界ではない)

\B (非単語境界) の例

print(re.search(r”\Bcat\B”, “category is here.”)) # category の中の cat にマッチ
print(re.search(r”\Bcat\B”, “The cat sat on the mat.”)) # マッチしない (cat は単語)
“`

\b は非常に重要で、単語全体にマッチさせたい場合に誤ったマッチングを防ぐために使われます。例えば、”cat” という単語だけを見つけたいのに、”category” の中の “cat” や “duplicate” の中の “cat” にマッチさせたくない、といった場合に \bcat\b が役立ちます。

4. 繰り返し(量指定子)の詳細

メタ文字 *, +, ?, {m,n} は、直前の要素がどれだけ繰り返されるかを指定する「量指定子」です。既に基本で紹介しましたが、ここではもう少し詳しく解説します。

  • *: 0回以上の繰り返し
  • +: 1回以上の繰り返し
  • ?: 0回または1回の繰り返し

  • {m}: ちょうどm回の繰り返し

  • {m,}: m回以上の繰り返し
  • {m,n}: m回からn回までの繰り返し (m <= n)

例:

  • a*: “”, “a”, “aa”, “aaa”, …
  • a+: “a”, “aa”, “aaa”, …
  • a?: “”, “a”
  • a{3}: “aaa”
  • a{2,}: “aa”, “aaa”, “aaaa”, …
  • a{2,4}: “aa”, “aaa”, “aaaa”

貪欲マッチ (Greedy) vs. 非貪欲マッチ (Non-greedy/Lazy)

デフォルトでは、量指定子は可能な限り長い文字列にマッチしようとします。これを「貪欲マッチ (Greedy Match)」と呼びます。しかし、多くの場合、最短のマッチングが必要になることがあります。これを実現するのが「非貪欲マッチ (Non-greedy Match)」または「怠惰マッチ (Lazy Match)」です。

量指定子の後に ? を付けることで、非貪欲マッチになります。

  • *?: 直前の要素の0回以上の繰り返し(最短マッチ)
  • +?: 直前の要素の1回以上の繰り返し(最短マッチ)
  • ??: 直前の要素の0回または1回の繰り返し(最短マッチ)
  • {m,n}?: 直前の要素のm回からn回までの繰り返し(最短マッチ)

コード例:

“`python
import re

text = “Link1Link2

貪欲マッチの例

から までのパターン。.* は可能な限り長い文字列にマッチしようとする。

結果: Link1Link2 全体にマッチしてしまう。

print(re.search(r”.*“, text))

非貪欲マッチの例

.*? は可能な限り短い文字列にマッチしようとする。

結果: Link1 にマッチ。findall を使うと全ての最短マッチが見つかる。

print(re.search(r”.?“, text))
print(re.findall(r”.
?“, text)) # [‘Link1‘, ‘Link2‘]
“`

貪欲マッチと非貪欲マッチは、特にHTMLタグのような構造化されたテキストを扱う際に重要になります。デフォルトの貪欲マッチでは意図しない長い範囲にマッチしてしまうことがよくあります。

5. グループ化とキャプチャ

丸カッコ () は、正規表現において非常に重要な役割を果たします。主に以下の二つの目的で使用されます。

  1. グループ化: 複数の要素をまとめて一つの単位として扱います。例えば、abc+ は ‘c’ のみを繰り返しますが、(abc)+ は ‘abc’ という並び全体を繰り返します。
  2. キャプチャ: マッチした文字列の一部を抽出するために使用されます。カッコで囲まれた部分は「キャプチャグループ」となり、後でその内容を取り出すことができます。

re.search()re.findall() といった関数は、キャプチャグループの情報も取得できます。

コード例:

“`python
import re

text = “Name: Alice, Age: 30”

グループ化の例

(abc)+ は ‘abc’ を1回以上繰り返すパターン

print(re.search(r”(abc)+”, “abcabcabc”))

キャプチャの例

(.*) で Name: の後、 Age: の前の部分をキャプチャ

match = re.search(r”Name: (.), Age: (.)”, text)

if match:
print(“マッチ全体:”, match.group(0)) # match.group(0) または match.group() はマッチ全体
print(“グループ1 (名前):”, match.group(1)) # 最初の () の中身
print(“グループ2 (年齢):”, match.group(2)) # 2番目の () の中身
print(“全てのグループ:”, match.groups()) # 全てのキャプチャグループをタプルで取得

findall とキャプチャグループ

() がない場合: マッチした文字列全体をリストで返す

print(re.findall(r”\d+”, “Numbers: 123, 456, 789”)) # [‘123’, ‘456’, ‘789’]

() が1つだけある場合: そのキャプチャグループの中身のリストを返す

print(re.findall(r”(\d+)”, “Numbers: 123, 456, 789”)) # [‘123’, ‘456’, ‘789’]

() が複数ある場合: 各マッチについて、キャプチャグループの中身を要素とするタプルのリストを返す

print(re.findall(r”(\w+): (\d+)”, “Alice: 25, Bob: 30”)) # [(‘Alice’, ’25’), (‘Bob’, ’30’)]
“`

名前付きグループ (?P<name>...)

キャプチャグループに名前を付けることで、インデックス番号ではなく名前でアクセスできるようになり、コードの可読性が向上します。構文は (?P<name>...) です。

“`python
import re

text = “Email: [email protected], UserID: 12345”

名前付きグループの例

pattern = r”Email: (?P\S+), UserID: (?P\d+)”
match = re.search(pattern, text)

if match:
print(“メールアドレス:”, match.group(“email”))
print(“ユーザーID:”, match.group(“userid”))
# 名前付きグループは groupdict() で辞書として取得できる
print(“辞書:”, match.groupdict()) # {‘email’: ‘[email protected]’, ‘userid’: ‘12345’}
“`

非キャプチャグループ (?:...)

カッコでグループ化したいだけで、その部分をキャプチャする必要がない場合は、非キャプチャグループ (?:...) を使用できます。これは特に | (または) と組み合わせて使う場合に便利です。非キャプチャグループはパフォーマンスのわずかな向上にもつながります(キャプチャ情報を保持しないため)。

“`python
import re

text = “catnap or dogmatic”

キャプチャグループの例 (意図せず ‘nap’ または ‘matic’ がキャプチャされる)

print(re.findall(r”(cat|dog)(nap|matic)”, text)) # [(‘cat’, ‘nap’), (‘dog’, ‘matic’)]

非キャプチャグループの例 (全体の ‘catnap’ または ‘dogmatic’ にマッチ)

print(re.findall(r”(?:cat|dog)(?:nap|matic)”, text)) # [‘catnap’, ‘dogmatic’]
“`

6. 先読み (Lookahead) と 後読み (Lookbehind)

先読みと後読みは「アサーション (Assertion)」と呼ばれる機能の一つです。これらは、特定のパターンが「その後に続くか(先読み)」または「その前に存在するか(後読み)」をチェックしますが、そのパターン自体はマッチ結果に含まれず、検索位置を消費しません。これは、特定の条件を満たす位置でマッチを開始したい場合に役立ちます。

アサーションは (?...) の形式で記述されます。

肯定的先読み Positive Lookahead (?=...)

(?=...) は、現在の位置の後に ... で指定されたパターンが続く場合にマッチします。マッチするのは現在の位置そのものであり、 ... の内容はマッチ結果に含まれません。

例: Python(?=3\.9) は、”Python” という文字列の直後に “3.9” が続く場合にのみ、”Python” にマッチします。

否定的先読み Negative Lookahead (?!...)

(?!...) は、現在の位置の後に ... で指定されたパターンが続かない場合にマッチします。マッチするのは現在の位置そのものです。

例: Python(?!3\.9) は、”Python” という文字列の直後に “3.9” が続かない場合にのみ、”Python” にマッチします。

肯定的後読み Positive Lookbehind (?<=...)

(?<=...) は、現在の位置の前に ... で指定されたパターンが存在する場合にマッチします。マッチするのは現在の位置そのものであり、 ... の内容はマッチ結果に含まれません。Pythonの正規表現エンジンでは、後読みのアサーション ... の長さは固定長である必要があります(一部のエンジンでは可変長も可能ですが、re モジュールでは基本的には固定長です)。

例: (?<=Salary: )\d+ は、”Salary: ” という文字列の直後に続く数字 (\d+) にマッチします。”Salary: ” 自体はマッチ結果に含まれません。この例では “Salary: ” は固定長です。

否定的後読み Negative Lookbehind (?<!...)

(?<!...) は、現在の位置の前に ... で指定されたパターンが存在しない場合にマッチします。マッチするのは現在の位置そのものです。こちらも ... の長さは固定長である必要があります。

例: (?<!$)\d+ は、数字の直前に “$” が存在しない場合に、その数字 (\d+) にマッチします。

コード例:

“`python
import re

text = “金額: $100, Price: ¥200, Salary: $5000”

肯定的先読みの例: $ の後に続く数字にマッチ

$ 自体はマッチに含めない

print(re.findall(r”\d+(?=,)”, text)) # “,” の直前の数字 [‘100’, ‘200’, ‘5000’]

肯定的後読みの例: “Salary: ” の後に続く数字にマッチ

“Salary: ” 自体はマッチに含めない (固定長 “Salary: ” は10文字)

print(re.findall(r”(?<=Salary: )\d+”, text)) # [‘5000’]

否定的先読みの例: $ で終わらない数字にマッチ

print(re.findall(r”\d+(?!$)”, text)) # 5000 以外の数字 (末尾ではない) [‘100’, ‘200’]

否定的後読みの例: $ が直前にない数字にマッチ

print(re.findall(r”(?<!\$)\d+”, text)) # $ の直後にない数字 [‘200’, ‘5000’]

この例では、’100′ は $ の直後にあるためマッチしない。’200′ と ‘5000’ はマッチする。

“`

先読み/後読みは、特定の文脈にある文字列だけを抜き出したい場合に非常に強力です。

7. Pythonの re モジュール

Pythonで正規表現を扱うには、まず import re として re モジュールをインポートします。re モジュールには、正規表現を使った様々な操作を行うための関数が用意されています。

主要な関数とその役割を説明します。

re.search(pattern, string, flags=0)

  • 機能: string の中で pattern にマッチする最初の位置を探します。
  • 返り値: マッチが見つかった場合は Matchオブジェクトを返します。見つからなかった場合は None を返します。
  • 注意: 文字列の先頭からだけでなく、文字列全体をスキャンします。

“`python
import re

text = “apple banana orange”

match = re.search(r”banana”, text)
if match:
print(“Found:”, match.group()) # Found: banana

match = re.search(r”grape”, text)
if match is None:
print(“Not found”) # Not found
“`

re.match(pattern, string, flags=0)

  • 機能: string先頭が pattern にマッチするかどうかを確認します。
  • 返り値: 文字列の先頭がパターンにマッチした場合は Matchオブジェクトを返します。マッチしなかった場合は None を返します。
  • 注意: re.search とは異なり、常に文字列の先頭からマッチングを試みます。先頭以外にパターンがあってもマッチしません。

“`python
import re

text = “apple banana orange”

match = re.match(r”apple”, text)
if match:
print(“Match at start:”, match.group()) # Match at start: apple

match = re.match(r”banana”, text)
if match is None:
print(“No match at start”) # No match at start
“`

re.match は、文字列の先頭が特定のパターンで始まるかどうかを検証したい場合に便利です。それ以外の場所でのマッチングには re.search を使います。

re.fullmatch(pattern, string, flags=0)

  • 機能: string 全体が pattern に完全にマッチするかどうかを確認します。
  • 返り値: 文字列全体がパターンにマッチした場合は Matchオブジェクトを返します。マッチしなかった場合は None を返します。
  • 注意: 文字列の一部だけがマッチしてもダメで、文字列の最初から最後までパターンが完全に一致する必要があります。これは、パターンに暗黙的に ^$ が付けられているのと同じように動作します。

“`python
import re

text1 = “12345”
text2 = “12345abc”

pattern = r”\d+” # 1回以上の数字の繰り返し

print(re.search(pattern, text1)) # マッチ
print(re.match(pattern, text1)) # マッチ
print(re.fullmatch(pattern, text1)) # マッチ (文字列全体が数字)

print(re.search(pattern, text2)) # マッチ (12345 の部分)
print(re.match(pattern, text2)) # マッチ (12345 の部分、先頭から)
print(re.fullmatch(pattern, text2)) # マッチしない (全体が数字ではない)
“`

re.findall(pattern, string, flags=0)

  • 機能: string の中で pattern にマッチする全ての部分を探し、リストとして返します。
  • 返り値: マッチした部分文字列のリストを返します。マッチが見つからなかった場合は空のリスト [] を返します。
  • 注意: パターンにキャプチャグループ () が含まれている場合、リストの要素はマッチ全体ではなく、キャプチャグループの中身になります。複数のキャプチャグループがある場合は、タプルのリストになります(((group1), (group2), ...))。キャプチャグループがない場合は、マッチした部分文字列のリストになります。

“`python
import re

text = “Emails: [email protected], [email protected], invalid-email”

キャプチャグループなし: マッチした部分文字列全体をリストで返す

print(re.findall(r”\w+@\w+.\w+”, text))

出力例: [‘[email protected]’, ‘[email protected]’]

キャプチャグループあり: 各キャプチャグループの中身を要素とするタプルのリストを返す

print(re.findall(r”(\w+)@(\w+).(\w+)”, text))

出力例: [(‘test1’, ‘example’, ‘com’), (‘user’, ‘domain’, ‘co’), (‘jp’,)] # .jp の扱いは要調整

より正確なメールアドレスパターンは後述の実用例で紹介します。

“`

re.finditer(pattern, string, flags=0)

  • 機能: string の中で pattern にマッチする全ての部分を探し、それらを要素とするイテレーターを返します。
  • 返り値: マッチが見つかるたびに Matchオブジェクトを生成するイテレーターを返します。
  • 注意: re.findall が結果をリストで一度に返すのに対し、re.finditer は一つずつ Matchオブジェクトを生成します。特に大きな文字列を扱う場合や、マッチした各部分について開始/終了位置などの詳細情報が必要な場合にメモリ効率が良い場合があります。

“`python
import re

text = “Numbers: 10 20 30 40 50″
pattern = r”\d+”

findall の場合

matches_list = re.findall(pattern, text)
print(matches_list) # [’10’, ’20’, ’30’, ’40’, ’50’]

finditer の場合

matches_iter = re.finditer(pattern, text)
for match in matches_iter:
print(f”Found ‘{match.group()}’ at position {match.span()}”)

出力例:

Found ’10’ at position (9, 11)

Found ’20’ at position (12, 14)

Found ’30’ at position (15, 17)

Found ’40’ at position (18, 20)

Found ’50’ at position (21, 23)

“`

re.sub(pattern, repl, string, count=0, flags=0)

  • 機能: string の中で pattern にマッチする部分を repl で置換します。
  • 返り値: 置換された新しい文字列を返します。
  • 引数:
    • pattern: 置換対象の正規表現パターン。
    • repl: 置換後の文字列。関数を指定することも可能。
    • string: 置換対象の文字列。
    • count: 最大置換回数。デフォルトは0で、全てのマッチを置換します。
    • flags: フラグ。
  • 注意: repl 文字列では、バックスラッシュ + 数字 (\1, \2 など) やバックスラッシュ + グループ名 (\g<name>, \g<1>) を使って、マッチしたキャプチャグループの内容を参照できます。

“`python
import re

text = “Date: 2023-10-27”

ハイフンをスラッシュに置換

new_text = re.sub(r”-“, “/”, text)
print(new_text) # Date: 2023/10/27

日付形式を YYYY/MM/DD から MM-DD-YYYY に変更 (キャプチャグループ参照)

パターン: (\d{4})-(\d{2})-(\d{2}) => グループ1:年, グループ2:月, グループ3:日

置換文字列: \2-\3-\1 => グループ2-グループ3-グループ1

date_text = “Today’s date is 2023-10-27.”
new_date_text = re.sub(r”(\d{4})-(\d{2})-(\d{2})”, r”\2-\3-\1″, date_text)
print(new_date_text) # Today’s date is 10-27-2023.

置換文字列として関数を指定する例

def replace_func(match):
# マッチした部分の数字を2倍にする
num_str = match.group(0)
num = int(num_str)
return str(num * 2)

text_nums = “Numbers: 10 20 30″
new_text_nums = re.sub(r”\d+”, replace_func, text_nums)
print(new_text_nums) # Numbers: 20 40 60
“`

re.split(pattern, string, maxsplit=0, flags=0)

  • 機能: stringpattern にマッチする部分で分割し、文字列のリストを返します。
  • 返り値: 分割された文字列のリストを返します。
  • 引数:
    • pattern: 区切り文字となる正規表現パターン。
    • string: 分割対象の文字列。
    • maxsplit: 最大分割回数。デフォルトは0で、可能な限り全て分割します。
    • flags: フラグ。
  • 注意: 区切り文字パターンにキャプチャグループが含まれている場合、マッチした区切り文字も結果のリストに含まれます。

“`python
import re

text = “apple,banana;orange grape”

カンマまたはセミコロンまたはスペースで分割

print(re.split(r”[,; ]”, text)) # [‘apple’, ‘banana’, ‘orange’, ‘grape’]

最大分割回数を指定

print(re.split(r”[,; ]”, text, maxsplit=1)) # [‘apple’, ‘banana;orange grape’]

区切り文字をキャプチャグループで囲むと、区切り文字も結果に含まれる

print(re.split(r”([,; ])”, text)) # [‘apple’, ‘,’, ‘banana’, ‘;’, ‘orange’, ‘ ‘, ‘grape’]
“`

re.compile(pattern, flags=0)

  • 機能: 正規表現パターンを正規表現オブジェクトにコンパイルします。
  • 返り値: 正規表現オブジェクトを返します。
  • 注意: 同じパターンを繰り返し使用する場合(特にループ内など)、毎回パターン文字列をコンパイルするよりも、一度 re.compile() でコンパイルしておき、そのオブジェクトのメソッド(search, findall など)を使用する方が効率的です。パフォーマンスが重要な場合に検討します。

“`python
import re

パターンをコンパイル

compiled_pattern = re.compile(r”\d+”)

コンパイルされたオブジェクトのメソッドを使用

text1 = “Value: 100”
text2 = “Score: 95”

match1 = compiled_pattern.search(text1)
match2 = compiled_pattern.search(text2)

if match1:
print(“Match in text1:”, match1.group())
if match2:
print(“Match in text2:”, match2.group())

reモジュールの関数は内部でコンパイルを行っているが、繰り返し使うなら手動でコンパイルが推奨

re.search(r”\d+”, text1) # これは毎回コンパイルが走る(内部的にはキャッシュされることもあるが)

“`

8. Matchオブジェクト

re.search(), re.match(), re.fullmatch(), re.finditer() がマッチを見つけた際に返すのが Matchオブジェクトです。このオブジェクトには、マッチした結果に関する様々な情報が格納されています。

Matchオブジェクトの主なメソッドと属性を紹介します。

メソッド/属性 意味
group() マッチした部分文字列全体 match.group()
group(g) 指定したグループ番号 g のマッチ文字列 match.group(1) (1番目のキャプチャ)
group(g1, g2,...) 指定した複数のグループのタプル match.group(1, 2)
groups() 全てのキャプチャグループのタプル match.groups()
groupdict() 名前付きキャプチャグループの辞書 match.groupdict()
start() マッチした部分の開始インデックス match.start()
end() マッチした部分の終了インデックス (排他的) match.end()
span() マッチした部分の (開始, 終了) タプル match.span()
re マッチに使用された正規表現オブジェクト match.re.pattern (パターン文字列)
string 検索対象となった元の文字列 match.string

コード例:

“`python
import re

text = “User: admin (ID: 101)”
pattern = r”User: (\w+) (ID: (\d+))” # (\w+) がグループ1, (\d+) がグループ2

match = re.search(pattern, text)

if match:
print(“マッチ全体:”, match.group(0))
print(“ユーザー名 (グループ1):”, match.group(1))
print(“ID (グループ2):”, match.group(2))
print(“複数のグループ:”, match.group(1, 2))
print(“全てのグループ:”, match.groups())

print("マッチの開始インデックス:", match.start())
print("マッチの終了インデックス (排他的):", match.end())
print("マッチの範囲 (タプル):", match.span())

# 名前付きグループを使った場合
pattern_named = r"User: (?P<username>\w+) \(ID: (?P<userid>\d+)\)"
match_named = re.search(pattern_named, text)
if match_named:
    print("ユーザー名 (名前付き):", match_named.group("username"))
    print("ID (名前付き):", match_named.group("userid"))
    print("名前付きグループ辞書:", match_named.groupdict())

print("使用されたパターン:", match.re.pattern)
print("検索対象文字列:", match.string)

“`

Matchオブジェクトを使いこなすことで、単にパターンにマッチしたかどうかだけでなく、具体的にどの部分がどのようにマッチしたのかという詳細な情報をプログラムから取得し、活用することができます。

9. フラグ (Flags)

re モジュールの関数の多くや re.compile() には flags 引数があります。これにより、正規表現のマッチング挙動を変更できます。複数のフラグを指定する場合は、ビット論理和演算子 | を使って結合します。

よく使われるフラグを紹介します。

  • re.IGNORECASE (または re.I): 大文字小文字を区別せずにマッチングを行います。
  • re.MULTILINE (または re.M): 文字列中の各行の開始 (^) と終了 ($) にマッチするようになります。通常、^ は文字列全体の開始、$ は文字列全体の終了または末尾の改行の直前にのみマッチします。
  • re.DOTALL (または re.S): メタ文字 . が改行文字 (\n) を含む全ての文字にマッチするようになります。通常、. は改行文字にはマッチしません。
  • re.VERBOSE (または re.X): 正規表現の中で空白文字や # で始まるコメントを無視できるようになります。これにより、複雑なパターンを読みやすく記述できます。

コード例:

“`python
import re

re.IGNORECASE の例

print(re.search(r”apple”, “Apple”, re.IGNORECASE)) # マッチする

re.MULTILINE の例

text = “Line 1\nLine 2\nLine 3”

フラグなし: $ は文字列全体の末尾にしかマッチしない

print(re.findall(r”^Line”, text)) # [‘Line’]
print(re.findall(r”e$”, text)) # [‘e’] (Line 3 の e)

フラグあり (re.M): ^ は各行の先頭, $ は各行の末尾にマッチ

print(re.findall(r”^Line”, text, re.MULTILINE)) # [‘Line’, ‘Line’, ‘Line’]
print(re.findall(r”e$”, text, re.MULTILINE)) # [‘e’, ‘e’, ‘e’] (Line 1, Line 2, Line 3 の e)

re.DOTALL の例

text_with_newline = “Line 1\nLine 2”

フラグなし: . は改行にマッチしない

print(re.search(r”Line.\nLine”, text_with_newline)) # マッチしない (.が改行にマッチしないため)
print(re.search(r”Line.\sLine”, text_with_newline)) # マッチする (.がスペース以外の任意文字、\sが空白文字にマッチ)

フラグあり (re.S): . が改行にもマッチする

print(re.search(r”Line.\nLine”, text_with_newline, re.DOTALL)) # マッチする (.が改行にマッチするため)

re.VERBOSE の例

複雑なパターンを読みやすく書く

pattern = r”””
^ # 文字列の開始
\d{4} # 年 (4桁の数字)
– # ハイフン
\d{2} # 月 (2桁の数字)
– # ハイフン
\d{2} # 日 (2桁の数字)
$ # 文字列の終了
“””
date_string = “2023-10-27”
print(re.fullmatch(pattern, date_string, re.VERBOSE)) # マッチする
“`

re.VERBOSE は、特に複雑な正規表現パターンを記述する際に、パターン内にコメントを書いたり、要素を改行して並べたりできるようになるため、非常に役立ちます。

10. 正規表現のコンパイル (re.compile)

前述の通り、re.compile(pattern, flags=0) 関数は、正規表現パターンを正規表現オブジェクトにコンパイルします。

なぜコンパイルが必要か?

Pythonの re モジュールの関数(re.search(), re.findall() など)は、パターン文字列を受け取ると、内部的にそのパターンを正規表現エンジンが処理できる形式(バイトコードのようなもの)にコンパイルしてからマッチングを実行します。同じパターンを繰り返し使用する場合、毎回このコンパイル処理が行われると、特に大量の文字列に対して多くのマッチングを行う場合にオーバーヘッドが発生します。

re.compile() を使って一度パターンをコンパイルしておけば、そのコンパイル済みのオブジェクトを使って繰り返しマッチングを実行できます。これにより、コンパイルのコストが削減され、パフォーマンスが向上します。

使用例:

“`python
import re

パターンをコンパイル

このパターンを何度も使うと仮定する

phone_pattern = re.compile(r”^\d{3}-\d{4}-\d{4}$”)

コンパイル済みオブジェクトのメソッドを使う

search, match, fullmatch, findall, finditer, sub, split が使える

print(phone_pattern.fullmatch(“090-1234-5678”)) # Matchオブジェクト
print(phone_pattern.fullmatch(“09012345678”)) # None

text = “Call 090-1111-2222 or 080-3333-4444”
print(phone_pattern.findall(text)) # findall も使える

出力例: [‘090-1111-2222’, ‘080-3333-4444’]

“`

パフォーマンスが問題にならないような単発の短いスクリプトであれば、re.search()re.findall() を直接使うので十分です。しかし、ループ内でマッチングを行う場合や、多くのテキストファイルに対して同じパターンを適用する場合など、繰り返し同じパターンを使う場面では re.compile() を使用することを検討しましょう。

なお、Pythonの re モジュールは内部で最近使用されたパターンのキャッシュも行っているため、短いスクリプトであれば re.compile の有無によるパフォーマンス差はほとんど感じられないこともあります。しかし、大規模なアプリケーションや性能が求められる場面では、明示的にコンパイルすることが推奨されます。

11. 実用的な例

これまでに学んだ正規表現の概念とPythonの re モジュールの機能を組み合わせて、いくつかの実用的な例を見てみましょう。

例1:メールアドレスの検証

メールアドレスの形式は厳密には非常に複雑ですが、ここでは一般的な形式 ([email protected]) を検証する簡単なパターンを作成します。

“`python
import re

def is_valid_email(email):
# 簡単なメールアドレスパターン:
# ^\S+@\S+.\S+$
# ^ : 文字列の先頭
# \S+ : 1文字以上の空白文字以外の文字 (ユーザー名部分)
# @ : @マーク
# \S+ : 1文字以上の空白文字以外の文字 (ドメイン名部分)
# . : ドット (.)
# \S+ : 1文字以上の空白文字以外の文字 (トップレベルドメイン部分)
# $ : 文字列の末尾
pattern = re.compile(r”^\S+@\S+.\S+$”)
# fullmatch を使うことで、文字列全体がパターンに一致するか確認
return pattern.fullmatch(email) is not None

print(is_valid_email(“[email protected]”)) # True
print(is_valid_email(“[email protected]”)) # True (このパターンでも許容)
print(is_valid_email(“invalid-email”)) # False (@ がない)
print(is_valid_email(“no@domain”)) # False (. がない)
print(is_valid_email(“has [email protected]”)) # False (スペースがある)
“`

注意: 上記のパターンはあくまでシンプルな例であり、RFCに準拠した厳密なメールアドレス検証にはさらに複雑なパターンが必要になります(あるいは専用のライブラリを使うのが現実的です)。

例2:URLの抽出

テキストの中からHTTPまたはHTTPSで始まるURLを抽出します。

“`python
import re

text = “””
Visit my website at https://www.example.com.
Also check out http://blog.example.org/post/123.
An invalid url: ftp://file.server.
“””

URL抽出パターン:

(https?://) : グループ1 (https:// または http://) – ?はsの有無

(\S+) : グループ2 (プロトコル部分以降、空白文字以外の文字の繰り返し)

pattern = re.compile(r”(https?://)(\S+)”)

findall を使うと全てのマッチをリストで取得

このパターンには2つのキャプチャグループがあるため、結果はタプルのリストになる

urls = re.findall(pattern, text)

print(“URLs found:”)
for protocol, rest in urls:
print(protocol + rest) # プロトコル部分と残りの部分を結合して表示

finditer を使うと Matchオブジェクトを取得できる

print(“\nDetails using finditer:”)
for match in re.finditer(pattern, text):
print(f”Match: {match.group(0)}”) # マッチ全体
print(f” Protocol: {match.group(1)}”) # グループ1 (プロトコル)
print(f” Rest: {match.group(2)}”) # グループ2 (プロトコル以降)
print(f” Span: {match.span()}”)
“`

例3:特定のタグ内のコンテンツを抽出 (HTMLライクな文字列から)

<tag>...</tag> のような形式のテキストから、タグで囲まれた中身を抽出します。ただし、正規表現はHTML/XMLのような構造化されたデータを厳密に解析するのには向きません。ここでは単純なケースで非貪欲マッチの利用例として紹介します。

“`python
import re

html_text = “Page Title

This is a paragraph.

から までのパターン (非貪欲マッチを使用):

<(.*?)> : グループ1 (開始タグの名前) – < と > に囲まれた最短のマッチ

(.*?) : グループ2 (タグの中身) – 最短のマッチ

<\/\1> : グループ1の名前と同じ終了タグ – \1 は最初のキャプチャグループの内容を参照

pattern = re.compile(r”<(.?)>(.?)</\1>”)

findall でマッチした全てのタグとその中身を取得

このパターンには2つのキャプチャグループがあるので、結果はタプルのリスト

matches = re.findall(pattern, html_text)

print(“Tag contents found:”)
for tag_name, content in matches:
print(f”Tag: {tag_name}, Content: {content}”)

出力例:

Tag: title, Content: Page Title

Tag: p, Content: This is a paragraph.

“`

注意: このパターンは非常に単純であり、ネストしたタグや属性を含む複雑なHTMLを正確にパースすることはできません。HTMLやXMLを扱う場合は、Beautiful Soupやlxmlなどの専用ライブラリを使用することを強く推奨します。正規表現でのHTMLパースは、特定のごく単純なパターンを探す場合に限定すべきです。

例4:CSVライクな文字列の解析

簡単なCSV形式の文字列を、フィールドごとに分割します。区切り文字がカンマで、フィールド内にカンマや改行が含まれる可能性(ただし、その場合は引用符で囲まれている)は考慮しないシンプルな例です。

“`python
import re

csv_line = “Alice,30,New York”

カンマで分割

fields = re.split(r”,”, csv_line)
print(fields) # [‘Alice’, ’30’, ‘New York’]

csv_lines = “””
Name,Age,City
Alice,30,”New York”
Bob,25,London
“””

各行を分割し、さらに各行をカンマで分割する

lines = re.split(r”\n”, csv_lines.strip()) # trim() で先頭・末尾の空白・改行を除去
data = [re.split(r”,”, line) for line in lines]

ヘッダーとデータを表示

print(“Header:”, data[0])
print(“Data:”)
for row in data[1:]:
print(row)
“`

より複雑なCSV(引用符付きフィールドや区切り文字のエスケープなど)を扱う場合は、Python標準ライブラリの csv モジュールを使う方がはるかに堅牢です。正規表現での split は、区切り文字が単純な場合に便利です。

12. 正規表現を書く上でのヒントと注意点

正規表現は強力なツールですが、正しく安全に使うためにはいくつかの注意点があります。

  • Raw String (r”…”) を使う: Pythonの文字列リテラルでのバックスラッシュのエスケープと正規表現のバックスラッシュが衝突するのを避けるため、正規表現パターンには必ず Raw String (r"...") を使いましょう。
  • 複雑なパターンは re.VERBOSE を使う: パターンが長くなったり複雑になったりする場合は、re.VERBOSE フラグを使って、パターンを複数の行に分割し、コメントを追加することで可読性を大幅に向上させましょう。
  • 適切な関数を選ぶ: re.search, re.match, re.fullmatch, re.findall, re.finditer それぞれに得意な場面があります。用途に合わせて最適な関数を選びましょう。文字列の先頭にマッチさせたいだけなら re.match、文字列全体なら re.fullmatch、全てのマッチを探したいなら re.findallre.finditer といった具合です。
  • Matchオブジェクトを有効活用する: マッチした位置やキャプチャグループの内容が必要な場合は、Matchオブジェクトからこれらの情報を取得しましょう。
  • デバッグツールを活用する: 複雑な正規表現パターンは意図しない挙動をすることがよくあります。regex101.com や pythex.org のようなオンラインの正規表現デバッグツールは、パターンとテスト文字列を入力して、どのようにマッチするか、どの部分がキャプチャされるかなどを視覚的に確認できるため、非常に役立ちます。
  • 過度に複雑なパターンを避ける: 一つの正規表現で全てを解決しようとすると、パターンが非常に長大で解読困難になることがあります。必要に応じて、複数の正規表現を組み合わせたり、Pythonの通常の文字列処理と組み合わせたりすることも検討しましょう。
  • セキュリティに注意 (ReDoS): ユーザーからの入力などを元に正規表現パターンを動的に生成したり、ユーザーに正規表現パターンを入力させたりする場合、悪意のあるパターン(特定の繰り返しとグループ化の組み合わせなど)によって、マッチング処理が極端に遅くなる「正規表現によるサービス拒否 (ReDoS)」攻撃を受ける可能性があります。信頼できないソースからのパターンを使う場合は注意が必要です。Pythonの re モジュールは一部の脆弱なパターンに対して保護機能を持っていますが、複雑なケースでは注意が必要です。
  • 構造化データのパースには専用ライブラリを: HTML, XML, JSONなどの構造化データをパースする際には、正規表現ではなく、それぞれのデータ形式に特化したライブラリ(例: Beautiful Soup, lxml, json)を使用するのが一般的で安全です。正規表現はシンプルなパターンマッチングに適しており、入れ子構造のような複雑な構造の解析には向きません。

13. まとめ

正規表現は、文字列処理の強力なツールであり、Pythonの re モジュールを使うことでその機能を最大限に引き出すことができます。この記事では、正規表現の基本的な概念であるパターンとマッチングから始まり、主要なメタ文字、文字クラス、量指定子、グループ化、先読み・後読みといった高度な構文を詳細に解説しました。

さらに、Pythonの re モジュールに用意されている re.search, re.match, re.fullmatch, re.findall, re.finditer, re.sub, re.split, re.compile といった主要な関数や、マッチ結果の詳細情報を格納する Matchオブジェクトの使い方についても詳しく見てきました。また、正規表現の挙動を変更するフラグや、繰り返しパターンを使う場合に有効なコンパイルについても説明しました。

メールアドレス検証、URL抽出、タグ内のコンテンツ抽出といった具体的な例を通して、これらの機能がどのように組み合わせて使われるのかを示しました。最後に、正規表現を効率的かつ安全に使うためのヒントや注意点についても触れました。

正規表現の学習は、最初はメタ文字や特殊な構文が多くて難しく感じるかもしれませんが、実際に手を動かして様々なパターンを試してみることで徐々に慣れていきます。この記事で紹介した内容を参考に、ぜひPythonでの正規表現活用に挑戦してみてください。文字列処理の幅が大きく広がるはずです。

コメントする

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

上部へスクロール