Pythonの正規表現は、標準ライブラリの re モジュールで使います。import re のあと、re.search() で探す、re.findall() で全部取り出す、re.sub() で置き換える、の3つを覚えれば大半の用途に対応できます。

この記事では、re の関数10種の「戻り値の違い」、Matchオブジェクト、フラグを早見表にまとめ、すべてのコードに Python 3.11.15 と 3.14.7 で実際に動かした実行結果 を添えました。メール・電話番号・郵便番号・日付などの よく使うパターン集、名前付きグループや先読み・後読み、re.sub() に関数を渡す置換、さらに「\d が全角数字にも一致する」「match は先頭だけ」といった つまずきやすい点を実行で確認 し、compile の速度差と ReDoS(正規表現の処理が極端に遅くなる現象)の回避策も実測しています。

記号(メタ文字)そのものの一覧は「正規表現 早見表」記事に譲り、ここでは Pythonで書くときの使い方 に絞って解説します。

Python 正規表現(re)の早見表

関数10種と戻り値の違い

やりたいこと 関数 戻り値 見つからないとき
文字列のどこかに1つ探す re.search(p, s) Match None
先頭から一致するか調べる re.match(p, s) Match None
全体が一致するか調べる(入力チェック) re.fullmatch(p, s) Match None
一致を全部リストで取る re.findall(p, s) list[str](グループがあれば list[tuple]) []
一致を位置つきで1つずつ取る re.finditer(p, s) Matchのイテレータ 空のイテレータ
置換する re.sub(p, repl, s) str 元の文字列
置換して件数も知る re.subn(p, repl, s) (str, 件数) (元の文字列, 0)
パターンで分割する re.split(p, s) list[str] [s]
パターンを使い回す re.compile(p, flags) Patternオブジェクト (構文エラーで例外)
記号をそのまま文字として扱う re.escape(s) str ―

Matchオブジェクトの早見表

書き方 取れるもの 例((?P<kind>[A-Z])-(?P<no>\d+) が A-101 に一致)
m.group() / m[0] 一致した全体 'A-101'
m.group(1) / m[1] 1番目のグループ 'A'
m.group("no") / m["no"] 名前付きグループ '101'
m.groups() 全グループのタプル ('A', '101')
m.groupdict() 名前付きグループの辞書 {'kind': 'A', 'no': '101'}
m.span() / m.span("no") (開始, 終了) の位置 (2, 7) / (4, 7)
m.start() / m.end() 開始位置 / 終了位置 2 / 7

フラグの早見表

フラグ 別名 インライン 効果
re.I re.IGNORECASE (?i) 大文字・小文字を区別しない
re.M re.MULTILINE (?m) ^ $ が各行の先頭・末尾に一致
re.S re.DOTALL (?s) . が改行にも一致
re.X re.VERBOSE (?x) パターン内の空白を無視し # コメントを書ける
re.A re.ASCII (?a) \d \w \s \b をASCIIだけに限定

フラグは re.I | re.M のように | で組み合わせます。

バージョンによる違い(公式ドキュメントで確認)

バージョン 変更点
3.11 アトミックグループ (?>...) と強欲な量指定子 *+ ++ ?+ が使えるようになった
3.13 re.sub() の count・flags を位置引数で渡すと DeprecationWarning。例外 re.error の新しい名前 re.PatternError が追加
3.14 文字列の末尾を表す \z が使えるようになった(\Z と同じ意味)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Python 正規表現(re)の使い方|関数・パターン早見表付き」

reモジュールの基本:import して関数を呼ぶ

re は標準ライブラリなので、インストールは不要です。 パターンは必ず r"..."(raw文字列)で書きます。理由は後半の「つまずき」で実行して確認します。

10個の関数をまとめて動かす

import re
import sys

print(sys.version.split()[0])
s = "注文A-101は3個、注文B-205は12個"
print("match   :", re.match(r"\d+", s))
print("search  :", re.search(r"\d+", s))
print("fullmatch:", re.fullmatch(r"\d+", "12345"))
print("findall :", re.findall(r"\d+", s))
print("finditer:", [m.span() for m in re.finditer(r"\d+", s)])
print("sub     :", re.sub(r"\d+", "#", s))
print("subn    :", re.subn(r"\d+", "#", s))
print("split   :", re.split(r"[、]", s))
p = re.compile(r"([A-Z])-(\d+)")
print("compile :", p, p.findall(s))
print("escape  :", re.escape("1+1=2 (a.b)*"))

