「ログファイルからエラーの行だけ抜き出したい…」「メモに散らばった日付をまとめて同じ形式にそろえたい…」

こんなとき、split() や in だけで頑張ろうとすると、if文がどんどん増えてしまいますよね。そこで役に立つのが正規表現(せいきひょうげん)です。

この記事では、Python標準の reモジュール を使って、電話番号・日付・メールアドレス・ログの行を「パターンで」抜き出す方法を、実際に動かした結果つきで解説します。

  • 対象:if文・for文・関数がひととおり書ける初心者〜中級者
  • 難易度:★★☆(記号が多いだけで、考え方はシンプルです)
  • 動作確認:Python 3.13(追加インストール不要)

読み終わるころには、「正規表現=呪文」が「正規表現=便利な検索条件」に変わっているはずです。一緒に見ていきましょう!🚀

正規表現とは?イメージは「あいまい検索の条件書き」

text pattern search
text pattern search / Photo by Miguel Á. Padriñán via Pexels

正規表現は、「こういう形の文字列」をひとことで表す書き方です。イメージとしては、Excelの検索に「数字が4つ続いて、スラッシュがあって…」という形の条件を渡せる感じです。

まずはよく使う記号だけ押さえましょう。全部覚える必要はなく、この表を手元に置いておけば十分です😊

記号 意味 例
\d 数字1文字 \d\d → 「42」
\w 英数字・アンダースコア(日本語の文字も含む) \w+ → 「db01」
\s 空白(スペース・タブ・改行) a\sb → 「a b」
. 改行以外の任意の1文字 a.c → 「abc」「a-c」
+ / * / ? 1回以上 / 0回以上 / 0か1回 -? → ハイフンがあってもなくてもOK
{m,n} m回以上n回以下 \d{2,4} → 数字2〜4桁
^ / $ 行の先頭 / 行の末尾 ^ERROR → ERRORで始まる行
( ) グループ(あとで取り出せる) (\d{4})/ → 年だけ取り出す

STEP1:search・match・fullmatch の違いを知る

最初のつまずきポイントがここです。reモジュールには「探す」関数が3つあって、どこを見るかが違います。文章の中から電話番号を探してみましょう。

import re

text = "お問い合わせは 03-1234-5678 まで(受付 9:00〜18:00)"

# 電話番号のパターン:数字2〜4桁-数字2〜4桁-数字4桁
pattern = r"\d{2,4}-\d{2,4}-\d{4}"

m = re.search(pattern, text)  # 文字列のどこかにあれば見つける
if m:
    print("見つかった:", m.group())
    print("位置:", m.start(), "〜", m.end())

# match は「先頭から」しか見ない
print("match:", re.match(pattern, text))
# fullmatch は「文字列全体」がパターンに一致するか
print("fullmatch:", re.fullmatch(pattern, "03-1234-5678"))

実行すると、search は 03-1234-5678 を見つけて位置「8 〜 20」を返しました。一方で match の結果は None です。

ここが重要です👇

  • re.search():文字列のどこかにあればOK(いちばんよく使う)
  • re.match():先頭から一致しないとダメ。文章は「お問い合わせは…」で始まるので None
  • re.fullmatch():文字列全体がパターン通りか。入力チェックに使う
  • 見つかるとマッチオブジェクトが返り、.group() で中身、.start()・.end() で位置が取れる

パターンの前の r"..." はraw文字列です。理由はSTEP4で「実際に失敗する例」と一緒に説明しますね。

STEP2:findall・finditer でまとめて抜き出す

1つ見つけるだけでなく、全部抜き出したいときは findall と finditer を使います。メモの中から「年/月/日」の日付を集めてみましょう。

import re

memo = """10/3 打ち合わせ(2026/10/03)
請求書の締めは2026/10/31、入金予定は2026/11/30です。
来年の予定:2027/1/15 キックオフ"""

# ( ) で囲んだ部分が「グループ」になる
pattern = r"(\d{4})/(\d{1,2})/(\d{1,2})"

# findall:グループがあると、タプルのリストが返る
print(re.findall(pattern, memo))

