Python正規表現(Regex)の使い方完全ガイド|reモジュールの検索・抽出・置換を解説
Pythonで文字列を処理していると、「特定の形式だけを検索したい」「文章から数字だけを取り出したい」「複数種類の区切り文字をまとめて処理したい」といった場面が頻繁に発生します。単純な文字列検索だけでは条件が増えるほど処理が複雑になりますが、正規表現を使えば文字列の規則を一つのパターンとして表現できます。
Pythonには、正規表現を扱うための標準モジュールとしてreが用意されています。追加のライブラリを導入しなくても、文字列の検索、完全一致の確認、複数箇所の抽出、置換、分割などを実行できるため、Web開発、データ分析、業務自動化、ログ解析など幅広い用途で利用されています。
一方、正規表現は短く書ける反面、記号の意味や検索方法の違いを理解しないまま使うと、意図しない文字列まで一致させたり、処理速度を大きく低下させたりする可能性があります。本記事では、単なる記号一覧ではなく、実際のコードでどのように設計し、検証し、安全に運用するかまで順番に解説します。
1. Python正規表現とは
Python正規表現を正しく扱うには、最初に「何を検索する仕組みなのか」を理解する必要があります。正規表現は特定の文字列そのものではなく、文字列が従う規則をパターンとして記述する方法です。
1.1 正規表現とは
正規表現とは、文字列の並び方や形式を記号によって表現するための記述方法です。たとえば、文章中に含まれるすべての数字、郵便番号のような形式、特定の単語から始まる行などを、一つのパターンで検索できます。
通常の文字列検索では、検索したい文字をそのまま指定します。一方、正規表現では「数字が3文字続き、その後にハイフンがあり、さらに数字が4文字続く」といった構造を指定できる点が大きな特徴です。
コード例
import re
text = "お問い合わせ番号は123-4567です。"
pattern = r"\d{3}-\d{4}"
result = re.search(pattern, text)
if result:
print(result.group())
実行結果は123-4567になります。\dは数字、{3}は直前の要素が3回続くことを表しています。
1.2 パターンと検索対象文字列の関係
正規表現処理では、検索条件を表すパターンと、実際に調べる文字列を分けて考えます。パターンは「どのような文字列を探すか」を示し、検索対象文字列は「どこから探すか」を示します。
同じパターンを複数の文章に適用することも、複数のパターンを一つの文章に適用することも可能です。業務処理では、先に検索規則を決め、受け取ったデータへ繰り返し適用する形がよく使われます。
import re
pattern = r"\d+"
texts = [
"商品Aは1200円です",
"商品Bは850円です",
"商品Cは3000円です",
]
for text in texts:
result = re.search(pattern, text)
if result:
print(result.group())
この例では、同じ数字検索のパターンを三つの文字列に適用しています。\d+は、1文字以上連続する数字を意味します。
1.3 一致位置と一致範囲
正規表現の結果には、一致した文字列だけでなく、一致した開始位置と終了位置も含まれます。位置情報を利用すれば、一致部分の前後を取得したり、元の文章内で強調表示したりできます。
Pythonの文字列位置は0から始まり、終了位置は一致した最後の文字の次を示します。そのため、start()とend()を使うと、文字列の切り出しにもそのまま利用できます。
import re
text = "注文番号はAB-2048です。"
result = re.search(r"AB-\d+", text)
if result:
print(result.group())
print(result.start())
print(result.end())
print(result.span())
span()は、開始位置と終了位置を組にして返します。文章内の一部だけを別の処理へ渡したい場合に便利です。
1.4 正規表現が活用される場面
正規表現は、入力値の確認、ログからの情報抽出、ファイル名の分類、文章の整形、個人情報の伏せ字化などで活用されています。文字列に一定の規則が存在する処理ほど、正規表現との相性がよくなります。
たとえば、数千行のログからエラー番号だけを集める処理を、条件分岐だけで実装するとコードが長くなります。正規表現を使えば、エラー番号の形式を一つのパターンとして記述し、すべての行へ適用できます。
import re
logs = [
"INFO 処理を開始しました",
"ERROR E102 接続に失敗しました",
"WARNING 応答が遅延しています",
"ERROR E305 ファイルを開けません",
]
pattern = r"ERROR\s+(E\d+)"
for log in logs:
result = re.search(pattern, log)
if result:
print(result.group(1))
このコードでは、E102とE305だけを抽出できます。丸括弧で囲んだ部分は、後から個別に取得できます。
1.5 正規表現を使わないほうがよい場面
すべての文字列処理を正規表現で書く必要はありません。固定文字列の有無だけを調べる場合や、単純な前方一致、後方一致、置換だけで済む場合は、Pythonの文字列メソッドを利用したほうが読みやすくなります。
また、入れ子構造を持つHTML、複雑なJSON、構文規則を持つプログラムコードなどを正規表現だけで解析するのは適切ではありません。専用の解析機能や標準ライブラリが存在する場合は、そちらを優先することで誤判定を減らせます。
text = "report_2026.csv"
print(text.startswith("report_"))
print(text.endswith(".csv"))
print("2026" in text)
この程度の確認であれば、正規表現を使うよりも文字列メソッドのほうが意図を理解しやすく、処理も簡潔です。
2. reモジュールの導入と実行方法
Pythonでは、標準ライブラリのreモジュールを読み込むことで正規表現を利用できます。検索方法ごとの返り値や、パターン文字列の書き方を理解することが、安定した処理を作る第一歩です。
2.1 reモジュールの読み込み方
reモジュールはPythonに標準で含まれているため、通常は追加インストールが不要です。プログラムの先頭付近でimport reを実行すれば、正規表現用の各関数を呼び出せます。
検索だけでなく、完全一致、複数抽出、置換、分割なども同じモジュールから利用できます。処理内容によって関数を使い分けることで、検索意図が明確なコードになります。
import re
text = "Python 3.12"
result = re.search(r"\d+\.\d+", text)
if result:
print(result.group())
この例では、整数部分と小数部分を持つバージョン番号を検索しています。ピリオドを通常の文字として検索するため、\.と記述しています。
2.2 生文字列を使う理由
Pythonの正規表現では、パターンの前にrを付けた生文字列が一般的に使われます。正規表現ではバックスラッシュを多用しますが、通常のPython文字列でもバックスラッシュには特別な意味があるためです。
生文字列を使うと、Python側のエスケープ処理を減らし、正規表現として意図した記述を保ちやすくなります。特に\d、\s、\bなどを利用するときは、生文字列で記述する習慣を付けると安全です。
import re
text = "ID: 4582"
pattern = r"\d+"
result = re.search(pattern, text)
if result:
print(result.group())
通常文字列でも動作する場合はありますが、パターンが複雑になるほど誤解や警告の原因になります。原則として正規表現パターンにはr"..."を使います。
2.3 パターンを事前にコンパイルする方法
同じ正規表現を繰り返し使う場合は、re.compile()でパターンを事前に作成できます。作成された正規表現オブジェクトから、search()やfindall()などを直接呼び出せます。
事前コンパイルは、検索条件に名前を付ける役割も持っています。処理の途中に長い正規表現を直接書くより、目的を示す変数名へ代入したほうが、コードの保守性を高められます。
import re
postal_code_pattern = re.compile(r"\d{3}-\d{4}")
texts = [
"東京都 100-0001",
"大阪府 530-0001",
"形式不明",
]
for text in texts:
result = postal_code_pattern.search(text)
if result:
print(result.group())
繰り返し処理や複数の関数で同じパターンを使う場合は、コンパイル済みオブジェクトを共有すると便利です。
2.4 一致結果オブジェクトの扱い方
re.search()やre.match()が一致を検出すると、一致結果オブジェクトを返します。一致しなかった場合はNoneが返るため、結果を利用する前に存在確認が必要です。
一致結果オブジェクトからは、全文、グループ、開始位置、終了位置などを取得できます。単に一致したかどうかだけでなく、後続処理に必要な情報をまとめて保持している点が特徴です。
import re
text = "担当者: 佐藤"
result = re.search(r"担当者:\s*(.+)", text)
if result is not None:
print(result.group(0))
print(result.group(1))
group(0)は一致全体、group(1)は最初の丸括弧に一致した部分です。存在しないグループ番号を指定すると例外になるため注意が必要です。
2.5 複数一致の返り値を確認する
文章中に条件を満たす文字列が複数ある場合は、findall()やfinditer()を利用します。ただし、丸括弧の有無によってfindall()の返り値が変わるため、実装前に確認しておく必要があります。
グループがない場合は一致文字列の一覧、一つのグループがある場合はグループ部分の一覧、複数グループがある場合は組の一覧になります。複雑な抽出では、位置情報も扱えるfinditer()のほうが分かりやすい場合があります。
import re
text = "A-120、B-350、C-980"
print(re.findall(r"[A-Z]-\d+", text))
print(re.findall(r"([A-Z])-(\d+)", text))
一つ目は一致文字列の一覧、二つ目は英字と数字を分けた組の一覧を返します。返り値の形を理解せずに処理すると、後続コードで意図しない結果になります。
3. 正規表現検索メソッドの違い
Pythonのreモジュールには、似た用途に見える複数の検索メソッドがあります。それぞれ検索範囲、返り値、一致条件が異なるため、目的に応じて選ぶ必要があります。
3.1 searchとmatchの違い
search()は文字列全体を左から調べ、最初に一致した箇所を返します。match()は文字列の先頭位置から一致するかどうかだけを調べるため、途中にある文字列は対象になりません。
先頭から始まる形式を確認する場合はmatch()、文章のどこかに対象が含まれるか確認する場合はsearch()が適しています。両者を同じものとして使うと、途中一致を見逃す可能性があります。
| 比較項目 | search() | match() |
|---|---|---|
| 検索開始位置 | 文字列全体 | 文字列の先頭 |
| 途中の一致 | 検出する | 検出しない |
| 主な用途 | 含有検索 | 先頭形式の確認 |
| 一致しない場合 | None | None |
コード例
import re
text = "注文番号: A-100"
print(re.search(r"A-\d+", text))
print(re.match(r"A-\d+", text))
search()は一致しますが、match()は先頭が「注」であるため一致しません。
3.2 fullmatchとmatchの違い
match()は文字列の先頭部分がパターンに一致すれば結果を返します。文字列の後ろに余分な内容が残っていても、一致部分が先頭にあれば成功します。
fullmatch()は、文字列全体がパターンに一致した場合だけ成功します。入力値の形式確認では、余分な文字を許可しないfullmatch()のほうが意図に合うことが多くなります。
| 比較項目 | fullmatch() | match() |
|---|---|---|
| 一致範囲 | 文字列全体 | 先頭部分 |
| 後続文字 | 許可しない | 許可する |
| 入力形式確認 | 適している | 条件次第 |
| 部分一致 | 不可 | 先頭のみ可能 |
import re
pattern = r"\d{3}-\d{4}"
print(re.fullmatch(pattern, "123-4567"))
print(re.fullmatch(pattern, "123-4567です"))
print(re.match(pattern, "123-4567です"))
郵便番号だけを受け付けたい場合、fullmatch()を使うことで後ろに付いた不要な文字列を拒否できます。
3.3 findallとfinditerの違い
findall()は、パターンに一致した内容を一覧として一度に返します。結果を単純に列挙したい場合は扱いやすく、短い文字列や一致件数が少ない処理に向いています。
finditer()は、一致結果オブジェクトを順番に返す反復可能な値を生成します。開始位置、終了位置、名前付きグループなども利用できるため、詳細な処理や大量データに適しています。
| 比較項目 | findall() | finditer() |
|---|---|---|
| 返り値 | 一覧 | 一致結果の反復 |
| 位置情報 | 直接取得できない | 取得できる |
| 大量データ | 一覧を作成する | 順番に処理できる |
| グループの影響 | 返り値が変化する | 一致結果として保持 |
import re
text = "価格は120円、350円、980円です。"
for result in re.finditer(r"\d+円", text):
print(result.group(), result.span())
一致箇所を強調表示したい場合や、元の文字列上の位置が必要な場合はfinditer()が便利です。
3.4 subとsubnの違い
sub()は、正規表現に一致した部分を指定した文字列へ置換し、置換後の文字列だけを返します。通常の一括置換では、sub()だけで十分です。
subn()は、置換後の文字列と置換回数を組にして返します。置換が実際に行われたか確認したい処理や、処理件数を記録したい業務ではsubn()が役立ちます。
| 比較項目 | sub() | subn() |
|---|---|---|
| 返り値 | 置換後の文字列 | 文字列と置換回数 |
| 件数確認 | 別途必要 | 同時に取得可能 |
| 主な用途 | 通常の置換 | 処理結果の検証 |
| 置換条件 | 同じ | 同じ |
import re
text = "在庫: 12、予約: 8、返品: 2"
replaced_text, count = re.subn(r"\d+", "*", text)
print(replaced_text)
print(count)
この例では数字が3箇所置換されるため、countには3が入ります。
3.5 re.splitとstr.splitの違い
文字列のsplit()は、基本的に一つの固定文字列を区切りとして分割します。空白区切りやカンマ区切りのように、区切り文字が一定の場合は簡潔に使えます。
re.split()では、複数種類の区切り文字や、連続する空白などを正規表現で指定できます。入力データによってカンマ、読点、空白が混在する場合に有効です。
| 比較項目 | re.split() | str.split() |
|---|---|---|
| 区切り条件 | 正規表現 | 固定文字列 |
| 複数の区切り | 一つの条件で対応可能 | 追加処理が必要 |
| 可読性 | 条件が複雑だと低下 | 単純で高い |
| 主な用途 | 不規則な区切り | 一定の区切り |
import re
text = "赤,青、緑 白"
items = re.split(r"[,、\s]+", text)
print(items)
結果は['赤', '青', '緑', '白']になります。区切り文字が一種類なら、通常のsplit()を優先しても問題ありません。
4. メタ文字と文字クラスの使い方
正規表現では、特別な意味を持つ記号をメタ文字と呼びます。これらを組み合わせることで、固定文字だけでは表せない柔軟な検索条件を作成できます。
4.1 ピリオドで任意の1文字を表す
正規表現のピリオド.は、通常、改行以外の任意の1文字に一致します。文字の種類を問わず、ある位置に1文字存在すればよい場合に利用できます。
ただし、範囲が広いため、意図しない記号や空白まで一致することがあります。検索対象を限定できる場合は、文字クラスを利用したほうが安全です。
import re
texts = ["cat", "cut", "c1t", "ct"]
for text in texts:
if re.fullmatch(r"c.t", text):
print(text)
cat、cut、c1tは一致しますが、中央の1文字が存在しないctは一致しません。
4.2 文字クラスで候補を限定する
角括弧[]を使うと、その位置に一致してよい文字の候補を指定できます。[abc]は、a、b、cのいずれか1文字に一致します。
範囲記号を使えば、[0-9]や[A-Z]のように連続した文字を指定できます。許可する文字が明確な場合、.より文字クラスのほうが誤一致を防ぎやすくなります。
import re
text = "A1 B2 C3 D4"
results = re.findall(r"[A-C][1-3]", text)
print(results)
この例では、英字がAからC、数字が1から3の組み合わせだけを抽出します。
4.3 否定文字クラスで除外条件を作る
角括弧の直後に^を置くと、指定した文字以外の1文字に一致します。たとえば[^0-9]は、数字ではない任意の1文字を表します。
否定文字クラスは、「特定の区切り文字が現れるまで取得する」といった処理でよく使われます。ただし、改行を含む可能性や、除外対象が不足していないかを確認する必要があります。
import re
text = "名前:山田,年齢:30"
result = re.search(r"名前:([^,]+)", text)
if result:
print(result.group(1))
[^,]+は、カンマ以外の文字が1文字以上続く部分に一致するため、山田を取得できます。
4.4 数字・空白・単語文字の短縮表現
正規表現には、頻繁に使われる文字クラスの短縮表現があります。\dは数字、\sは空白文字、\wは単語を構成する文字に一致します。
大文字にすると否定条件になります。\Dは数字以外、\Sは空白以外、\Wは単語文字以外を表しますが、Unicode環境では日本語にも一致する場合があるため注意が必要です。
import re
text = "商品ID ABC_120 在庫 45"
print(re.findall(r"\d+", text))
print(re.findall(r"\w+", text))
print(re.findall(r"\S+", text))
\wを「半角英数字だけ」と決めつけず、必要に応じて[A-Za-z0-9_]のように明示することが重要です。
4.5 特別な記号を通常文字として検索する
ピリオド、丸括弧、角括弧、プラス記号などは、正規表現内で特別な意味を持ちます。それらを通常の文字として検索するには、前にバックスラッシュを付けます。
利用者が入力した文字列をそのまま検索条件へ組み込む場合は、re.escape()を使うと安全です。入力に含まれる正規表現記号を自動的にエスケープできます。
import re
keyword = "Python 3.12+"
text = "対応環境はPython 3.12+です。"
pattern = re.escape(keyword)
result = re.search(pattern, text)
if result:
print(result.group())
利用者の入力をそのままパターンとして扱うと、意図せず正規表現として評価される可能性があります。
5. 量指定子による繰り返し回数の指定
量指定子を使うと、直前の文字やグループが何回続くかを指定できます。数字の桁数、文字数の上限、任意項目の有無などを表現する際に欠かせません。
5.1 アスタリスク・プラス・疑問符の違い
*は0回以上、+は1回以上、?は0回または1回の繰り返しを表します。似た記号ですが、対象が存在しなくても一致するかどうかが異なります。
必須項目には+、省略可能な項目には?を使うなど、入力仕様に合わせて選択します。*は空文字にも一致するため、繰り返し処理で意図しない空一致を生まないよう注意が必要です。
| 記号 | 回数 | 空文字との一致 |
|---|---|---|
* | 0回以上 | 一致する |
+ | 1回以上 | 一致しない |
? | 0回または1回 | 一致する |
import re
print(re.fullmatch(r"\d*", ""))
print(re.fullmatch(r"\d+", ""))
print(re.fullmatch(r"\d?", "7"))
数字を必ず1文字以上入力させたい場合は、\d*ではなく\d+を使います。
5.2 波括弧で回数を細かく指定する
{n}はちょうどn回、{n,}はn回以上、{n,m}はn回以上m回以下を表します。桁数や識別番号の長さが決まっている場合に適しています。
文字数制限を正規表現だけに任せるのではなく、業務仕様として最小値と最大値を確認することも重要です。必要以上に広い範囲を許可すると、不正な入力を見逃します。
import re
patterns = [
r"\d{4}",
r"\d{4,}",
r"\d{2,4}",
]
values = ["12", "1234", "123456"]
for pattern in patterns:
print(pattern, [bool(re.fullmatch(pattern, value)) for value in values])
{2,4}のような範囲指定では、下限と上限の両方を満たす文字列だけが一致します。
5.3 貪欲一致が長く取得する理由
通常の量指定子は、条件を満たす範囲で可能な限り長く一致しようとします。この動作は貪欲一致と呼ばれ、.*や.+を使ったときに想定より広い範囲を取得する原因になります。
開始記号と終了記号が同じ文章内に複数存在する場合、最初の開始記号から最後の終了記号まで取得することがあります。対象範囲を明確に限定できる文字クラスがある場合は、そちらを優先します。
import re
text = "<b>Python</b><b>正規表現</b>"
result = re.search(r"<b>.*</b>", text)
if result:
print(result.group())
このパターンは、二つの<b>要素をまとめて取得します。.*が可能な限り長く一致するためです。
5.4 最短一致で範囲を限定する
量指定子の後ろに?を付けると、可能な限り短く一致する最短一致になります。.*?や.+?は、最初に条件を満たした時点で一致範囲を確定します。
ただし、最短一致だけに頼ると、入力形式が崩れたときに予想外の部分へ一致する可能性があります。終了条件が明確なら、否定文字クラスを使う方法も検討します。
import re
text = "<b>Python</b><b>正規表現</b>"
results = re.findall(r"<b>.*?</b>", text)
print(results)
この場合は、二つの要素が別々に取得されます。単純なタグ処理の例としては使えますが、本格的なHTML解析には専用の解析機能が必要です。
5.5 省略可能な文字列を表現する
疑問符?は、直前の1文字またはグループ全体を省略可能にできます。ハイフンの有無、敬称の有無、末尾文字の表記揺れなどを一つのパターンで扱えます。
省略可能な範囲が複数文字なら、丸括弧でグループ化してから?を付けます。どこまでを省略可能にするかが不明確だと、許可したくない形式まで一致するため注意が必要です。
import re
pattern = r"\d{3}-?\d{4}"
values = [
"123-4567",
"1234567",
"12-34567",
]
for value in values:
print(value, bool(re.fullmatch(pattern, value)))
このパターンでは、ハイフンありとハイフンなしの両方を許可し、数字の桁数は固定します。
6. グループ化とキャプチャの活用方法
丸括弧を使うと、複数の要素を一つのまとまりとして扱えます。繰り返し範囲の指定だけでなく、一致した文字列の一部を取り出す用途でも重要です。
6.1 キャプチャグループで一部を取得する
丸括弧()で囲んだ部分はキャプチャグループとなり、一致後に個別取得できます。日付を年、月、日に分けたり、商品番号を分類記号と連番に分けたりする処理で利用できます。
グループ番号は左側の開始丸括弧から順番に1、2、3と割り当てられます。group(0)だけは特別で、正規表現全体に一致した文字列を返します。
import re
text = "2026-07-22"
result = re.fullmatch(r"(\d{4})-(\d{2})-(\d{2})", text)
if result:
print(result.group(0))
print(result.group(1))
print(result.group(2))
print(result.group(3))
位置によるグループ番号は、パターン変更によってずれる可能性があります。複雑なパターンでは名前付きグループが安全です。
6.2 非キャプチャグループを使う理由
丸括弧でまとまりを作りたいものの、後から内容を取得する必要がない場合は(?:...)を利用します。これは非キャプチャグループと呼ばれ、グループ番号に含まれません。
不要なキャプチャを減らすことで、返り値の形を単純に保てます。特にfindall()では、キャプチャグループの有無が結果に影響するため、取得不要なグループは非キャプチャにすることが重要です。
import re
text = "https://example.com http://sample.jp"
results = re.findall(r"(?:https?|ftp)://[^\s]+", text)
print(results)
httpとhttpsの違いをまとめていますが、通信方式だけを別に取得する必要がないため、非キャプチャグループを使用しています。
6.3 名前付きグループで意味を明確にする
(?P<名前>...)を使うと、グループに名前を付けられます。番号ではなく意味のある名前で取得できるため、年月日、氏名、識別番号など複数項目を扱うときに便利です。
名前付きグループは、パターンを修正して丸括弧が増えた場合でも、取得部分の意味を保ちやすくなります。複数人で保守するコードでは、番号指定より読みやすい設計になります。
import re
text = "2026/07/22"
pattern = (
r"(?P<year>\d{4})/"
r"(?P<month>\d{2})/"
r"(?P<day>\d{2})"
)
result = re.fullmatch(pattern, text)
if result:
print(result.group("year"))
print(result.groupdict())
groupdict()を使うと、名前付きグループを辞書として取得できます。
6.4 後方参照で同じ文字列を再利用する
正規表現内で、前に一致したグループと同じ文字列を再度求める場合は後方参照を使います。番号付きグループなら\1、名前付きグループなら(?P=名前)で参照できます。
連続する重複単語や、開始記号と終了記号の対応を確認する際に利用できます。ただし、後方参照が増えるとパターンが読みづらくなり、処理負荷も増える可能性があります。
import re
text = "これは はい はい と答えた例です。"
result = re.search(r"(\S+)\s+\1", text)
if result:
print(result.group())
この例では、空白を挟んで同じ語が連続する部分を検索します。
6.5 選択肢とグループの優先順位
縦線|は、複数のパターンのいずれかに一致させる選択肢を表します。赤|青|緑なら、三つの語のいずれかに一致します。
選択肢の適用範囲を明確にするには丸括弧を使います。cat|dogfoodと(?:cat|dog)foodでは意味が異なるため、前後の文字を含めてどこまでが選択肢なのか確認する必要があります。
import re
pattern = r"(?:株式会社|合同会社)\s*\S+"
texts = [
"株式会社 サンプル",
"合同会社テスト",
"有限会社 例示",
]
for text in texts:
result = re.fullmatch(pattern, text)
print(text, bool(result))
このパターンでは、会社種別を二つの選択肢としてまとめています。
7. 行頭・行末・境界・前後条件の指定
正規表現では、文字自体だけでなく、文字列内の位置や前後関係も条件にできます。位置条件を適切に使うと、不要な部分一致を減らし、検索精度を高められます。
7.1 行頭と行末を指定する
^は行頭、$は行末を表します。文字列全体の形式を確認したり、特定の語で始まる行だけを抽出したりするときに使用します。
通常は文字列全体の先頭と末尾を対象にしますが、複数行モードを有効にすると各行の先頭と末尾に一致します。利用するフラグによって挙動が変わる点に注意が必要です。
import re
values = [
"ERROR E100",
"INFO ERROR E100",
]
for value in values:
print(bool(re.search(r"^ERROR", value)))
二つ目の文字列にはERRORが含まれていますが、行頭ではないため一致しません。
7.2 文字列全体の先頭と末尾を固定する
\Aは文字列全体の先頭、\Zは文字列全体の末尾を表します。^と$に似ていますが、複数行モードの影響を受けません。
複数行の文章を扱いながら、文章全体の開始位置だけを確認したい場合に利用できます。行単位の条件なのか、文字列全体の条件なのかを明確に選択することが重要です。
import re
text = "START\n項目1\n項目2"
print(bool(re.search(r"\ASTART", text)))
print(bool(re.search(r"^項目1", text, re.MULTILINE)))
\Aは文章全体の先頭を確認し、^は複数行モードで各行の先頭を確認します。
7.3 単語境界で部分一致を防ぐ
\bは単語文字と非単語文字の境界を表します。英単語を検索するときに、別の単語の一部へ一致することを防ぐ目的で使われます。
ただし、Pythonの単語文字にはUnicode文字が含まれるため、日本語文章では期待どおりの境界にならない場合があります。日本語の語境界は空白だけでは決まらないため、前後の文字条件を明示する方法が必要です。
import re
text = "cat catalog category"
print(re.findall(r"\bcat\b", text))
結果は独立したcatだけです。catalogやcategoryの先頭部分には一致しません。
7.4 先読みで後続条件だけを確認する
先読みは、後ろに特定の条件が続くことを確認しながら、その後続文字列を一致結果へ含めない機能です。肯定先読みは(?=...)、否定先読みは(?!...)で表します。
単位の直前にある数字だけを取得したり、特定の語が続かない項目だけを検索したりできます。一致位置を消費しないため、通常のグループとは異なる結果になります。
import re
text = "100円、250ドル、300円"
results = re.findall(r"\d+(?=円)", text)
print(results)
結果は100と300です。円は条件確認に使われますが、取得結果には含まれません。
7.5 後読みで直前条件を確認する
後読みは、検索位置の直前に特定の条件が存在するかを確認します。肯定後読みは(?<=...)、否定後読みは(?<!...)で表します。
Python標準の正規表現では、後読みの長さに制約があります。長さが変化する複雑な条件をそのまま指定できない場合があるため、キャプチャグループで全体を検索して必要部分だけ取得する方法も検討します。
import re
text = "価格:1200円 税:120円"
results = re.findall(r"(?<=価格:)\d+", text)
print(results)
価格:の直後にある数字だけを取得します。税:の後ろにある数字は対象になりません。
8. フラグによる検索動作の変更
フラグを利用すると、大文字と小文字の区別、複数行の扱い、ピリオドと改行の関係などを変更できます。パターンを書き換えずに検索動作を調整できる点が特徴です。
8.1 大文字と小文字を区別しない検索
re.IGNORECASEを指定すると、英字の大文字と小文字を区別せずに検索できます。省略形のre.Iも同じ意味です。
商品コードなどで大文字と小文字を区別する必要がある場合は、安易に指定しないようにします。検索用途と識別用途では、大文字小文字の扱いが異なることがあります。
import re
text = "Python python PYTHON"
results = re.findall(r"python", text, re.IGNORECASE)
print(results)
三つの表記がすべて取得されます。返される文字列は、元の文章に記載されていた表記のままです。
8.2 複数行で行頭と行末を扱う
re.MULTILINEを指定すると、^と$が文字列全体だけでなく、各行の先頭と末尾にも一致します。複数行ログから特定の接頭辞を持つ行を抽出する場合に便利です。
省略形はre.Mです。\Aと\Zの意味は変化しないため、行単位と文章全体の位置条件を使い分けられます。
import re
text = """INFO 開始
ERROR 接続失敗
INFO 再試行
ERROR 終了"""
results = re.findall(r"^ERROR.*$", text, re.MULTILINE)
print(results)
行頭がERRORで始まる行だけを一覧として取得できます。
8.3 ピリオドを改行にも一致させる
通常の.は改行文字に一致しません。re.DOTALLを指定すると、改行を含む任意の1文字へ一致するようになります。
複数行にまたがる範囲を抽出するときに便利ですが、.*と組み合わせると非常に広い範囲へ一致する可能性があります。開始条件と終了条件を明確にし、必要に応じて最短一致を使用します。
import re
text = """BEGIN
1行目
2行目
END"""
result = re.search(r"BEGIN.*?END", text, re.DOTALL)
if result:
print(result.group())
DOTALLを指定しない場合、最初の改行で.の一致が止まります。
8.4 複雑なパターンを複数行で書く
re.VERBOSEを指定すると、正規表現パターン内の空白や改行を読みやすさのために利用できます。#から行末までは説明文として扱われます。
複雑な日付、識別番号、ログ形式などを一行で記述すると、修正時に誤りが生じやすくなります。意味ごとに改行し、説明を加えることで保守性を高められます。
import re
date_pattern = re.compile(
r"""
(?P<year>\d{4}) # 年
[-/] # 区切り
(?P<month>\d{1,2}) # 月
[-/] # 区切り
(?P<day>\d{1,2}) # 日
""",
re.VERBOSE,
)
result = date_pattern.fullmatch("2026/7/22")
if result:
print(result.groupdict())
VERBOSE使用時に通常の空白を一致させたい場合は、\sや文字クラス内の空白を使います。
8.5 複数のフラグを組み合わせる
複数のフラグは縦線|で結合できます。大文字小文字を区別せず、複数行の行頭検索を行うなど、用途に合わせた組み合わせが可能です。
フラグが増えると検索動作を見落としやすくなるため、コンパイル時にまとめて記述し、変数名で目的を示す方法が有効です。必要のないフラグまで指定しないことも大切です。
import re
pattern = re.compile(
r"^error.*$",
re.IGNORECASE | re.MULTILINE,
)
text = """INFO start
Error connection failed
ERROR timeout"""
print(pattern.findall(text))
このパターンは、ErrorとERRORの両方で始まる行を取得します。
9. 正規表現による文字列置換
正規表現による置換では、固定文字だけでなく、パターンに一致した複数の表記をまとめて変換できます。文章の正規化、不要文字の削除、機密情報の伏せ字化などに利用できます。
9.1 一致部分を固定文字列へ置換する
re.sub()の第1引数にパターン、第2引数に置換後の文字列、第3引数に対象文字列を指定します。一致箇所が複数ある場合は、通常すべて置換されます。
置換回数を制限したい場合はcountを指定できます。先頭の1件だけを変更したい処理では、count=1を設定します。
import re
text = "商品A 1200円、商品B 980円"
result = re.sub(r"\d+", "****", text)
print(result)
数字部分がすべて****へ変換されます。元の数字の桁数を保ちたい場合は、置換関数を利用します。
9.2 キャプチャした内容を置換後に再利用する
置換文字列では、キャプチャグループに一致した内容を再利用できます。\1や\g<1>で番号付きグループを参照し、\g<名前>で名前付きグループを参照します。
数字が続く置換文字列では、\1の後ろの数字とグループ番号を混同する可能性があります。明確に区切れる\g<1>の記法を使うと安全です。
import re
text = "2026-07-22"
result = re.sub(
r"(\d{4})-(\d{2})-(\d{2})",
r"\1年\2月\3日",
text,
)
print(result)
結果は2026年07月22日になります。元の各部分を保持しながら、区切り表記だけを変更しています。
9.3 関数を使って置換内容を計算する
re.sub()の置換値には関数を指定できます。関数は一致結果オブジェクトを受け取り、置換後の文字列を返します。
数値計算、大小文字変換、桁数に応じた伏せ字など、固定文字列では対応できない置換に適しています。関数内で例外が発生すると置換全体が失敗するため、入力形式を確認する必要があります。
import re
def add_tax(match: re.Match[str]) -> str:
price = int(match.group())
taxed_price = int(price * 1.1)
return str(taxed_price)
text = "本体価格は1000円です。"
result = re.sub(r"\d+", add_tax, text)
print(result)
この例では、数字部分を整数へ変換し、計算後の値で置換しています。
9.4 空白や改行を正規化する
利用者入力や外部データには、半角空白、タブ、改行が不規則に含まれることがあります。\s+を一つの空白へ置換すると、連続する空白文字をまとめられます。
ただし、文章内の改行を保持したい場合に\s+を使うと、改行まで削除されます。対象を半角空白とタブだけに限定するなど、残すべき文字を先に決めることが重要です。
import re
text = "Python 正規表現\t入門"
result = re.sub(r"[ \t]+", " ", text)
print(result)
行構造を保ちながら、横方向の余分な空白だけを一つにできます。
9.5 個人情報を伏せ字にする
正規表現を使えば、電話番号やメールアドレスなど、一定形式の情報を検出して伏せ字にできます。ログ出力や画面表示の前に、不要な個人情報を隠す処理として利用できます。
ただし、一つの正規表現ですべての表記を完全に検出できるとは限りません。入力経路の制限、保存時の暗号化、権限管理などと組み合わせて扱う必要があります。
import re
text = "連絡先は090-1234-5678です。"
masked = re.sub(
r"(\d{3})-\d{4}-(\d{4})",
r"\1-****-\2",
text,
)
print(masked)
結果は090-****-5678になります。業務仕様に応じて、表示可能な桁数を調整します。
10. 分割と複数データの抽出
正規表現は、一致する文字列を探すだけでなく、文章を複数要素へ分割したり、一定形式のデータをまとめて抽出したりできます。外部データの表記揺れに対応する際に有効です。
10.1 複数の区切り文字で分割する
re.split()を使うと、カンマ、読点、空白、スラッシュなどを一つの条件にまとめられます。入力元によって区切り文字が統一されていない場合に便利です。
連続する区切り文字を+でまとめると、空文字が大量に生成されることを防げます。ただし、先頭や末尾に区切り文字がある場合は空文字が残ることがあります。
import re
text = "東京,大阪、名古屋 / 福岡"
items = re.split(r"[,、/\s]+", text)
print(items)
複数の区切り文字を同じ役割として扱い、四つの都市名へ分割できます。
10.2 区切り文字も結果に残す
re.split()のパターンにキャプチャグループを含めると、区切り文字も分割結果へ含まれます。文章を分割した後に、元の記号を使って再構成したい場合に利用できます。
区切り文字を残す必要がなければ、非キャプチャグループまたは通常の文字クラスを使います。キャプチャの有無で返り値が変わるため、意図を明確にして選択します。
import re
text = "赤,青、緑"
parts = re.split(r"([,、])", text)
print(parts)
結果には赤、,、青、、、緑が順番に含まれます。
10.3 複数項目を一度に抽出する
名前付きグループとfinditer()を組み合わせると、文章内の複数データを項目ごとに取得できます。商品番号、数量、価格などが繰り返される形式に適しています。
抽出結果を辞書へ変換すれば、後続の集計処理やデータ保存へ渡しやすくなります。数字は文字列として取得されるため、計算前に整数や小数へ変換します。
import re
text = "商品A:2個:1200円 商品B:5個:800円"
pattern = re.compile(
r"(?P<name>商品[A-Z]):"
r"(?P<count>\d+)個:"
r"(?P<price>\d+)円"
)
for result in pattern.finditer(text):
item = result.groupdict()
item["count"] = int(item["count"])
item["price"] = int(item["price"])
print(item)
抽出と型変換を分けて考えることで、正規表現を必要以上に複雑にせずに済みます。
10.4 重なりのある一致を抽出する
通常のfindall()やfinditer()は、一度一致した範囲の次から検索を続けるため、重なり合う一致は取得しません。たとえば1234から2桁ずつ取り出す場合、通常は12と34になります。
肯定先読みを使い、一致位置を消費しない形にすると、12、23、34のような重なりを取得できます。文字列分析や連続パターンの検出に利用できます。
import re
text = "1234"
results = re.findall(r"(?=(\d{2}))", text)
print(results)
先読み全体は文字を消費せず、内部のキャプチャグループだけが結果として返されます。
10.5 抽出結果を処理工程へ渡す
正規表現による抽出は、処理の最終目的ではなく、その後の変換や保存へつなげる入口として使われます。抽出後に型変換、必須確認、範囲確認などを行うことで、信頼できるデータにできます。
パターンに一致しなかったデータを無視するだけでは、入力不備に気付けない場合があります。成功件数と失敗件数を記録し、失敗した元データを確認できる設計が重要です。
import re
rows = [
"A001,1200",
"B002,850",
"形式不正",
]
pattern = re.compile(r"(?P<code>[A-Z]\d{3}),(?P<price>\d+)")
valid_rows = []
invalid_rows = []
for row in rows:
result = pattern.fullmatch(row)
if result:
valid_rows.append({
"code": result.group("code"),
"price": int(result.group("price")),
})
else:
invalid_rows.append(row)
print(valid_rows)
print(invalid_rows)
不一致データを別に保持することで、正規表現の不足や入力元の問題を追跡できます。
11. 日本語文字列を正規表現で扱う方法
Python 3の文字列はUnicodeとして扱われるため、日本語も正規表現の対象にできます。ただし、漢字、ひらがな、カタカナ、全角記号、結合文字など、日本語特有の表記差を考慮する必要があります。
11.1 Unicode文字列として日本語を検索する
Python 3では、日本語文字列を特別な宣言なしで正規表現へ渡せます。固定語句の検索や、日本語を含む前後条件の指定も通常の文字列と同様に行えます。
ファイルから読み込む場合は、文字コードの指定が重要です。正規表現が正しくても、読み込み時の文字コードが一致していなければ、文字化けや読み込みエラーが発生します。
import re
text = "Pythonで日本語の正規表現を学習します。"
result = re.search(r"日本語の正規表現", text)
if result:
print(result.group())
UTF-8のファイルを読み込む場合は、open()にencoding="utf-8"を指定すると意図が明確になります。
11.2 ひらがな・カタカナ・漢字を指定する
文字範囲を利用すると、ひらがな、カタカナ、基本的な漢字をおおまかに分類できます。日本語だけで構成された部分を検出する処理で使われます。
ただし、Unicodeには一般的な範囲外の漢字、カタカナ拡張、記号、長音符なども存在します。厳密な自然言語判定ではなく、用途に合わせた近似条件として扱う必要があります。
import re
text = "東京でPythonとデータ分析を学ぶ"
hiragana = re.findall(r"[ぁ-ん]+", text)
katakana = re.findall(r"[ァ-ヶー]+", text)
kanji = re.findall(r"[一-龥]+", text)
print(hiragana)
print(katakana)
print(kanji)
長音符ーはカタカナ範囲と別になるため、必要なら文字クラスへ明示的に追加します。
11.3 全角数字と半角数字を区別する
\dはUnicodeの数字にも一致するため、半角数字だけでなく全角数字に一致する場合があります。半角数字だけを許可したい場合は[0-9]を明示します。
逆に、全角と半角の両方を受け付けたい場合は、正規表現の前に文字列を正規化する方法が有効です。検索条件を複雑にするより、入力表記を統一したほうが後続処理も簡単になります。
import re
text = "半角123 全角123"
print(re.findall(r"\d+", text))
print(re.findall(r"[0-9]+", text))
入力検証で\dを使う場合は、どの種類の数字を許可するかを仕様として決める必要があります。
11.4 日本語文章の語境界を扱う
日本語文章は英語のように単語間へ空白を置かないため、\bだけで単語境界を正確に判定することは困難です。特定の語が別の語の一部として含まれる可能性もあります。
前後に許可しない文字を否定先読みや否定後読みで指定する方法がありますが、自然言語としての単語分割が必要なら、形態素解析などの専用処理が適しています。
import re
text = "東京都と京都府を訪問した"
results = re.findall(r"(?<!東)京都", text)
print(results)
この例では、直前が東ではない京都だけを取得します。ただし、用途ごとに前後条件を設計する必要があります。
11.5 正規化してから検索する
同じ見た目の文字でも、全角と半角、結合文字、互換文字など、内部表現が異なる場合があります。表記差を正規表現だけで吸収すると、パターンが長くなり、漏れも発生しやすくなります。
unicodedata.normalize()を使って入力を正規化してから検索すると、条件を単純化できます。ただし、正規化によって文字の見た目や意味が変わる可能性もあるため、元データを保持しておくことが重要です。
import re
import unicodedata
text = "商品番号:ABC-123"
normalized = unicodedata.normalize("NFKC", text)
result = re.search(r"ABC-123", normalized)
print(normalized)
if result:
print(result.group())
正規化後は、全角英数字や全角記号が半角形式へ変換されます。
12. 入力値検証での実践的な使い方
正規表現は入力形式の確認に便利ですが、一致したからといって値が実在するとは限りません。形式確認と内容確認を分け、必要に応じて専用ライブラリや外部データを利用することが大切です。
12.1 メールアドレス形式を確認する
簡易的なメールアドレス確認では、@の前後や末尾部分の存在を正規表現で確認できます。ただし、正式なメールアドレス仕様は複雑であり、一つの短いパターンですべてを正確に判定することは困難です。
実務では、明らかな入力ミスを防ぐ簡易確認と、確認メールの送信を組み合わせます。正規表現だけで利用可能なメールアドレスかどうかを保証することはできません。
import re
pattern = re.compile(
r"[A-Za-z0-9._%+-]+"
r"@[A-Za-z0-9.-]+"
r"\.[A-Za-z]{2,}"
)
values = [
"[email protected]",
"invalid-address",
"name@localhost",
]
for value in values:
print(value, bool(pattern.fullmatch(value)))
業務要件によっては、国際化されたドメイン名や日本語メールアドレスへの対応方針も必要です。
12.2 日本の電話番号形式を確認する
日本の電話番号には、携帯電話、固定電話、フリーダイヤルなど複数の形式があります。単一の正規表現ですべてを厳密に扱おうとすると、条件が非常に複雑になります。
最初に空白や括弧を除去し、数字とハイフンを統一してから、用途ごとのパターンへ分ける方法が現実的です。形式が一致しても、その番号が実際に利用されているかまでは確認できません。
import re
mobile_pattern = re.compile(r"0[789]0-\d{4}-\d{4}")
values = [
"090-1234-5678",
"080-0000-1111",
"03-1234-5678",
]
for value in values:
print(value, bool(mobile_pattern.fullmatch(value)))
携帯電話だけを受け付けるのか、固定電話も受け付けるのかを先に決めてパターンを分けます。
12.3 郵便番号形式を確認する
日本の郵便番号は、通常3桁、ハイフン、4桁の形式で表されます。形式確認だけであれば、比較的短い正規表現で記述できます。
ハイフンなしの入力を許可する場合は、保存前に統一形式へ変換すると扱いやすくなります。実在する郵便番号かどうかを確認するには、郵便番号データとの照合が必要です。
import re
def normalize_postal_code(value: str) -> str | None:
digits = re.sub(r"\D", "", value)
if not re.fullmatch(r"\d{7}", digits):
return None
return f"{digits[:3]}-{digits[3:]}"
print(normalize_postal_code("1000001"))
print(normalize_postal_code("100-0001"))
print(normalize_postal_code("ABC"))
入力形式を確認した後、統一した形式へ変換して保存できます。
12.4 日付形式と実在日を分けて確認する
正規表現では、2026-07-22のような年月日の並び方を確認できます。しかし、2026-02-31のような実在しない日付も、数字の形式だけなら一致してしまいます。
形式確認後にdatetimeへ変換すれば、月ごとの日数やうるう年を含めて判定できます。正規表現は区切りや桁数の確認に限定し、暦の判定は専用機能へ任せます。
import re
from datetime import datetime
def parse_date(value: str) -> datetime | None:
if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", value):
return None
try:
return datetime.strptime(value, "%Y-%m-%d")
except ValueError:
return None
print(parse_date("2026-07-22"))
print(parse_date("2026-02-31"))
形式と意味を二段階で検証することで、誤った日付の登録を防げます。
12.5 パスワード条件を確認する際の注意点
正規表現では、大文字、小文字、数字、記号をそれぞれ含むかどうかを先読みで確認できます。ただし、条件を増やしすぎると、利用者が覚えにくい形式を強制する可能性があります。
文字種類だけでなく、十分な長さ、漏えい済みパスワードの拒否、多要素認証なども重要です。正規表現は一部の条件確認に使い、認証全体の安全性を正規表現だけへ依存させないようにします。
import re
password_pattern = re.compile(
r"(?=.*[a-z])"
r"(?=.*[A-Z])"
r"(?=.*[0-9])"
r".{12,}"
)
values = [
"StrongPassword2026",
"short1A",
"onlylowercase1234",
]
for value in values:
print(bool(password_pattern.fullmatch(value)))
実際のシステムでは、パスワードを平文で保存せず、適切なハッシュ処理を行う必要があります。
13. ログ解析と業務データ処理への応用
ログや業務データには、一定の書式を持つ文字列が大量に含まれます。正規表現を使えば、必要な項目だけを抽出し、集計や監視へ渡す処理を効率化できます。
13.1 アクセスログから項目を抽出する
アクセスログには、接続元、日時、要求方法、経路、状態番号などが一定順序で記録されます。形式が固定されていれば、名前付きグループで各項目を取得できます。
ただし、実際のログ形式には引用符や空欄、追加項目が含まれることがあります。運用中のログ形式を確認し、取得できなかった行を記録する必要があります。
import re
log = '192.0.2.1 - - [22/Jul/2026:10:15:00 +0700] "GET /items HTTP/1.1" 200 512'
pattern = re.compile(
r"(?P<ip>\d{1,3}(?:\.\d{1,3}){3}).*?"
r'"(?P<method>[A-Z]+)\s+'
r"(?P<path>\S+)\s+"
r'[^"]+"\s+'
r"(?P<status>\d{3})\s+"
r"(?P<size>\d+)"
)
result = pattern.fullmatch(log)
if result:
print(result.groupdict())
IPアドレスの数値範囲まで厳密に確認したい場合は、専用のipaddressモジュールと組み合わせます。
13.2 エラーログから重要情報を抽出する
エラーログでは、日時、重要度、エラー番号、説明文などを分離すると集計しやすくなります。正規表現で必要項目を抽出し、エラー番号ごとの発生回数を数える処理が可能です。
説明文には予期しない記号や改行が含まれる場合があります。末尾までを取得する部分は、ログの改行規則と複数行例外の有無を確認して設計します。
import re
from collections import Counter
logs = [
"2026-07-22 ERROR E101 接続失敗",
"2026-07-22 ERROR E101 再接続失敗",
"2026-07-22 ERROR E205 ファイルなし",
]
pattern = re.compile(
r"\d{4}-\d{2}-\d{2}\s+"
r"ERROR\s+"
r"(?P<code>E\d{3})\s+"
r"(?P<message>.+)"
)
counter = Counter()
for log in logs:
result = pattern.fullmatch(log)
if result:
counter[result.group("code")] += 1
print(counter)
抽出失敗を単に無視せず、別の記録へ残すことでログ形式の変更を検出できます。
13.3 キーと値の組を読み取る
name=山田 age=30 status=activeのような形式では、キーと値の組を繰り返し抽出できます。項目順が変わっても、キー名を基準に辞書へ変換可能です。
値に空白や引用符が含まれる場合は、単純な\S+では正しく取得できません。データ仕様に応じて、引用符付きの値と引用符なしの値を分けて処理します。
import re
text = "name=山田 age=30 status=active"
pairs = re.findall(r"(\w+)=([^\s]+)", text)
data = dict(pairs)
print(data)
信頼できない入力をそのまま辞書へ変換する場合は、許可するキー名も確認します。
13.4 CSVを正規表現だけで解析しない
単純なカンマ区切りに見えるデータでも、値の中にカンマ、改行、引用符が含まれる可能性があります。正規表現やsplit(",")だけでは、正式なCSV規則を正しく処理できません。
PythonにはCSV処理用の標準モジュールがあるため、CSV全体の解析にはcsvを利用します。正規表現は、読み取り後の各項目に対する形式確認や部分抽出へ使用します。
import csv
import io
import re
text = '"山田,太郎",30,Tokyo\n'
reader = csv.reader(io.StringIO(text))
row = next(reader)
print(row)
if re.fullmatch(r"\d+", row[1]):
print("年齢形式は有効です")
構造化形式は専用機能で解析し、正規表現は値単位の処理へ限定すると安全です。
13.5 大量ファイルを1行ずつ処理する
大きなログファイルを一度に読み込むと、メモリ使用量が増加します。ファイルを1行ずつ読み取り、コンパイル済みパターンを適用すれば、使用量を抑えながら処理できます。
一致件数だけでなく、行番号や不一致行を記録すると、問題発生時の調査が容易になります。文字コードエラーの扱いも、入力元の仕様に合わせて決める必要があります。
import re
pattern = re.compile(r"ERROR\s+(E\d{3})")
with open("application.log", encoding="utf-8") as file:
for line_number, line in enumerate(file, start=1):
result = pattern.search(line)
if result:
print(line_number, result.group(1))
行末の改行が不要な場合は、rstrip("\n")などで必要な文字だけを除去します。
14. 処理速度と安全性を高める設計
正規表現は便利ですが、書き方によっては入力文字列が少し長くなるだけで処理時間が大幅に増えることがあります。外部入力へ適用する場合は、正しさだけでなく処理負荷も確認する必要があります。
14.1 過剰なバックトラッキングを避ける
曖昧な量指定子を入れ子にすると、正規表現エンジンが多数の組み合わせを試す場合があります。特に(a+)+のような構造は、一致しない長い入力で処理時間が急増する可能性があります。
繰り返し範囲を明確にし、同じ文字列を複数の方法で解釈できる構造を減らすことが重要です。想定される最長入力だけでなく、意図的に一致しない長い入力でも速度を測定します。
import re
unsafe_pattern = re.compile(r"(a+)+$")
text = "a" * 20 + "!"
print(bool(unsafe_pattern.fullmatch(text)))
このようなパターンは入力長によって急激に遅くなる可能性があるため、外部入力には使用を避けます。
14.2 先頭条件で検索範囲を減らす
検索対象の開始条件が決まっている場合は、アンカーや固定文字をパターンの前方へ置くと、不要な探索を減らせます。文字列全体を何度も試すパターンより、開始位置が明確なパターンのほうが効率的です。
ただし、実際に途中検索が必要な処理へ^を付けると、一致を見逃します。速度改善のために意味を変えるのではなく、入力仕様に基づいて適用します。
import re
pattern = re.compile(r"^ERROR\s+E\d{3}")
lines = [
"ERROR E101 connection failed",
"INFO ERROR E101 was reported",
]
for line in lines:
print(bool(pattern.search(line)))
ログ行の先頭に重要度が必ず記録される仕様なら、先頭固定は検索精度の向上にもつながります。
14.3 コンパイル済みパターンを再利用する
同じパターンを大量の文字列へ適用する場合は、re.compile()で一度作成し、繰り返し利用します。コード上でも、どの検索規則を使用しているかが明確になります。
Python内部では一部のパターンが保持されますが、明示的なコンパイルは設計意図を表現する点で有効です。関数呼び出しのたびに長い正規表現を書くより、変更箇所を一つにできます。
import re
order_code_pattern = re.compile(r"[A-Z]{2}-\d{6}")
orders = [
"AB-123456",
"CD-654321",
"INVALID",
]
valid_orders = [
order
for order in orders
if order_code_pattern.fullmatch(order)
]
print(valid_orders)
パターンを定数として管理すると、テストコードからも同じ条件を参照できます。
14.4 入力長を制限して処理負荷を抑える
Python標準のreでは、一般的な検索関数へ処理時間の上限を直接指定する方法がありません。そのため、信頼できない外部入力へ複雑なパターンを適用する場合は、入力長や処理環境を制限する必要があります。
フォーム入力なら最大文字数を設定し、ログ処理なら1行の上限を決めるなど、正規表現を実行する前に防御できます。極端に長い入力を拒否するだけでも、想定外の負荷を抑えられます。
import re
MAX_LENGTH = 5000
pattern = re.compile(r"[A-Za-z0-9._-]+")
def validate_identifier(value: str) -> bool:
if len(value) > MAX_LENGTH:
return False
return pattern.fullmatch(value) is not None
入力長制限は、正規表現だけでなく、保存容量や画面表示の安全性にも役立ちます。
14.5 読みやすさを優先して分割する
一つの巨大な正規表現へすべての条件を詰め込むと、修正時の影響範囲が分かりにくくなります。前処理、形式確認、意味確認を複数段階へ分けたほうが、安全に変更できます。
正規表現でしか表現できない部分だけに使用し、単純な比較や型変換はPythonコードへ任せます。処理行数が少ないことより、第三者が条件を説明できることを優先します。
import re
code_pattern = re.compile(r"[A-Z]{2}-\d{4}")
def validate_product_code(value: str) -> bool:
normalized = value.strip().upper()
if len(normalized) != 7:
return False
return code_pattern.fullmatch(normalized) is not None
文字列の正規化、長さ確認、形式確認を分けることで、各条件の役割が明確になります。
15. 正規表現のテストと保守方法
正規表現は短い記述に多くの条件が含まれるため、変更による影響を目視だけで判断するのは危険です。正常な入力、不正な入力、境界値をテストし、意図した条件が維持されていることを確認します。
15.1 単体テストで一致結果を固定する
正規表現を関数として扱い、期待する入力と結果を単体テストへ記述すると、将来の変更による誤判定を検出できます。成功例だけでなく、必ず失敗例も含めます。
入力形式が追加されたときは、パターンを変更する前にテスト例を追加します。期待する仕様を先にコード化することで、修正後の結果を客観的に判断できます。
コード例
import reimport unittest
postal_code_pattern = re.compile(r"\d{3}-\d{4}")
class PostalCodeTest(unittest.TestCase): def test_valid_postal_code(self) -> None: self.assertIsNotNone( postal_code_pattern.fullmatch("100-0001") )
def test_invalid_postal_code(self) -> None: self.assertIsNone( postal_code_pattern.fullmatch("10-00001") )
if __name__ == "__main__": unittest.main()
テスト名に確認内容を記載すると、失敗時にどの条件が崩れたかを把握しやすくなります。
15.2 境界値と異常値を確認する
文字数に上限と下限がある場合は、最小値の直前、最小値、最大値、最大値の直後を確認します。通常の入力だけを試しても、量指定子の誤りは発見しにくいためです。
空文字、改行、全角文字、前後の空白、非常に長い文字列も重要な確認対象です。利用者が入力できる値だけでなく、外部連携から届く異常値も想定します。
import re
pattern = re.compile(r"[A-Z0-9]{4,8}")
test_values = [ "", "ABC", "ABCD", "ABCDEFGH", "ABCDEFGHI", "ABCD", "AB C",]
for value in test_values: print(repr(value), bool(pattern.fullmatch(value)))
半角英数字だけを許可する仕様なら、見た目が似ている全角文字を失敗例へ含めます。
15.3 VERBOSEと説明文で意図を残す
複雑な正規表現はre.VERBOSEを使って複数行へ分け、各部分の役割を説明します。ただし、説明文が実際の仕様とずれると誤解を生むため、パターン変更時に同時更新します。
変数名にも目的を含め、pattern1のような曖昧な名前を避けます。どのデータに対して何を確認するパターンなのかが、呼び出し側から理解できる状態が理想です。
import re
japanese_mobile_phone_pattern = re.compile(
r"""
0[789]0 # 携帯電話の先頭番号
-
[0-9]{4} # 中央4桁
-
[0-9]{4} # 末尾4桁
""",
re.VERBOSE,
)
パターンの出典となる業務仕様がある場合は、コードコメントや設計文書から参照できるようにします。
15.4 正規表現を補助関数へ分離する
正規表現を業務処理の途中へ直接書くと、同じ条件が複数箇所へ重複する可能性があります。入力の正規化と形式確認を補助関数へまとめれば、呼び出し側を単純にできます。
補助関数は、真偽値を返すだけでなく、正規化済みの値やエラー理由を返す設計も可能です。画面表示、記録、外部連携で必要な情報を考えて返り値を決めます。
import refrom dataclasses import dataclass
product_code_pattern = re.compile(r"[A-Z]{2}-\d{4}")
@dataclassclass ValidationResult: valid: bool value: str | None message: str | None
def validate_product_code(value: str) -> ValidationResult: normalized = value.strip().upper()
if product_code_pattern.fullmatch(normalized): return ValidationResult(True, normalized, None)
return ValidationResult( False, None, "商品番号の形式が正しくありません。", )
判定結果と利用者向けの説明を分けて返すと、業務画面でも扱いやすくなります。
15.5 変更前後の動作を比較する
正規表現を変更するときは、既存テストに加えて、変更対象となる入力例を用意します。新しい形式を許可した結果、以前は拒否していた不正な形式まで通過しないかを確認します。
正規表現の変更理由、許可する形式、拒否する形式を記録しておくと、後からパターンを短くしたり広げたりする際の判断材料になります。変更後は処理速度も測り、長い不一致入力で極端に遅くならないか確認します。
import reimport time
pattern = re.compile(r"[A-Z]{2}-\d{4}")
samples = [ "AB-1234", "ab-1234", "AB1234", "AB-12345", "A" * 10000,]
start = time.perf_counter()
for sample in samples: result = pattern.fullmatch(sample) print(repr(sample[:20]), bool(result))
elapsed = time.perf_counter() - startprint(f"処理時間: {elapsed:.6f}秒")
一致結果だけでなく処理時間も確認することで、機能面と性能面の両方から変更を検証できます。
おわりに
Pythonの正規表現は、文字列の検索、抽出、置換、分割、入力形式の確認などを一つの仕組みで扱える強力な機能です。re.search()、re.fullmatch()、re.findall()、re.sub()などの違いを理解し、処理目的に合った関数を選ぶことが重要です。
実務では、正規表現だけで処理を完結させようとせず、文字列の正規化、型変換、範囲確認、専用ライブラリによる意味確認を組み合わせます。メールアドレス、日付、電話番号、CSV、HTMLなどは、形式が一致しただけではデータの正しさや実在性を保証できません。
また、複雑なパターンは読みやすさと処理速度の両方に影響します。名前付きグループ、非キャプチャグループ、re.VERBOSE、コンパイル済みパターン、単体テストを活用し、正常値だけでなく異常値や長い入力でも安全に動作する正規表現を設計することが大切です。
EN
JP
KR