実行結果(Python 3.11.15。3.14.7でも同じ出力):

3.11.15
match   : None
search  : <re.Match object; span=(4, 7), match='101'>
fullmatch: <re.Match object; span=(0, 5), match='12345'>
findall : ['101', '3', '205', '12']
finditer: [(4, 7), (8, 9), (15, 18), (19, 21)]
sub     : 注文A-#は#個、注文B-#は#個
subn    : ('注文A-#は#個、注文B-#は#個', 4)
split   : ['注文A-101は3個', '注文B-205は12個']
compile : re.compile('([A-Z])-(\\d+)') [('A', '101'), ('B', '205')]
escape  : 1\+1=2\ \(a\.b\)\*

読み取れるポイントは次のとおりです。

  • match が None になるのは、文字列の先頭が「注」で数字ではないからです。どこかにあるかを探すなら search を使います。
  • findall は文字列のリスト、finditer は Match を返すので、位置(span)も欲しいときは finditer を選びます。
  • compile したパターンは p.findall(s) のようにメソッドで呼べます。 同じパターンを何度も使うときに便利です。
  • escape は記号の前に \ を付けて、ユーザー入力などを「文字そのもの」として検索できる形にします。

どの関数を使うか迷ったら

目的 選ぶ関数 理由
入力値の形式チェック(郵便番号など) fullmatch 前後に余計な文字があると不一致にできる
ログ1行から値を1つ取り出す search 最初の1つで十分
文章から全部抜き出す findall リストで受け取れて簡単
全部抜き出し+位置や名前付きグループも使う finditer Match が手に入る
置き換え sub(件数も必要なら subn) 置換結果の文字列を返す
区切り文字が複数種類ある分割 split str.split は1種類しか指定できない

Matchオブジェクトで一致した部分を取り出す

search などが返す Match オブジェクトから、group() や groupdict() で値を取り出します。 グループは ( ) で作り、(?P<名前>...) で名前を付けられます。

import re

s = "注文A-101は3個"
m = re.search(r"(?P<kind>[A-Z])-(?P<no>\d+)", s)
print(m.group(), m.group(1), m.group("no"))
print(m.groups(), m.groupdict())
print(m.span(), m.start(), m.end(), m.span("no"))
print(m[0], m["kind"])

実行結果(Python 3.11.15):

A-101 A 101
('A', '101') {'kind': 'A', 'no': '101'}
(2, 7) 2 7 (4, 7)
A-101 A

m[0] や m["kind"] のような角かっこの書き方も使えます。 短く書けるので、lambda の中などで重宝します。

見つからないときの書き方(None 対策)

import re

m = re.search(r"\d+", "なし")
try:
    m.group()
except AttributeError as e:
    print("AttributeError:", e)

if (m := re.search(r"\d+", "在庫12")):
    print("walrus:", m[0])

実行結果(Python 3.11.15):

AttributeError: 'NoneType' object has no attribute 'group'
walrus: 12

見つからないと None が返るため、そのまま .group() を呼ぶと AttributeError になります。 if (m := re.search(...)): のように代入式(セイウチ演算子)で受けて、見つかったときだけ処理すると安全で短く書けます。

フラグの使い方を実行結果で確認

import re

print(re.findall(r"abc", "ABC abc", re.I))
txt = "line1\nline2"
print(re.findall(r"^line\d", txt), re.findall(r"^line\d", txt, re.M))
print(re.findall(r"1.l", txt), re.findall(r"1.l", txt, re.S))
pat = re.compile(r"""
  (\d{3})  # 3桁
  -        # ハイフン
  (\d{4})  # 4桁
""", re.X)
print(pat.findall("〒100-0001 と 530-0001"))
print(re.findall(r"\d+", "123と123"), re.findall(r"\d+", "123と123", re.A))
print(re.findall(r"\w+", "café_名前"), re.findall(r"\w+", "café_名前", re.A))
print(re.I | re.M)

実行結果(Python 3.11.15。3.14.7でも同じ):

