CASE-005
グレップ・アンド・コー データ探偵事務所
依頼書
依頼人
ネットショップ「マーケット・チョン」のチョン社長
証拠品
マーケット・チョンの注文記録。アルバイトごとに日付をばらばらの形で書いている。
解決条件
すべての注文から年・月・日を別々に取り出し、同じ形にそろえる。
偽の会員登録事件の翌日、チョン社長がまたやってきます。今度はまずため息からです。「登録は防げました。でも注文記録を日付ごとに整理しようとしたら、アルバイトの子が3人とも日付を違う書き方で入れてたんですよ」所長がコーヒーをひと口すすります。「探し方は第1部で覚えた。次は、見つけたものの中から 取り出す 方法の番だ」
注文記録はこうです。
#1001 2026-10-20 ship=done
#1002 2026.10.20 ship=wait
#1003 2026/10/19 ship=done
#1004 20261019 ship=done
#1005 2026-10/20 ship=wait日付を見つけるだけでは足りません。「2026」「10」「20」を 別々に 取り出さないと整理できません。今日の道具は丸かっこ、つまり グループ です。
3限目で、量指定子は直前のひとかたまりにだけ付くと言いました。丸かっこ ( ) でくくると、複数の文字が ひとかたまり になります。
re.fullmatch(r"ha+", "hahaha") # None
re.fullmatch(r"(ha)+", "hahaha") # <re.Match object; span=(0, 6), match='hahaha'>IPアドレスのように「数字とピリオド」が3回くり返される形も、グループに量指定子を付けて書きます。
re.search(r"(\d{1,3}\.){3}\d{1,3}", "ip=203.0.113.66")
# <re.Match object; span=(3, 15), match='203.0.113.66'>(\d{1,3}\.){3} は「1〜3桁の数字とピリオド」を3回です。最後の数字の後ろにはピリオドがないので、\d{1,3} を別に付けました。
| は「左 または 右」です。ところが | は、正規表現の中で 優先順位がいちばん低い のです。丸かっこで区切らないと、パターン全体を2つに割ってしまいます。
re.fullmatch(r"ship=done|wait", "wait") # <re.Match object; span=(0, 4), match='wait'>
re.fullmatch(r"ship=(done|wait)", "wait") # None
re.fullmatch(r"ship=(done|wait)", "ship=wait") # <re.Match object; span=(0, 9), match='ship=wait'>ship=done|wait は「ship=done または wait」です。「ship= のあとに done または wait」にしたいなら、選ぶ部分だけを丸かっこで囲みます。
アンカーと組み合わせるときは特に注意が必要です。^a|b$ は「aかbだけでできた文字列」ではなく、「^a または b$」です。
re.findall(r"^a|b$", "a.b") # ['a', 'b']
re.search(r"^ok|no$", "say no") # <re.Match object; span=(4, 6), match='no'>say no は ok で始まっていないのにマッチしました。本来の意図どおりにするなら ^(ok|no)$ と書きます。
選択肢は 左から 順に試され、先に成功したものが使われます。いちばん長いものが選ばれるわけではありません。
re.search(r"10|100", "100") # <re.Match object; span=(0, 2), match='10'>
re.search(r"100|10", "100") # <re.Match object; span=(0, 3), match='100'>-|\.|/ は、クラス [-./] と同じです。こういうときはクラスのほうが短くて速くなります。証拠分析室
re.search(r"", text)
戻り値
丸かっこには、くくる以外にもうひとつの役目があります。丸かっこの中でマッチした文字を 別に覚えておく のです。これを キャプチャグループ と呼び、開きかっこの順に1、2、3……と番号が付きます。
上の注文記録が orders という文字列に入っているとしましょう。
m = re.search(r"(\d{4})-(\d\d)-(\d\d)", orders)
print(m.group(0)) # 2026-10-20
print(m.group(1)) # 2026
print(m.group(2), m.group(3)) # 10 20
print(m.groups()) # ('2026', '10', '20')
print(m.group(1, 2)) # ('2026', '10')
print(m[1], m[0]) # 2026 2026-10-20group(0)(またはただの group())はマッチ 全体 です。グループは1番からです。groups() は1番から最後までをタプルで返します。group(1, 2) のように番号を複数渡すと、タプルで返ってきます。m[1] は m.group(1) の省略形です。かっこが入れ子になっていてもルールは同じです。開きかっこ ( が現れる順番 に番号を数えます。
((\d{4})-(\d\d))-(\d\d)
12 3 4m = re.search(r"((\d{4})-(\d\d))-(\d\d)", "2026-10-20")
print(m.groups()) # ('2026-10', '2026', '10', '20')証拠分析室
re.search(r"", text)
戻り値
くくるだけで、覚えておく必要はないこともよくあります。| の範囲を決めたり、量指定子を付けたりするために丸かっこを使うときです。そんなときは (?:...) を使います。くくりはしますが、番号を受け取りません。
m = re.search(r"(?:\d{4})-(\d\d)", "2026-10")
print(m.group(1)) # 10
print(re.fullmatch(r"(?:ab)+", "abab").groups()) # ()最初のかっこが番号を受け取らないので、(\d\d) が1番になりました。必要なものだけをキャプチャすれば番号がすっきりし、次の節のfindallの落とし穴も避けられます。
findallは、パターンにキャプチャグループがあると動きが変わります。
| キャプチャグループの数 | findallが返すもの |
|---|---|
| 0個 | マッチ全体の文字列のリスト |
| 1個 | そのグループ の文字列のリスト |
| 2個以上 | グループの タプル のリスト |
re.findall(r"\d{4}[-./]\d\d[-./]\d\d", orders)
# ['2026-10-20', '2026.10.20', '2026/10/19', '2026-10/20']
re.findall(r"(\d{4})[-./]\d\d[-./]\d\d", orders)
# ['2026', '2026', '2026', '2026']
re.findall(r"(\d{4})[-./](\d\d)[-./](\d\d)", orders)
# [('2026', '10', '20'), ('2026', '10', '20'), ('2026', '10', '19'), ('2026', '10', '20')]年・月・日がほしいときは、タプルのリストが便利です。問題は、くくるためだけに使った丸かっこ までグループになってしまうことです。
re.findall(r"\d{4}(-|\.|/)\d\d(-|\.|/)\d\d", orders)
# [('-', '-'), ('.', '.'), ('/', '/'), ('-', '/')]日付がほしかったのに、区切り文字だけが返ってきました。マッチ 全体 が必要なら、丸かっこを (?:...) に変えるか、finditerでMatchを受け取って group() を使います。
re.findall(r"\d{4}(?:-|\.|/)\d\d(?:-|\.|/)\d\d", orders)
# ['2026-10-20', '2026.10.20', '2026/10/19', '2026-10/20']
[m.group() for m in re.finditer(r"\d{4}(-|\.|/)\d\d(-|\.|/)\d\d", orders)]
# ['2026-10-20', '2026.10.20', '2026/10/19', '2026-10/20']ドットのひとこと
findallの結果がやけに短かったり、タプルが出てきたりしたら、まずパターンの丸かっこを数えてみてください。くくるためだけのかっこが、こっそりグループになっていることがよくあるんです。3限目で (ha)+ をfindallで回したら 'ha' しか出なかったのも、これが原因ですよ。
証拠分析室
re.search(r"", text)
戻り値
グループがマッチに参加しなかった場合、その位置は None になります。? の付いたグループが0回マッチしたときや、| のもう一方が選ばれたときです。
m = re.fullmatch(r"(\d{4})-(\d\d)(-(\d\d))?", "2026-10")
print(m.groups()) # ('2026', '10', None, None)
print(m.group(3)) # None
print(re.search(r"(a)|(b)", "b").groups()) # (None, 'b')グループに量指定子を付けて何度もマッチさせると、グループには 最後の1回 だけが残ります。
m = re.fullmatch(r"(\d)+", "2026")
print(m.group(0), m.group(1)) # 2026 6
re.findall(r"(\d{1,3}\.){3}\d{1,3}", "a=203.0.113.66 b=198.51.100.7")
# ['113.', '100.']
re.findall(r"(?:\d{1,3}\.){3}\d{1,3}", "a=203.0.113.66 b=198.51.100.7")
# ['203.0.113.66', '198.51.100.7'](\d)+ は4回マッチしましたが、グループ1には最後の 6 だけが残りました。IPアドレスも、グループには3回目のくり返しである 113. だけが残り、findallはそのグループを返しました。5-1のIPパターンをfindallで使うなら、(?:...) が正解です。
3種類の区切り -、.、/ は [-./] で、区切りのない 20261019 は ? で受け止めます。年・月・日はそれぞれキャプチャします。
DATE = r"(\d{4})[-./]?(\d\d)[-./]?(\d\d)"
for m in re.finditer(DATE, orders):
y, mo, d = m.groups()
print(f"{y}년 {int(mo)}월 {int(d)}일")
# 2026년 10월 20일
# 2026년 10월 20일
# 2026년 10월 19일
# 2026년 10월 19일
# 2026년 10월 20일韓国語の 년、월、일 は、それぞれ「年」「月」「日」のことです。注文番号もいっしょに取り出したいなら、グループをもうひとつ置きます。グループが4つなので、findallは4つの要素を持つタプルを返します。
re.findall(r"#(\d+) " + DATE, orders)
# [('1001', '2026', '10', '20'), ('1002', '2026', '10', '20'), ('1003', '2026', '10', '19'), ('1004', '2026', '10', '19'), ('1005', '2026', '10', '20')]証拠分析室
re.search(r"", text)
戻り値
事件解決報告書 — 注文5件の日付を年・月・日に分けて整理した。ただし#1005は区切り文字が混ざったまま入力されており、スタッフによる確認が必要。
整理された表を受け取ったチョン社長が、最後の行を指さします。「#1005は 2026-10/20 なんですけど。これもそのまま通っちゃってますよね?」
re.fullmatch(DATE, "2026-10/20")
# <re.Match object; span=(0, 10), match='2026-10/20'>そのとおりです。2つの [-./]? は、お互いのことを知りません。前で - を使ったら後ろでも - を使え、と伝える方法が今のパターンにはないのです。所長が手帳に1行書きつけます。「前で捕まえたものを後ろでもう一度呼ぶ方法がある。それは10限目で」今日はこの1件を人の目で確認して終わりにします。
re.findall(r"ship=(done|wait)", orders) の結果を予想してみましょう。(?:done|wait) に変えると、どう変わるでしょうか。re.search(r"(\d{4})-(\d\d)(-(\d\d))?", "2026-10 発送") で、groups() は何になるでしょうか。03:11:02 から時・分・秒をグループで取り出すパターンを書き、m.group(1, 3) の結果を確かめてみましょう。所長の手帳
( ) はくくってひとかたまりにし、マッチした文字を覚えておく。(ab)+ のように量指定子を付ける。
| は優先順位がいちばん低い。^a|b$ は「^a または b$」。選択肢は左から試す。
グループ番号は開きかっこの順。group(0) は全体、groups() はタプル、m[1] は group(1)。
(?:...) はくくるだけで、番号を受け取らない。
findall:グループ0個なら全体、1個ならそのグループ、2個以上ならタプル。全体がほしければ (?:...) か finditer。
マッチしなかったグループは None、(\d)+ のようにくり返したグループは最後の1回だけが残る。
新しい授業・講座のお知らせを受け取る
この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。
R
ReflexKR
触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。
ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する
アフィリエイトと支援について© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
質問と回答
この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。