CASE-001
グレップ・アンド・コー データ探偵事務所
依頼書
依頼人
PCバン「レベルアップ」の店長
証拠品
決済サーバーのログ。客の一人が4.99ドルを払ったのに記録がないと苦情を言っている。
解決条件
ログからエラーが出た行を見つけ、ちょうど$4.99の決済だけを選び出す。
初出勤の朝、所長がコーヒーカップを置き、紙を1枚こちらへ滑らせます。「昨日、PCバン(韓国のネットカフェ)の店長が来てね。客は4.99ドル払ったと言うのに、記録がないそうだ。ログは嘘をつかない。探して」
ログはこんな形をしています。今日はこの5行から証拠を探します。
2026-10-20 09:12:01 INFO PAY user=kim amount=$4.99 status=OK
2026-10-20 09:12:07 INFO PAY user=lee amount=$14.99 status=OK
2026-10-20 09:13:44 ERROR PAY user=park amount=$4.99 status=TIMEOUT
2026-10-20 09:15:02 INFO PAY user=choi amount=$4999 status=OK
2026-10-20 09:16:30 ERROR DB connection lost正規表現(regular expression)は、探したい文字の 形 を書き表す小さな言語です。Pythonでは re モジュールで使います。いちばん簡単な正規表現は、ただの文字です。ERROR というパターンは ERROR という文字を探します。
import re
log = "2026-10-20 09:13:44 ERROR PAY user=park"
m = re.search("ERROR", log)
print(m) # <re.Match object; span=(20, 25), match='ERROR'>re.search(パターン, 文字列) は、文字列の 先頭から 見ていき、パターンが最初に現れる場所を探します。None を返します。証拠分析室
re.search(r"", text)
戻り値
Matchオブジェクトは「見つけた」という証拠であり、何をどこで見つけたかが書かれた報告書でもあります。
m = re.search("ERROR", log)
if m: # 見つかれば真、Noneなら偽
print(m.group()) # ERROR (見つけた文字)
print(m.span()) # (20, 25) (開始位置, 終了位置)if m: のように、そのまま真偽で判定します。見つからないと None なので、m.group() を呼ぶと AttributeError になります。span() の終了位置は 含みません。log[20:25] が "ERROR" になるのと同じルールです。ほとんどの文字は自分自身を探します。ところが、下の14文字は正規表現の中で 特別な意味 を持つメタ文字です。
. ^ $ * + ? { } [ ] \ | ( )今日はその最初のひとつ、. だけを学びます。. は 改行以外の任意の1文字 を表します。残りはこれから1回にひとつずつ取り出して使っていきます。
re.search("a.c", "abc") # マッチ:b が . の位置に入る
re.search("a.c", "a-c") # マッチ:- も「任意の文字」
re.search("a.c", "ac") # None:. は必ず1文字分を使う
re.search("a.c", "a\nc") # None:改行は . にならないドットのひとこと
ぼくは事務所の机にも椅子にも、所長のキーボードの上にだって、どこにでも登れます。でもたったひとつ、窓(改行)だけは越えられないんです。だから名前がドット(.)。いつか誰かが窓を開けてくれたら、そのときは越えられるらしいですよ。
証拠分析室
re.search(r"", text)
戻り値
では事件に戻りましょう。いちばん上のログ5行が pay_log という文字列に入っているとします。4.99ドルの決済を探そうとして 4.99 と書くと、どうなるでしょうか。
re.findall("4.99", pay_log) # ['4.99', '4.99', '4.99', '4999'] ← 4つ?2つのはずの結果が4つあります。$14.99 の後ろ半分の 4.99 が引っかかり、さらに . が「任意の1文字」なので $4999 の 4999 まで入ってきました。メタ文字を 文字そのまま 探したいときは、前にバックスラッシュ \ を付けます。\. は本物のピリオド1つです。
$ もメタ文字です。意味は4限目で学びますが、今は「そのまま書いてもドル記号は探してくれない」とだけ覚えておいてください。というわけで、正解は \$4\.99 です。
re.findall(r"\$4\.99", pay_log) # ['$4.99', '$4.99']証拠分析室
re.search(r"", text)
戻り値
上では r"\$4\.99" のように、引用符の前に r を付けました。これを raw文字列 と呼びます。
バックスラッシュはPythonの文字列の中でも特別です。"\n" は改行、"\t" はタブでしたね。そのため正規表現を普通の文字列で書くと、Pythonが先にバックスラッシュを解釈してしまい、re にはおかしなパターンが渡ります。
print(len("\b")) # 1 ← Pythonが \b を「バックスペース」1文字に変えた
print(len(r"\b")) # 2 ← バックスラッシュとb、2文字のままr"..." はバックスラッシュに手を付けず、そのまま re に渡します。正規表現は必ずraw文字列で書きましょう。"\d" のようにPythonが知らないエスケープを普通の文字列に書くとSyntaxWarningが出ます。raw文字列なら警告は出ません。PCバンの店長が「客のIDが kim.99+ なんだけど、これも探してくれ」と言います。こういう文字をそのままパターンに書くと、. と + がメタ文字として解釈されてしまいます。人が入力した文字をそのまま探すときは、re.escape がメタ文字の前にバックスラッシュを付けてくれます。
print(re.escape("kim.99+")) # kim\.99\+
re.search(re.escape("kim.99+"), "user=kim.99+") # マッチまとめると、エラーが出た行は ERROR で、ちょうど4.99ドルの決済は \$4\.99 で見つかります。2つの条件を重ねると、客 park の決済が TIMEOUT エラーで記録されていなかったことがわかります。
for line in pay_log.splitlines():
if re.search("ERROR", line) and re.search(r"\$4\.99", line):
print(line)
# 2026-10-20 09:13:44 ERROR PAY user=park amount=$4.99 status=TIMEOUT事件解決報告書 — 決済サーバーは09:13:44にタイムアウトで決済を失敗扱いにしたが、客の画面には完了と表示されていた。PCバンの店長に返金を勧めた。
INFO の行をすべて探してみましょう。いくつありますか。\$1.\.99 は何を見つけるでしょうか。分析室で確かめる前に、まず予想してみてください。re.escape("1+1=2? [yes]") の結果を予想してから、実際に実行してみましょう。所長の手帳
ほとんどの文字は自分自身を探す。特別なのはメタ文字14個 . ^ $ * + ? { } [ ] \ | ( ) だけ。
. は改行以外の任意の1文字。本物のピリオドは \.
re.search は Match か None を返す。if m: で判定し、m.group()、m.span() で中身を見る。
正規表現は必ず r"..." のraw文字列で。
人が入力した文字をそのまま探すときは re.escape。
新しい授業・講座のお知らせを受け取る
この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。
R
ReflexKR
触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。
ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する
アフィリエイトと支援について© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
質問と回答
この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。