['ABC', 'abc']
['line1'] ['line1', 'line2']
[] ['1\nl']
[('100', '0001'), ('530', '0001')]
['123', '123'] ['123']
['café_名前'] ['caf', '_']
re.IGNORECASE|re.MULTILINE
  • re.M を付けると ^ が2行目の先頭にも一致し、line2 も取れます。
  • re.S を付けると . が改行 \n にも一致します。 複数行にまたがる範囲を取りたいときに使います。
  • re.X は長いパターンにコメントを書けるので、あとから読む人にやさしい書き方です。
  • re.A を付けると \d は半角数字だけ、\w は英数字と _ だけになります。日本語や é は対象外になります。

よく使うパターン集(コピペ用)

次のコードは、1つのテキストからメール・電話番号・郵便番号・日付・URL・数字を取り出す完成形です。 用途に合わせてパターンだけ切り出して使えます。

import re

text = """問い合わせ: info@example.com / sales.team+jp@example.co.jp
電話: 03-1234-5678、090-1234-5678、0120-123-456
郵便番号: 〒150-0002
日付: 2026-10-01, 2026/9/5
URL: https://www.example.com/path?q=1 と http://example.org
担当: ヤマダ タロウ
"""
EMAIL = re.compile(r"[\w.+-]+@[\w-]+(?:\.[\w-]+)+", re.A)
print("email:", EMAIL.findall(text))
TEL = re.compile(r"0\d{1,4}-\d{1,4}-\d{3,4}")
print("tel  :", TEL.findall(text))
ZIP = re.compile(r"\b\d{3}-\d{4}\b", re.A)
print("zip  :", ZIP.findall(text))
DATE = re.compile(r"(?P<y>\d{4})[-/](?P<m>\d{1,2})[-/](?P<d>\d{1,2})")
print("date :", [m.groupdict() for m in DATE.finditer(text)])
URL = re.compile(r"https?://[\w/:%#$&?()~.=+-]+", re.A)
print("url  :", URL.findall(text))
print("kana :", re.fullmatch(r"[ァ-ヶー]+", "ヤマダ"), re.fullmatch(r"[ァ-ヶー]+", "ヤマダ"))
print("num  :", [int(x) for x in re.findall(r"-?\d+", "在庫 12, 入荷 -3, 合計 9", re.A)])
print("float:", re.findall(r"-?\d+(?:\.\d+)?", "1.5kg と 20 と -0.25", re.A))
print("space:", repr(re.sub(r"\s+", " ", "  a\t b\u3000\u3000c \n").strip()))
print("trailing:", repr(re.sub(r"[ \t]+$", "", "a  \nb\t\n", flags=re.M)))

実行結果(Python 3.11.15):

email: ['info@example.com', 'sales.team+jp@example.co.jp']
tel  : ['03-1234-5678', '090-1234-5678', '0120-123-456']
zip  : ['090-1234', '150-0002']
date : [{'y': '2026', 'm': '10', 'd': '01'}, {'y': '2026', 'm': '9', 'd': '5'}]
url  : ['https://www.example.com/path?q=1', 'http://example.org']
kana : <re.Match object; span=(0, 3), match='ヤマダ'> None
num  : [12, -3, 9]
float: ['1.5', '20', '-0.25']
space: 'a b c'
trailing: 'a\nb\n'

郵便番号の結果に 090-1234 が混ざっています。 携帯番号 090-1234-5678 の一部に \d{3}-\d{4} が一致したためです。前後に数字やハイフンが続かないことを、次の「後読み・先読み」で条件にすると解決します。

import re

ZIP2 = re.compile(r"(?<![\d-])\d{3}-\d{4}(?![\d-])")
print("zip2 :", ZIP2.findall("090-1234-5678 〒150-0002"))
zip2 : ['150-0002']

パターン一覧表

