Q1re.match(r"\d+", "abc 123")の結果として正しいのはどれですか?
正規表現 re — 文字列のパターン検索と置換
re.match(先頭)・search(最初の一致)・findall(全件)の違い、\d \w \sや( )のグループキャプチャ、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 を使う主な理由はどれですか?