順番に読み進めながら学べます

正規表現 re — 文字列のパターン検索と置換

Python の re モジュールを基礎から解説します。re.match / search / findall の違い、メタ文字とグループキャプチャ、re.sub による置換、re.compile での再利用まで、ハンズオンで学べます。

正規表現 を扱う re モジュールで、「特定のパターンに合う文字列だけを抽出・置換する」 操作を整理します。電話番号・メール・ログ行・URL の解析など、実プロジェクトで頻出する処理を 1 行で書けるようになります。

正規表現を試せるツール

正規表現は組み合わせが多くて、頭の中だけだと組み立てづらい構文です。書いたパターンが意図通りに当たるかは、ブラウザで動く 正規表現エクストラクター で確認できます — テキストとパターンを入力すれば一致箇所がリアルタイムで見えるので、本記事の内容を読みながら横で試すと理解が進みます。

match と search と findall — 検索 3 種類の使い分け

re モジュール には文字列を検索する関数が複数あり、用途で 3 つを使い分けます。名前の対応を覚えておくと混乱しません — match = 先頭マッチ、search = 探す、findall = 全部見つける、です。具体的な検索範囲・戻り値・見つからないときの挙動は次の表で整理します。

関数検索範囲戻り値見つからないとき
re.match文字列の先頭のみMatch オブジェクトNone
re.search任意の位置で最初の一致Match オブジェクトNone
re.findallすべての一致文字列のリスト空のリスト []

re.matchre.search が返す Match オブジェクト(一致した位置・文字列・グループ情報を持つオブジェクト)から、一致した文字列を取り出すには .group() メソッド を呼びます — m.group() または m.group(0)マッチ全体、後述の グループキャプチャ を使うと m.group(1)( ) で囲んだ部分 だけを取り出せます。re.findall だけは戻り値が 直接リスト で、.group() を呼ぶ必要はありません。

re.match / search / findall の違い
re.match先頭から照合先頭が合わなければNonere.search任意の位置で最初の一致見つかれば Match無ければ Nonere.findallすべての一致見つかった文字列のリスト(見つからなければ [])
match は文字列の 先頭 にパターンが現れるかだけ見る。search任意の位置 で最初の一致を返す。findallすべての一致 をリストで返す。
メタ文字意味
\d1 桁の数字 (0-9)\d+ → 1 文字以上の数字
\w1 文字の英数字 + アンダースコア\w+ → ID やキーワード
\s1 文字の空白 (スペース/タブ/改行)区切り文字
.改行以外の任意の 1 文字ワイルドカード
*直前を 0 回以上a* → 空も OK
+直前を 1 回以上a+ → 1 文字以上
?直前を 0 回または 1 回省略可能
[abc]a / b / c のいずれか 1 文字選択
^ / $文字列の先頭 / 末尾アンカー
import re

text = "user_id: 12345, age: 30"

# match: 先頭から (\w+ は英数字の連続)
m = re.match(r"\w+", text)
print(m.group())            # user_id

# search: 任意の位置で最初の数字
s = re.search(r"\d+", text)
print(s.group())            # 12345

# findall: すべての数字
nums = re.findall(r"\d+", text)
print(nums)                 # ['12345', '30']

正規表現は raw string r"..." で書く

正規表現の中ではバックスラッシュ \ を多用します。普通の文字列 "\d" だと エスケープ解釈で消える ことがあるので、先頭に r を付けた raw string r"\d" で書くのが安全です。エディタでも raw string はハイライトされやすく、可読性も上がります。

ログ行から ID と数値を抽出 します。re.match / re.search / re.findall の 3 種類を 1 つの文字列で試して、結果の違いを観察します。

① re を読み込んでください

text = "order_id: 9876, qty: 3, price: 1500" と設定してください

③ 文字列の 先頭から英数字 + アンダースコアの連続 を取り出して match: ◯◯ の形で表示してください

④ 文字列の中から 最初の連続する数字 を取り出して search: ◯◯ の形で表示してください

⑤ 文字列の中の すべての連続する数字 をリストで取り出して findall: ◯◯ の形で表示してください

(正しく実行できれば解説が表示されます)

Python エディタ

コードを実行してください

グループキャプチャ — パターンの中の特定部分だけ取り出す

正規表現の ( ) で囲んだ部分キャプチャグループ と呼ばれ、マッチ全体ではなく その部分だけ を別々に取り出せます。例えば r"#(\d+) on (\d{4})-(\d{2})-(\d{2})" というパターンでログから注文番号と年月日を一気に分離する、といった用途で重宝します。

Match オブジェクトの .group(N) メソッドで N 番目のグループ(1 始まり)が取り出せます。.group(0) または引数なしの .group()マッチ全体 を返します。

グループキャプチャの仕組み
#(\d+) on(\d{4})-(\d{2})-(\d{2}).group(0)マッチ全体.group(1)注文番号.group(2).group(3)
正規表現の ( ) で囲んだ部分グループ になり、.group(1) / .group(2) のように 1 始まりの番号 で取り出せる。.group(0)マッチ全体 を表す。
import re

text = "Order #1234 placed on 2024-03-15"

# パターンの意味:
#   #         → リテラルの # 記号
#   (\d+)     → 1 桁以上の数字 → group(1) 注文番号
#   placed on → リテラルの「placed on」
#   (\d{4})   → 4 桁の数字 → group(2) 年
#   (\d{2})   → 2 桁の数字 → group(3) 月
#   (\d{2})   → 2 桁の数字 → group(4) 日
m = re.search(r"#(\d+) placed on (\d{4})-(\d{2})-(\d{2})", text)
if m:
    print("全体:", m.group(0))      # #1234 placed on 2024-03-15
    print("注文番号:", m.group(1))   # 1234
    print("年:", m.group(2))         # 2024
    print("月:", m.group(3))         # 03
    print("日:", m.group(4))         # 15