用途 パターン メモ
メールアドレス(抽出) [\w.+-]+@[\w-]+(?:\.[\w-]+)+ + re.A 厳密な判定は「メールアドレスの正規表現」記事を参照
電話番号(ハイフンあり) 0\d{1,4}-\d{1,4}-\d{3,4} 桁の組み合わせは緩めに許容
郵便番号(抽出) (?<![\d-])\d{3}-\d{4}(?![\d-]) 電話番号の一部に一致しないようにする
郵便番号(入力チェック) fullmatch(r"\d{3}-?\d{4}", s, re.A) ハイフンなしも許可
日付 (?P<y>\d{4})[-/](?P<m>\d{1,2})[-/](?P<d>\d{1,2}) 実在する日付かは datetime で確認
URL https?://[\w/:%#$&?()~.=+-]+ + re.A 末尾の句読点を含めない
全角カタカナのみ [ァ-ヶー]+ 半角カナは別の範囲。詳しくは「ひらがな・カタカナ・漢字を判定」記事
整数(符号つき) -?\d+ + re.A int() で数値に変換
小数も含む数値 -?\d+(?:\.\d+)? + re.A (?:...) で findall の戻り値をそろえる
空白の正規化 re.sub(r"\s+", " ", s).strip() 全角スペース・タブ・改行もまとめて1つの半角に
行末の空白を削除 re.sub(r"[ \t]+$", "", s, flags=re.M) re.M で各行の末尾に効かせる

日付パターンは形だけを見るので、2026-13-45 にも一致します。 値の正しさまで確かめるときは、正規表現で取り出したあと datetime.date(int(y), int(m), int(d)) に渡して確認すると確実です。

グループ・後方参照・先読み/後読み

後方参照と置換での参照

\1 は「1番目のグループと同じ文字列」を表します。 置換文字列の中では \1 や \g<名前> でグループを参照できます。

import re