# finditer:マッチオブジェクトを1つずつ取り出せる
for m in re.finditer(pattern, memo):
    year, month, day = m.groups()
    print(f"{m.group()} → {year}年{int(month)}月{int(day)}日")

findall の結果は [('2026', '10', '03'), ('2026', '10', '31'), ('2026', '11', '30'), ('2027', '1', '15')] でした。

これ、実行せずに気づけましたか? 1行目の先頭にある「10/3」は抜き出されていません。パターンが「数字4桁/」から始まるので、年のない日付は対象外なんです。正規表現は書いた条件どおりにしか動かないので、「何を拾って何を拾わないか」を意識するのがコツです。

ポイントをまとめるとこんな感じです。

  • パターンに ( ) があると、findall はグループの中身のタプルを返す(日付全体ではない)
  • 日付全体もグループも欲しいなら finditer を使い、m.group() と m.groups() を使い分ける
  • 取り出した値は文字列なので、計算や「03→3」の変換には int() が必要

STEP3:re.sub で日付の形式を一括変換する

抜き出せたら、次は置き換えです。「2026/10/7」と「2026/10/14」のように桁数がバラバラな日付を、「2026-10-07」の形にそろえます。

import re

lines = [
    "納品日:2026/10/7",
    "検収日:2026/10/14",
    "支払日:2026/11/30",
]

# (?P<名前>...) で名前付きグループ
pattern = re.compile(r"(?P<y>\d{4})/(?P<m>\d{1,2})/(?P<d>\d{1,2})")

def to_iso(m):
    # 月・日を2桁にそろえて「2026-10-07」の形にする
    return f"{m['y']}-{int(m['m']):02d}-{int(m['d']):02d}"

for line in lines:
    print(pattern.sub(to_iso, line))

# 置き換えるだけなら文字列でもOK(\g<名前> でグループを参照)
print(pattern.sub(r"\g<y>年\g<m>月\g<d>日", "締切 2026/10/31"))

実行結果は 納品日:2026-10-07・検収日:2026-10-14・支払日:2026-11-30、最後の行は 締切 2026年10月31日 になりました。

ここが重要です👇

  • (?P<y>...) の名前付きグループにすると、m['y'] のように名前で取り出せて読みやすい
  • re.sub の置き換え先に関数を渡すと、マッチごとに関数が呼ばれ、戻り値で置き換わる(ゼロ埋めのような加工ができる)
  • 単純な並べ替えなら置き換え先は文字列でOK。\g<名前> でグループを参照できる
  • 同じパターンを何度も使うときは re.compile() しておくと、pattern.sub() のように書けてスッキリ

STEP4:初心者がハマる2大トラブル(貪欲マッチとraw文字列)

正規表現で「なんか思った結果にならない…」というとき、原因の多くはこの2つです。実際に失敗させてみましょう。

import re

html = '<p>こんにちは</p><p>正規表現</p>'

# 貪欲(.*):できるだけ長くマッチしようとする
print("貪欲  :", re.findall(r"<p>(.*)</p>", html))

# 非貪欲(.*?):できるだけ短くマッチする
print("非貪欲:", re.findall(r"<p>(.*?)</p>", html))

# raw文字列を付け忘れると…
print(len("\b"), len(r"\b"))
print(re.findall("\bcat\b", "cat catalog cat"))   # 何も見つからない
print(re.findall(r"\bcat\b", "cat catalog cat"))  # 単語の「cat」だけ
実行結果:貪欲マッチとraw文字列の違い
実行結果:貪欲マッチとraw文字列の違い

結果を見ると違いが一目でわかります。

  • 貪欲マッチ:.* は「できるだけ長く」マッチするので、最初の <p> から最後の </p> までを1つとして取ってしまう
  • ? を付けた .*?(非貪欲)なら「できるだけ短く」なり、「こんにちは」「正規表現」を別々に取れる
  • "\b" は長さ1、r"\b" は長さ2。普通の文字列だと \b がPythonの「バックスペース文字」に変換されてしまう
  • そのため "\bcat\b" では何も見つからず(空リスト)、r"\bcat\b" なら単語としての「cat」だけ(catalogは除外)が取れる