Match が None のときに .group() を呼ぶとエラー

re.search がパターンを 見つけられなかったときNone を返します。その状態で m.group() を呼ぶと AttributeError: 'NoneType' object has no attribute 'group' でクラッシュします。必ず if m: で None チェック してから .group() を呼ぶか、m := re.search(...) のセイウチ演算子 で同時に判定できます。

メールアドレスの形式から ユーザー名 と ドメイン を分離 します。グループキャプチャを使って、1 回の検索で 2 つのパーツを取り出します。

① re を読み込んでください

text = "問い合わせは alice@example.com まで" と設定してください

③ メールアドレスのパターンで アット記号の左右 をそれぞれグループとして取り出してください

- 左: 英数字とドット・アンダースコア・ハイフンの 1 文字以上

- 右: 同じく 1 文字以上、最後は .com などのドメインを含む

④ マッチが見つかった場合、ユーザー名: ◯◯ドメイン: ◯◯ の形でそれぞれ表示してください

Python エディタ

コードを実行してください

re.sub — パターンマッチで置換する

「ログから個人情報をマスキングしたい」「HTML タグを除去して本文だけ取り出したい」「全角・半角の空白をまとめて正規化したい」 — どれも 「特定パターンを、別の形に書き換えたい」 という置換のニーズです。文字列の replace だと固定文字列しか扱えませんが、re.sub ならパターンで指定できます。

re.sub(パターン, 置換文字列, 元文字列) は、パターンに一致した部分を 置換文字列に書き換えた新しい文字列 を返します。元の文字列は 変わりません(Python の文字列は不変なので、必ず戻り値を受け取る形になります)。

re.sub の動き
元文字列"連絡先 03-1234-5678"re.sub(\d, *, ...)新しい文字列"連絡先 **-****-****"
パターンに一致した箇所を置換文字列で書き換えた新しい文字列 を返す。元の文字列は不変で、戻り値で受け取るのが基本。
import re

# 電話番号の数字をマスク (\d 1 文字を * 1 文字に置換)
text = "連絡先: 03-1234-5678"
masked = re.sub(r"\d", "*", text)
print(masked)
# 連絡先: **-****-****

# HTML タグを除去して本文だけ取り出す
html = "<p>こんにちは <b>世界</b></p>"
plain = re.sub(r"<[^>]+>", "", html)
print(plain)
# こんにちは 世界

ログ行に含まれる電話番号の数字を全部 `` に置き換えてマスクします**。

① re を読み込んでください

text = "連絡先 03-1234-5678 と 090-9999-8888 まで" と設定してください

re.sub で *`\d` 1 文字を ` 1 文字に置換** して、結果を マスク後: ◯◯` の形で表示してください

④ 元の text変わっていない ことを 元のまま: ◯◯ の形で表示してください(re.sub は新しい文字列を返すだけ)

Python エディタ

コードを実行してください

re.compile — パターンを再利用する

同じ正規表現を 何度も使う とき、毎回 re.search(r"...", text) のように書くと、内部で パターンの解析(コンパイル)が毎回走って無駄 です。re.compile(パターン)コンパイル済みパターンオブジェクト を一度作っておけば、pattern.search(...) / pattern.findall(...) / pattern.sub(...) のようにそのオブジェクトに対してメソッドを呼べて、コードも整理されて速度も改善します。

re.compile の使い方
r"\d{2,4}-\d{4}-\d{4}"re.compile(...)phone_re(パターンオブジェクト)phone_re.search(text)phone_re.findall(text)phone_re.sub("*", text)
re.compile(パターン)パターンオブジェクト を作っておくと、.search / .findall / .sub何度でも呼び直せる。同じパターンを繰り返し使うときは必ずコンパイルする。
import re

# 同じ電話番号パターンを使い回す
phone_re = re.compile(r"\d{2,4}-\d{4}-\d{4}")

print(phone_re.findall("03-1234-5678 or 080-1111-2222"))
# ['03-1234-5678', '080-1111-2222']

print(phone_re.search("my phone is 03-9999-0000").group())
# 03-9999-0000

print(phone_re.sub("<電話番号>", "連絡先: 03-1234-5678 まで"))
# 連絡先: <電話番号> まで

電話番号パターンを `re.compile` で 1 回だけ作って、同じテキストに対して件数カウントと置換を続けて行います

① re を読み込んでください

text = "連絡先 03-1234-5678 と 090-9999-8888 まで" と設定してください

市外局番 2〜4 桁 + 4 桁 + 4 桁 の電話番号パターンを re.compile でコンパイルし、phone_re に入れてください

phone_re.findall(text)何件あるか を数えて、電話番号の件数: ◯ の形で表示してください

phone_re.sub電話番号全体を `<電話番号>` に置換 して、置換後: ◯◯ の形で表示してください

Python エディタ

コードを実行してください
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1re.match(r"\d+", "abc 123") の結果として正しいのはどれですか?

Q2正規表現で 1 文字以上の数字の連続 を表すパターンとして正しいのはどれですか?

Q3re.search(r"(\w+)@(\w+)", "alice@example") の結果から ドメイン側 だけを取り出すのはどれですか?

Q4正規表現を Python で書くときに r"..." の raw string を使う 主な理由はどれですか?