print("dup  :", re.findall(r"\b(\w+) \1\b", "the the cat sat sat on"))
print("swap :", re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\3/\2/\1", "2026-10-01"))
print("named:", re.sub(r"(?P<y>\d{4})-(?P<m>\d{2})", r"\g<m>月\g<y>年", "2026-10"))
print("g<1>0:", re.sub(r"(\d)", r"\g<1>0", "a1b2"))

実行結果(Python 3.11.15):

dup  : ['the', 'sat']
swap : 01/10/2026
named: 10月2026年
g<1>0: a10b20

グループのすぐ後ろに数字を続けたいときは \g<1>0 と書きます。 \10 と書くと「10番目のグループ」と解釈されるためです。

先読み・後読み

先読み・後読みは「条件にはするが、一致結果には含めない」書き方です。

書き方 名前 意味
X(?=Y) 肯定先読み 後ろに Y が続く X
X(?!Y) 否定先読み 後ろに Y が続かない X
(?<=Y)X 肯定後読み 前に Y がある X
(?<!Y)X 否定後読み 前に Y がない X
import re

print("lookahead :", re.findall(r"\d+(?=円)", "120円 と 30個 と 450円"))
print("neg ahead :", re.findall(r"\b\d+\b(?!円)", "120円 30 450円", re.A))
print("lookbehind:", re.findall(r"(?<=¥)\d+", "¥980 と $15"))
P = r"(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}"
print("password  :", bool(re.fullmatch(P, "Abcdefg1")), bool(re.fullmatch(P, "abcdefg1")))
print("comma     :", re.sub(r"(?<=\d)(?=(\d{3})+(?!\d))", ",", "1234567"))

実行結果(Python 3.11.15):

lookahead : ['120', '450']
neg ahead : ['30']
lookbehind: ['980']
password  : True False
comma     : 1,234,567
  • パスワードの例は、先読みを3つ重ねて「小文字・大文字・数字をそれぞれ1つ以上含む」を表しています。
  • 3桁区切りは、位置(幅ゼロ)に , を挿入する置換です。数値なら f"{1234567:,}" のほうが簡単なので、文字列の中の数字を整えたいときに使います。
  • Pythonの後読みは固定長だけです。 (?<=\d+) のように長さが変わるパターンは re.error(3.13以降は re.PatternError)になります。

re.sub に関数を渡して置換する

re.sub() の第2引数には文字列だけでなく関数も渡せます。 関数は Match を受け取り、置き換え後の文字列を返します。計算や辞書引きを伴う置換が1行で書けます。

import re

def to_tax(m):
    return f"{int(m[1]) * 110 // 100}円(税込)"

print("func  :", re.sub(r"(\d+)円", to_tax, "ペン100円、ノート250円"))
print("lambda:", re.sub(r"[a-z]+", lambda m: m[0].upper(), "id=abc, name=xyz"))

table = {"cat": "ねこ", "dog": "いぬ"}
print("dict  :", re.sub("|".join(map(re.escape, table)), lambda m: table[m[0]], "cat and dog"))
print("hotdog:", re.sub(r"\b(?:cat|dog)\b", lambda m: table[m[0]], "hotdog cat"))

実行結果(Python 3.11.15):

func  : ペン110円(税込)、ノート275円(税込)
lambda: ID=ABC, NAME=XYZ
dict  : ねこ and いぬ
hotdog: hotdog ねこ

辞書のキーをまとめて置換するときは re.escape でキーを安全な形にしてから | でつなぐのが定番です。単語の一部(hotdog の dog)に反応させたくないときは \b で単語の境界を指定します。

置換文字列の中の「\」に注意

import re

print(re.sub(r"x", "C:\\new", "x"))
print(re.sub(r"x", lambda m: "C:\\new", "x"))

実行結果(Python 3.11.15):

C:
ew
C:\new

置換文字列の \n は改行として解釈されます。 Windowsのパスなど \ を含む文字列をそのまま入れたいときは、関数(lambda)で返すと文字どおりに入ります。

つまずきやすいポイントを実行で確認

1. パターンは raw 文字列 r"..." で書く

import re

print(repr("\bword\b"), re.findall("\bword\b", "a word b"), re.findall(r"\bword\b", "a word b"))
print(re.findall("\\\\", r"C:\dir"), re.findall(r"\\", r"C:\dir"))
'\x08word\x08' [] ['word']
['\\'] ['\\']

r を付けないと "\b" はバックスペース文字(\x08)になり、単語境界として働きません。 \ 1文字を探すときも、通常の文字列だと "\\\\" と4つ必要ですが、raw 文字列なら r"\\" で済みます。

2. 貪欲(最長一致)と最短一致

import re

html = '<b>太字</b>と<b>強調</b>'
print("greedy:", re.findall(r"<b>.*</b>", html))
print("lazy  :", re.findall(r"<b>.*?</b>", html))
print("negcls:", re.findall(r"<b>([^<]*)</b>", html))
greedy: ['<b>太字</b>と<b>強調</b>']
lazy  : ['<b>太字</b>', '<b>強調</b>']
negcls: ['太字', '強調']

.* はできるだけ長く一致するため、最初の <b> から最後の </b> までが1つになります。 .*? で最短一致にするか、[^<]*(< 以外の文字の並び)のように範囲を絞ると意図どおりに取れます。本格的なHTMLの解析には BeautifulSoup などのパーサーを使うと確実です。

3. match は先頭だけ、$ は末尾の改行を許す

import re

print(re.match(r"\d+", "No.123"), re.search(r"\d+", "No.123"))
print(re.match(r"\d+", "123abc"), re.fullmatch(r"\d+", "123abc"))
print(bool(re.match(r"\d+$", "123\n")), bool(re.fullmatch(r"\d+", "123\n")), bool(re.match(r"\d+\Z", "123\n")))
None <re.Match object; span=(3, 6), match='123'>
<re.Match object; span=(0, 3), match='123'> None
True False False

match は先頭からしか見ないため、"No.123" では None になります。 また match(r"\d+$") は末尾の改行 "123\n" を許してしまいます。 入力チェックは fullmatch を使うと、前後の余計な文字や改行も不一致にできます(3.14以降は \z でも同じ意味を書けます)。

4. findall はグループがあると中身だけ返す

import re

d = "2026-10-01 2025-01-15"
print(re.findall(r"\d{4}-\d{2}-\d{2}", d))
print(re.findall(r"(\d{4})-\d{2}-\d{2}", d))
print(re.findall(r"(\d{4})-(\d{2})-(\d{2})", d))
print(re.findall(r"(?:\d{4})-\d{2}-\d{2}", d))
print(re.split(r"(,)", "a,b,c"), re.split(r",", "a,b,c"))
['2026-10-01', '2025-01-15']
['2026', '2025']
[('2026', '10', '01'), ('2025', '01', '15')]
['2026-10-01', '2025-01-15']
['a', ',', 'b', ',', 'c'] ['a', 'b', 'c']
パターン中のグループ数 findall の戻り値
0個 一致全体の文字列のリスト
1個 そのグループの文字列のリスト
2個以上 グループのタプルのリスト

まとめるためだけの括弧は (?:...)(キャプチャしないグループ)にすると、一致全体が返ります。 split でもグループを使うと区切り文字が結果に残ります。

5. \d は全角数字やほかの文字の数字にも一致する

import re

print(re.fullmatch(r"\d{3}-\d{4}", "150-0002"))
print(re.fullmatch(r"\d{3}-\d{4}", "150-0002", re.A), re.fullmatch(r"[0-9]{3}-[0-9]{4}", "150-0002"))
print(int("150"))
print(re.findall(r"\d", "٣ ३ 3 3"))
<re.Match object; span=(0, 8), match='150-0002'>
None None
150
['٣', '३', '3', '3']

Python 3 の str パターンでは、\d は Unicode の数字すべてに一致します。 全角数字のほか、アラビア数字やデーヴァナーガリー数字も対象です。半角だけを許可したいときは re.A を付けるか [0-9] と書きます。 逆に全角数字も受け付けて数値にしたいなら、int("150") が 150 になるので、\d のまま取り出して int() に渡す方法も使えます。

6. re.sub の count は キーワード引数で渡す

import re
import warnings

warnings.simplefilter("always")
print(re.sub(r"a", "b", "aaa", 2))        # 位置引数
print(re.sub(r"a", "b", "aaa", count=2))  # キーワード引数

Python 3.14.7 での実行結果(1〜2行目は標準エラー出力の警告。ファイル名 re6.py のパス部分は省略):

re6.py:5: DeprecationWarning: 'count' is passed as positional argument
  print(re.sub(r"a", "b", "aaa", 2))        # 位置引数
bba
bba

3.13以降、count と flags を位置引数で渡すと DeprecationWarning が出ます(3.11では出ません)。re.sub(p, r, s, re.I) のように書くと、re.I(値は2)が count として扱われてしまうこともあるので、count= flags= とキーワードで書くのが確実です。

7. 構文エラーは re.error(3.13以降は re.PatternError)

import re

try:
    re.compile(r"(abc")
except re.error as e:
    print(type(e).__name__, e)
(3.11.15)error missing ), unterminated subpattern at position 0
(3.14.7) PatternError missing ), unterminated subpattern at position 0