⚠️ 正規表現のパターンには必ず r を付ける、と決めてしまうのがいちばん安全です。なお、HTMLの解析を本格的にやるなら正規表現ではなくBeautifulSoupなどのパーサーを使うのがおすすめです。ここではあくまで貪欲・非貪欲の違いを見るための例です。

STEP5:実践① ログファイルからエラーを集計する

ここからは実務っぽい使い方です。サーバーのログから ERRORの行だけを抜き出し、レベル別の件数と「エラーが多いホスト」を集計してみます。

import re
from collections import Counter

log = """2026-10-07 09:00:01 INFO  起動しました
2026-10-07 09:00:05 WARNING ディスク残量 18%
2026-10-07 09:01:12 ERROR 接続失敗 host=db01 code=504
2026-10-07 09:02:40 INFO  ユーザー user_id=1024 がログイン
2026-10-07 09:03:02 ERROR 接続失敗 host=db02 code=503
2026-10-07 09:05:33 ERROR タイムアウト host=db01 code=504"""

# re.VERBOSE を使うと、パターンに改行やコメントを書ける
line_re = re.compile(r"""
    ^(?P<date>\d{4}-\d{2}-\d{2})\s     # 日付
    (?P<time>\d{2}:\d{2}:\d{2})\s      # 時刻
    (?P<level>INFO|WARNING|ERROR)\s+   # ログレベル
    (?P<msg>.*)$                       # 残りはメッセージ
""", re.VERBOSE | re.MULTILINE)

levels = Counter()
error_hosts = Counter()

for m in line_re.finditer(log):
    levels[m["level"]] += 1
    if m["level"] == "ERROR":
        host = re.search(r"host=(\w+)", m["msg"])
        if host:
            error_hosts[host.group(1)] += 1
        print(f"[{m['time']}] {m['msg']}")

print("レベル別:", dict(levels))
print("エラーの多いホスト:", error_hosts.most_common())
実行結果:ログからERRORを抜き出して集計
実行結果:ログからERRORを抜き出して集計

ERRORの3行が時刻つきで表示され、レベル別は INFO: 2, WARNING: 1, ERROR: 3、エラーの多いホストは db01 が2件という結果になりました。

ポイントをまとめるとこんな感じです。

  • re.VERBOSE:パターンの中に改行・空白・# コメントを書けるので、長いパターンでも読みやすい(パターン内の空白は無視されるので、空白にマッチさせたいときは \s を使う)
  • re.MULTILINE:^ と $ が「文字列全体」ではなく各行の先頭・末尾にマッチするようになる
  • フラグは | でつなげて複数指定できる
  • 「1行を分解 → メッセージの中をもう一度 re.search」と2段階にすると、パターンが複雑になりすぎない
  • 集計は collections.Counter と組み合わせると数行で書ける

実際のログファイルを読むなら、Path("app.log").read_text(encoding="utf-8") で読み込んだ文字列を log の代わりに渡すだけです。

STEP6:実践② 郵便番号・メールアドレスの入力チェック

最後は、フォームの入力チェックです。「全体がその形になっているか」を見たいので、STEP1で出てきた fullmatch の出番です。

import re

POSTAL = re.compile(r"\d{3}-?\d{4}")              # 郵便番号(ハイフンあり・なし)
EMAIL = re.compile(r"[\w.+-]+@[\w-]+(\.[\w-]+)+")  # メールのざっくりチェック

def check(label, pattern, value):
    ok = pattern.fullmatch(value) is not None
    print(f"{label}: {value} → {'OK' if ok else 'NG'}")

for v in ["100-0001", "1000001", "100-00011", "100-0001"]:
    check("郵便番号", POSTAL, v)

for v in ["taro@example.com", "hanako.y+blog@mail.example.jp", "taro@example", "taro example.com"]:
    check("メール", EMAIL, v)

