Q1re.match(r"\d+", "abc 123") の結果として正しいのはどれですか?
正規表現 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.match と re.search が返す Match オブジェクト(一致した位置・文字列・グループ情報を持つオブジェクト)から、一致した文字列を取り出すには .group() メソッド を呼びます — m.group() または m.group(0) で マッチ全体、後述の グループキャプチャ を使うと m.group(1) で ( ) で囲んだ部分 だけを取り出せます。re.findall だけは戻り値が 直接リスト で、.group() を呼ぶ必要はありません。
| メタ文字 | 意味 | 例 |
|---|---|---|
| \d | 1 桁の数字 (0-9) | \d+ → 1 文字以上の数字 |
| \w | 1 文字の英数字 + アンダースコア | \w+ → ID やキーワード |
| \s | 1 文字の空白 (スペース/タブ/改行) | 区切り文字 |
| . | 改行以外の任意の 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 はハイライトされやすく、可読性も上がります。
グループキャプチャ — パターンの中の特定部分だけ取り出す
正規表現の ( ) で囲んだ部分 は キャプチャグループ と呼ばれ、マッチ全体ではなく その部分だけ を別々に取り出せます。例えば r"#(\d+) on (\d{4})-(\d{2})-(\d{2})" というパターンでログから注文番号と年月日を一気に分離する、といった用途で重宝します。
Match オブジェクトの .group(N) メソッドで N 番目のグループ(1 始まり)が取り出せます。.group(0) または引数なしの .group() は マッチ全体 を返します。
.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(...) のセイウチ演算子 で同時に判定できます。
re.sub — パターンマッチで置換する
「ログから個人情報をマスキングしたい」「HTML タグを除去して本文だけ取り出したい」「全角・半角の空白をまとめて正規化したい」 — どれも 「特定パターンを、別の形に書き換えたい」 という置換のニーズです。文字列の replace だと固定文字列しか扱えませんが、re.sub ならパターンで指定できます。
re.sub(パターン, 置換文字列, 元文字列) は、パターンに一致した部分を 置換文字列に書き換えた新しい文字列 を返します。元の文字列は 変わりません(Python の文字列は不変なので、必ず戻り値を受け取る形になります)。
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.compile — パターンを再利用する
同じ正規表現を 何度も使う とき、毎回 re.search(r"...", text) のように書くと、内部で パターンの解析(コンパイル)が毎回走って無駄 です。re.compile(パターン) で コンパイル済みパターンオブジェクト を一度作っておけば、pattern.search(...) / pattern.findall(...) / pattern.sub(...) のようにそのオブジェクトに対してメソッドを呼べて、コードも整理されて速度も改善します。
.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 まで"))
# 連絡先: <電話番号> まで
理解度チェック
まずは1問ずつ答えてみましょう。
Q2正規表現で 1 文字以上の数字の連続 を表すパターンとして正しいのはどれですか?
Q3re.search(r"(\w+)@(\w+)", "alice@example") の結果から ドメイン側 だけを取り出すのはどれですか?
Q4正規表現を Python で書くときに r"..." の raw string を使う 主な理由はどれですか?