except re.error と書けば、どのバージョンでも捕まえられます。 re.PatternError は re.error の新しい名前で、同じクラスです。

compile の効果を実測する

Python の re は、直近に使ったパターンを内部でキャッシュ(最大512件。re._MAXCACHE で確認)しています。 そのため1つのパターンなら compile しなくても大差はありませんが、関数呼び出しのたびにキャッシュを引く分の差は出ます。

import re
import timeit

lines = [f"order-{i:05d} qty={i%7} user{i}@example.com" for i in range(100_000)]
P = r"qty=(\d+)"

def no_compile():
    return [re.search(P, s) for s in lines]

cp = re.compile(P)
def with_compile():
    return [cp.search(s) for s in lines]

def no_compile_many():
    # キャッシュ(512件)を超える600種類のパターンを順に使う
    return [re.search(rf"qty=(\d+)|x{i%600}", s) for i, s in enumerate(lines[:20000])]

pats = [re.compile(rf"qty=(\d+)|x{i}") for i in range(600)]
def compiled_many():
    return [pats[i%600].search(s) for i, s in enumerate(lines[:20000])]

for f in (no_compile, with_compile, no_compile_many, compiled_many):
    t = min(timeit.repeat(f, number=1, repeat=5))
    print(f"{f.__name__:16s} {t*1000:8.1f} ms")

実行結果(このコンテナでの計測。値は環境で変わります):

関数 Python 3.11.15 Python 3.14.7
no_compile(1パターン・10万行) 52.5 ms 45.1 ms
with_compile(1パターン・10万行) 24.2 ms 30.7 ms
no_compile_many(600パターン・2万行) 1103.9 ms 800.3 ms
compiled_many(600パターン・2万行) 6.1 ms 5.8 ms

1パターンでは差は1.5〜2倍程度ですが、パターンの種類がキャッシュの512件を超えると、毎回コンパイルし直すため100倍以上の差になりました。 次の方針にすると、速さと読みやすさを両立できます。

状況 おすすめ
1回だけ使う re.search(...) のまま
ループの中で同じパターンを使う モジュールの先頭で PAT = re.compile(...)
パターンをたくさん動的に作る 作ったものを compile して保持する
名前を付けて意味を伝えたい compile して ZIP_CODE = ... のように命名

ReDoS(処理が極端に遅くなるパターン)と回避策