# 全角数字もマッチさせたくないなら ASCII フラグ
POSTAL_ASCII = re.compile(r"\d{3}-?\d{4}", re.ASCII)
print("ASCIIフラグ:", POSTAL_ASCII.fullmatch("100-0001"))

実行してみると、意外な結果が1つ混ざっていました。

  • 100-0001・1000001 はOK、桁が多い 100-00011 はNG(search だと一部が一致してOKになってしまうところ)
  • メールは taro@example.com・hanako.y+blog@mail.example.jp がOK、ドメインに「.」がない taro@example と @ がない taro example.com はNG
  • ⚠️ 全角の「100-0001」もOKになった。Python 3の \d は、標準で全角数字などのUnicodeの数字にもマッチするため
  • re.ASCII フラグを付けると \d が半角の0〜9だけになり、全角はNG(None)になった

この「全角数字も通ってしまう」問題は、知らないとデータベースに全角の郵便番号が入ってしまう原因になります。半角だけ許したい場面では re.ASCII を付けるか、[0-9] と書くのが確実です。

また、メールアドレスの正規表現はあくまで「明らかな入力ミスをはじく」ためのざっくりチェックです。本当に届くアドレスかどうかは、確認メールを送って確かめるのが実務の定番です。

よく使う関数・フラグ早見表

やりたいこと 使うもの
どこかに含まれるか調べる re.search()
入力全体が形式どおりかチェック re.fullmatch()
全部抜き出す(リストでほしい) re.findall()
全部抜き出す(位置やグループも使う) re.finditer()
置き換える re.sub()
同じパターンを何度も使う re.compile()
長いパターンにコメントを書く re.VERBOSE
^・$ を行ごとに効かせる re.MULTILINE
\d・\w を半角だけにする re.ASCII
大文字・小文字を区別しない re.IGNORECASE

より詳しい仕様は、Python公式ドキュメントのre — 正規表現操作にまとまっています。

まとめ

今回は、Pythonの正規表現(reモジュール)を、実際に動かしながら見てきました。

  • 正規表現は「こういう形の文字列」をひとことで表す検索条件
  • search(どこか)・match(先頭)・fullmatch(全体)の違いを押さえる
  • findall/finditer で抜き出し、re.sub に関数を渡せば加工しながら置き換えられる
  • パターンには必ず r を付け、.* の貪欲マッチに注意する
  • \d は全角数字にもマッチする。半角だけなら re.ASCII か [0-9]

正規表現は、最初は記号の多さにひるみますが、「数字が何桁」「ここをグループで取り出す」と一つずつ読めば、ちゃんと意味のある条件になっています。手元のメモやログで、ぜひ試してみてください😊

\ 読んだら、次は自分で作ってみよう /

筆者が運営する無料の入門コース「作って学ぶPythonプログラミング入門」では、こうした基本文法を、割り勘計算機・おみくじ・数当てゲームなどを作りながら身につけます。各章末の定着ドリル(全42問)で、自分の手で書く練習まで。最初の4レッスンは登録なしで読めます。

→ 無料の入門コース(全7章52レッスン)を見てみる

📚 関連商品・おすすめ書籍

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

もしも

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

初心者に定番のPython入門書

Amazonで見る
徹底攻略! 電子工作&プログラミング Arduinoで学ぶ電子工作完全ガイド

もしも

徹底攻略! 電子工作&プログラミング Arduinoで学ぶ電子工作完全ガイド

電子工作とプログラミングを同時に学べる

Amazonで見る
実践Claude Code入門―現場で活用するためのAIコーディングの思考法

もしも

実践Claude Code入門―現場で活用するためのAIコーディングの思考法

AIコーディングの現場活用法を学ぶ一冊

Amazonで見る

※本記事にはアフィリエイトリンクが含まれます。

ABOUT ME
やまちゃん
これまで学生と社会人を合わせて5000人以上にプログラミング学習を指導。 ゼロからイチをわかりやすく解説する専門家として活動しており、本業ではArduinoを用いたIoT開発とロボットプログラミングが専門。 Pythonを用いたアプリ開発、ウェブアプリケーションの開発で業務の効率化をサポートしています。