「ログの中からエラー行だけを抜き出したい」「入力されたメールアドレスの形式をチェックしたい」「電話番号をまとめてマスキングしたい」——プログラミングを続けていると、こうした文字列のパターン検索に必ずぶつかります。
ひとつずつ if 文で条件を書いていくと、コードはあっという間に読めないほど長くなってしまいます。そんなときに絶大な威力を発揮するのが正規表現(regular expression)と、Python標準の re モジュールです。
正規表現は「記号だらけで難しそう」と敬遠されがちですが、実際によく使うのはほんの一握りのパターンと4つの関数だけです。この記事では、現役SEの視点から、re.search・re.match・re.findall・re.sub の使い方を、実際に動くコードと実行結果を示しながら初心者向けに徹底解説します。読み終えるころには、実務で使うレベルの文字列処理が書けるようになりますよ。
コンテンツ
正規表現とは?何ができるのか
正規表現とは、「文字列のパターンを、記号を使って表現するための書き方(ルール)」のことです。「数字が3つ並んで、ハイフンがあって、また数字が4つ並ぶ」といったあいまいな条件を、たった一行で表現できます。
たとえば「電話番号らしき文字列」は、正規表現ではこう書けます。
\d{2,4}-\d{4}-\d{4}これだけで「2〜4桁の数字 – 4桁の数字 – 4桁の数字」という意味になります。if 文で同じことを書こうとしたら何十行にもなるところが、1行で済んでしまうわけです。
正規表現が活躍する代表的な場面は次のとおりです。
- 検索:大量のログから ERROR 行だけを抜き出す
- 検証(バリデーション):メールアドレスや郵便番号の形式チェック
- 抽出:文章の中から日付・URL・金額だけを取り出す
- 置換:個人情報をマスキングする、表記ゆれを統一する
- 分割:区切り文字が複数種類ある文字列をきれいに分ける
なお、単純な「この文字が含まれるか」「この文字で始まるか」程度の判定であれば、正規表現を持ち出さなくても標準の文字列メソッドで十分です。基本の使い分けについてはPythonの文字列メソッドの使い方を徹底解説した記事もあわせて読んでみてください。「文字列メソッドで書けるなら、まずはそちらを使う」のが読みやすいコードの鉄則です。
reモジュールの基本|importとraw文字列
Pythonで正規表現を使うには、標準ライブラリの re モジュールを import するだけです。追加インストールは一切不要なので、Pythonさえ入っていればすぐに試せます。
import re
text = "お問い合わせ先は support@example.com です"
# パターンにマッチする部分を探す
result = re.search(r"[\w.-]+@[\w.-]+", text)
print(result.group())
# 実行結果: support@example.comたった3行で、文章の中からメールアドレスを取り出せました。
パターンの前には必ず r を付ける(raw文字列)
先ほどのコードで、パターンが r"..." という形になっていたことに気づきましたか? この r はraw文字列(ロー文字列)を表すもので、正規表現を書くときのお約束です。
Pythonの文字列では \n が改行、\t がタブというように、バックスラッシュに特別な意味があります。一方、正規表現でも \d(数字)や \w(英数字)のようにバックスラッシュを多用します。この2つが衝突すると、バックスラッシュを二重に書く羽目になり、とても読みにくくなります。
import re
# raw文字列を使わない書き方(バックスラッシュが増えて読みにくい)
pattern_ng = "\\d{3}-\\d{4}"
# raw文字列を使った書き方(見たままでOK・こちらが推奨)
pattern_ok = r"\d{3}-\d{4}"
print(re.search(pattern_ok, "郵便番号は 100-0001 です").group())
# 実行結果: 100-0001正規表現のパターンは、何も考えず先頭に r を付けると覚えてしまいましょう。これだけでハマりどころを1つ回避できます。
まず覚えるべき4つの関数
re モジュールにはたくさんの関数がありますが、実務でよく使うのは次の4つに集約されます。まずはこれだけ押さえれば十分です。
re.search()…文字列全体からパターンを探す(最初の1件)re.match()…文字列の先頭がパターンと一致するか調べるre.findall()…マッチする箇所をすべてリストで取得するre.sub()…マッチした部分を置換する
re.search()|どこかに含まれるか探す
もっとも出番が多いのが re.search() です。文字列全体を走査して、最初に見つかった箇所をマッチオブジェクトとして返します。見つからなかった場合は None を返します。
import re
text = "注文番号: A-2026-0717 を確認しました"
m = re.search(r"[A-Z]-\d{4}-\d{4}", text)
if m:
print("見つかりました:", m.group()) # マッチした文字列
print("開始位置:", m.start()) # 開始インデックス
print("終了位置:", m.end()) # 終了インデックス
else:
print("見つかりませんでした")
# 実行結果:
# 見つかりました: A-2026-0717
# 開始位置: 6
# 終了位置: 17ポイントは必ず if m: でマッチしたか確認してから .group() を呼ぶことです。マッチしなかったときは None が返るため、いきなり .group() を呼ぶと AttributeError になってしまいます。ここは初心者が最初につまずく定番ポイントです。
re.match()|先頭だけを調べる
re.match() は名前から「一致するか調べる関数」と誤解されやすいのですが、実際は文字列の先頭からマッチするかだけを見ます。途中にあってもマッチしません。
import re
text = "Python3 is fun"
print(re.match(r"Python", text)) # 先頭にあるのでマッチする
print(re.match(r"is", text)) # 先頭ではないのでマッチしない
print(re.search(r"is", text)) # searchなら途中でも見つかる
# 実行結果:
# <re.Match object; span=(0, 6), match='Python'>
# None
# <re.Match object; span=(8, 10), match='is'>迷ったら re.search() を使えば間違いありません。re.match() は「入力値が指定した形式で始まっているか」を検証したいときに使う、と覚えておきましょう。
re.findall()|該当する箇所をすべて集める
マッチする箇所をまとめてリストで欲しいときは re.findall() です。戻り値がそのままリストなので、そのまま for 文で回せて便利です。
import re
text = "本社は 03-1234-5678、大阪支社は 06-2222-3333、携帯は 090-8765-4321 です"
numbers = re.findall(r"\d{2,4}-\d{4}-\d{4}", text)
print(numbers)
# 実行結果: ['03-1234-5678', '06-2222-3333', '090-8765-4321']
for i, num in enumerate(numbers, 1):
print(f"{i}件目: {num}")
# 実行結果:
# 1件目: 03-1234-5678
# 2件目: 06-2222-3333
# 3件目: 090-8765-4321マッチが1件もなければ、空のリスト [] が返ります。None ではないので、if numbers: でそのまま判定できるのが findall() の気楽なところです。
re.sub()|パターンにマッチした部分を置換する
個人情報のマスキングや表記ゆれの統一で大活躍するのが re.sub() です。str.replace() は決まった文字列しか置換できませんが、re.sub() ならパターンで置換できます。
import re
text = "連絡先: 090-1234-5678 / 080-9999-0000"
# 電話番号をまとめてマスキングする
masked = re.sub(r"\d{3}-\d{4}-\d{4}", "***-****-****", text)
print(masked)
# 実行結果: 連絡先: ***-****-**** / ***-****-****
# 連続する空白を1つにまとめる(テキスト整形の定番)
messy = "Python は とても 便利です"
clean = re.sub(r"\s+", " ", messy)
print(clean)
# 実行結果: Python は とても 便利です2つ目の「連続する空白を1つにまとめる」は、スクレイピングやCSV処理で拾ってきた汚いテキストを整形するときに本当によく使うテクニックです。ぜひ覚えておいてください。
re.split()|複数の区切り文字で分割する
おまけとして re.split() も紹介します。区切り文字が1種類だけなら標準の split() で十分ですが、「カンマでも、セミコロンでも、空白でも区切りたい」という場合は正規表現の出番です。
import re
text = "りんご, みかん;ぶどう バナナ"
fruits = re.split(r"[,;\s]+", text)
print(fruits)
# 実行結果: ['りんご', 'みかん', 'ぶどう', 'バナナ']区切り文字が1種類の場合の基本的な分割・結合については、Pythonで文字列を分割・結合する方法(split・join)の解説記事で詳しく扱っています。取り出した後の一部分だけを切り出したいときはスライスの使い方を解説した記事も参考になりますよ。
ここまでで基本の4関数はマスターできました。ただ、正規表現は「読んで分かる」と「自分で書ける」の間に、それなりの壁があるのも事実です。独学で手が止まってしまう、質問できる相手がいなくて時間だけが過ぎていく——そんな状態が続くようなら、学習環境そのものを見直すのも立派な選択肢です。プログラミングスクール5選を比較した記事では、無料体験のあるスクールを実際に体験したうえで比較しているので、独学と併用する判断材料としてのぞいてみてください。
グループ化|必要な部分だけを取り出す
正規表現の真価はグループ化で発揮されます。パターンの一部を () で囲むと、その部分だけを個別に取り出せるようになります。ログ解析ではこれが必須テクニックです。
import re
log = "2026-07-17 09:00:00 ERROR ディスク容量が不足しています"
m = re.search(r"(\d{4})-(\d{2})-(\d{2}) (\d{2}:\d{2}:\d{2}) (\w+)", log)
print(m.group(0)) # パターン全体
print(m.group(1)) # 1つ目の()
print(m.group(5)) # 5つ目の()
print(m.groups()) # すべてのグループをタプルで取得
# 実行結果:
# 2026-07-17 09:00:00 ERROR
# 2026
# ERROR
# ('2026', '07', '17', '09:00:00', 'ERROR')group(0) がマッチ全体、group(1) 以降が左のカッコから順番に対応します。0始まりではなく1始まりである点に注意してください。
名前付きグループでコードを読みやすくする
group(3) のような番号指定は、パターンを修正した瞬間に番号がずれてバグの温床になります。実務では (?P<名前>...) という名前付きグループを使うのがおすすめです。
import re
log = "2026-07-17 09:00:00 ERROR ディスク容量が不足しています"
pattern = r"(?P<date>\d{4}-\d{2}-\d{2}) (?P<time>\d{2}:\d{2}:\d{2}) (?P<level>\w+) (?P<message>.+)"
m = re.search(pattern, log)
print(m.group("date"))
print(m.group("level"))
print(m.group("message"))
print(m.groupdict())
# 実行結果:
# 2026-07-17
# ERROR
# ディスク容量が不足しています
# {'date': '2026-07-17', 'time': '09:00:00', 'level': 'ERROR', 'message': 'ディスク容量が不足しています'}groupdict() を使えば、結果をそのまま辞書として受け取れます。辞書の扱い方に不安がある方は、辞書(dict)の使い方を徹底解説した記事もあわせてどうぞ。数か月後の自分がコードを読み返したときに感謝するのは、間違いなく名前付きグループのほうです。
最低限おさえたいメタ文字チートシート
正規表現の記号(メタ文字)は膨大にありますが、実務で使うのは以下でほぼ足ります。丸暗記せず、必要になったら見返す程度で十分です。
.…任意の1文字(改行以外)\d…数字1文字([0-9]と同じ)\w…英数字とアンダースコア1文字\s…空白文字(スペース・タブ・改行)[abc]…a か b か c のいずれか1文字[^abc]…a・b・c以外の1文字[a-z]…a〜zの範囲の1文字*…直前のパターンの0回以上の繰り返し+…直前のパターンの1回以上の繰り返し?…直前のパターンが0回か1回(=省略可能){3}…ちょうど3回、{2,4}…2〜4回の繰り返し^…文字列の先頭、$…文字列の末尾|…OR条件(猫|犬なら「猫」か「犬」)()…グループ化
なお \d を大文字にした \D は「数字以外」というように、大文字にすると意味が反転します(\W・\S も同様)。この法則を知っておくと覚える量が半分になりますよ。
re.compile()|同じパターンを何度も使うとき
同じパターンをループ内で何度も使う場合は、re.compile() であらかじめパターンをコンパイル(コンパイル済みオブジェクト化)しておくと、コードが読みやすくなり、実行速度の面でも有利です。
import re
# メールアドレスの形式チェック用パターンを1回だけ用意する
pattern = re.compile(r"^[\w.+-]+@[\w-]+\.[\w.-]+$")
emails = ["taro@example.com", "hanako@@example", "jiro@sub.example.co.jp"]
for e in emails:
if pattern.match(e):
print(f"{e} は有効な形式です")
else:
print(f"{e} は無効な形式です")
# 実行結果:
# taro@example.com は有効な形式です
# hanako@@example は無効な形式です
# jiro@sub.example.co.jp は有効な形式ですコンパイル済みオブジェクトからは、pattern.search()・pattern.findall()・pattern.sub() のように、これまで紹介した関数を同じ名前で呼び出せます。第1引数のパターンを渡さなくてよくなる、と理解しておけばOKです。
フラグで挙動を変える
各関数の引数にフラグを渡すと、マッチのルールを変更できます。もっとも使うのは大文字小文字を区別しない re.IGNORECASE です。
import re
text = "Python python PYTHON"
print(re.findall(r"python", text))
# 実行結果: ['python']
print(re.findall(r"python", text, re.IGNORECASE))
# 実行結果: ['Python', 'python', 'PYTHON']ほかにも re.MULTILINE(^ と $ を行単位で判定する)、re.DOTALL(. が改行にもマッチする)などがあります。複数行のログを1行ずつ判定したいときは re.MULTILINE が便利です。
初心者がハマりやすい注意点3つ
1. 「欲張りマッチ」で余計な部分まで取れてしまう
* や + は、デフォルトでできるだけ長くマッチしようとする性質(貪欲マッチ)を持っています。これが意図しない結果を生む最大の原因です。
import re
html = "<b>太字</b>と<i>斜体</i>"
# 貪欲マッチ:最初の<から最後の>まで丸ごと取れてしまう
print(re.findall(r"<.+>", html))
# 実行結果: ['<b>太字</b>と<i>斜体</i>']
# 非貪欲マッチ:?を付けると最短でマッチする
print(re.findall(r"<.+?>", html))
# 実行結果: ['<b>', '</b>', '<i>', '</i>']「なぜか想定より長く取れてしまう」と感じたら、+? や *? のように? を付けて最短マッチにする——これを覚えておくだけで解決することがほとんどです。
2. マッチしなかったときのNoneを忘れる
繰り返しになりますが、これが一番多いエラーです。re.search() と re.match() は、マッチしなければ None を返します。
import re
text = "数字が含まれていない文章です"
# NG例:マッチしないとAttributeErrorで落ちる
# print(re.search(r"\d+", text).group())
# AttributeError: 'NoneType' object has no attribute 'group'
# OK例:必ずNoneチェックをしてから使う
m = re.search(r"\d+", text)
if m:
print("見つかった数字:", m.group())
else:
print("数字は含まれていませんでした")
# 実行結果: 数字は含まれていませんでした「マッチしないこともある」という前提でコードを書くクセをつけましょう。実務のデータは、こちらの想定どおりの形では絶対に届きません。
3. なんでもかんでも正規表現で解こうとする
正規表現は強力ですが、複雑になるほど誰も読めない呪文と化します。半年後の自分が解読できないコードは、それだけで技術的負債です。
import re
text = "Pythonは楽しい"
# 冗長:正規表現を使うまでもない
print(bool(re.search(r"Python", text))) # True
# シンプル:こちらのほうが速いし読みやすい
print("Python" in text) # True
print(text.startswith("Python")) # True「文字列メソッドで書けるなら文字列メソッド、無理なら正規表現」。この判断ができるようになれば、正規表現とは良い付き合いができます。また、複雑なパターンを書いたときは、必ず # 郵便番号(3桁-4桁)にマッチ のようなコメントを1行添えておきましょう。将来の自分とチームメンバーへの、何よりの贈り物になります。
正規表現を「使いこなす」段階へ進みたい人へ
正規表現は、記事を読んで理解した気になっても、いざ実務のデータを前にすると手が止まりがちな分野です。パターンの書き方そのものより、「この処理は正規表現で書くべきか」という設計判断のほうが難しく、ここは経験者にレビューしてもらうのが一番の近道でもあります。
独学で行き詰まりを感じているなら、マンツーマンで質問できる環境を検討してみるのも手です。Winスクールの「Python Winner」は、未経験からのPython習得を前提としたマンツーマンレッスンで、転職サポートまで用意されています。無料カウンセリングだけでも受けてみると、自分に足りないものがはっきりしますよ。
※本記事には広告(アフィリエイトリンク)を含みます。
Pythonプログラミング|マンツーマンレッスンで初めてをプロに育てる【Python Winner】![]()
まとめ
今回は、Pythonの正規表現(re モジュール)の使い方を初心者向けに解説しました。要点を整理しておきます。
- 正規表現は「文字列のパターン」を記号で表現する書き方で、Pythonでは標準の
reモジュールをimportするだけで使える - パターンの先頭には必ず
rを付けてraw文字列にする re.search()は文字列全体から探す。迷ったらこれを使うre.match()は先頭のみを判定する。形式チェック向きre.findall()はマッチした箇所をすべてリストで返すre.sub()はパターンで置換できる。マスキングや整形の定番()でグループ化すれば必要な部分だけ取り出せる。実務では(?P<名前>...)の名前付きグループが読みやすくおすすめre.search()・re.match()はマッチしないとNoneを返すので、必ずNoneチェックしてから.group()を呼ぶ+や*は貪欲マッチ。長く取れすぎるときは+?で最短マッチにする- 文字列メソッドで書けるなら、まずはそちらを使う
正規表現は、一度書けるようになるとログ解析・データ整形・入力チェックなど、あらゆる場面で一生使える武器になります。まずは re.search() と re.findall() の2つを、手元のコードで試すところから始めてみてください。実際に手を動かして「取れた!」という感覚をつかむのが、一番の近道です。
最後までお読みいただき、ありがとうございました。
ナノトイラボ 