ReDoS は、入れ子の繰り返しなどでバックトラック(やり直し)の組み合わせが爆発し、短い入力でも処理時間が急増する現象です。ユーザーが入力した文字列を検査するときに起こりやすいので、パターンの形で防ぎます。

import re
import timeit

for n in (18, 20, 22, 24):
    s = "a" * n + "!"
    t = timeit.timeit(lambda: re.fullmatch(r"(a+)+", s), number=1)
    print(f"(a+)+  n={n}: {t*1000:9.1f} ms")

for n in (24, 10000):
    s = "a" * n + "!"
    for p in (r"a+", r"(?>a+)+", r"(a++)+"):
        t = timeit.timeit(lambda: re.fullmatch(p, s), number=1)
        print(f"{p:8s} n={n}: {t*1000:9.3f} ms")

実行結果(Python 3.11.15):

(a+)+  n=18:      12.0 ms
(a+)+  n=20:      51.0 ms
(a+)+  n=22:     200.1 ms
(a+)+  n=24:     786.2 ms
a+       n=24:     0.155 ms
(?>a+)+  n=24:     0.062 ms
(a++)+   n=24:     0.063 ms
a+       n=10000:     0.062 ms
(?>a+)+  n=10000:     0.006 ms
(a++)+   n=10000:     0.006 ms

(a+)+ は文字が2つ増えるごとに約4倍ずつ遅くなり、たった25文字で約0.79秒かかりました(別のスクリプトで計測した3.14.7では同じ n=24 で約1.55秒)。一方、入れ子をやめた a+ や、3.11で追加されたアトミックグループ (?>a+)+・強欲な量指定子 (a++)+ は1万文字でも1ミリ秒未満でした(1回だけの計測で、パターンの初回コンパイル時間を含みます)。

対策 書き方の例 ポイント
入れ子の繰り返しをやめる (a+)+ → a+ 同じ文字列を複数の分け方で一致させない
選択肢の重なりをなくす (\w|\d)+ → \w+ \d は \w に含まれるので重複している
強欲な量指定子(3.11+) a++、\d*+ 一度取った文字を手放さない
アトミックグループ(3.11+) (?>...) グループ内のやり直しをしない
入力の長さを先に制限 if len(s) > 256: ... 正規表現に渡す前に確認
範囲を具体的にする .* → [^,]* 一致できる文字を絞る

re には処理時間のタイムアウト機能がないため、パターンの書き方と入力の長さ制限で守るのが基本です。

よくある質問

Q. re.match と re.search の違いは何ですか? A. match は文字列の先頭からだけ、search は文字列のどこからでも探します。 実行結果のとおり、re.match(r"\d+", "No.123") は None、re.search は '123' を返します。全体が一致するかを確かめたいときは fullmatch を使います。

Q. re.compile は使ったほうがよいですか? A. 同じパターンをループで何度も使うなら使うと効果があります。 実測では1パターンで約1.5〜2倍、キャッシュの512件を超える数のパターンを使う場合は100倍以上の差が出ました。1回だけ使うなら re.search のままで十分です。

Q. 全角数字を除外して半角数字だけにしたいときは? A. re.A(re.ASCII)フラグを付けるか、\d の代わりに [0-9] と書きます。 Python 3 の \d は全角の 123 にも一致します。

Q. 置換で大文字にしたり、計算した値に置き換えたりできますか? A. re.sub() の第2引数に関数を渡せばできます。 re.sub(r"[a-z]+", lambda m: m[0].upper(), s) のように、Match を受け取って文字列を返す関数を指定します。

まとめ

  • re は標準ライブラリ。パターンは必ず r"..." の raw 文字列で書く
  • 探すのは search、全部取るのは findall / finditer、入力チェックは fullmatch、置換は sub
  • Match からは group()・groupdict()・span() で値と位置を取り出す。見つからないと None なので := で受ける
  • フラグは I・M・S・X・A の5つ。\d を半角だけにするなら re.A
  • re.sub() に関数を渡すと、計算や辞書引きを伴う置換を1行で書ける。count= はキーワードで渡す
  • ループで使うパターンは compile して保持すると速く、ReDoS は入れ子の繰り返しをやめるか ++・(?>...) で防ぐ

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Python 正規表現(re)の使い方|関数・パターン早見表付き」

株式会社RJCでは、Pythonの正規表現のような日々の道具の使い方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?