CASE-002
グレップ・アンド・コー データ探偵事務所
依頼書
依頼人
管轄警察署のカン班長
証拠品
目撃者の証言1枚と、地下駐車場の入出庫記録。目撃者はナンバープレートを一部しか見ていない。
解決条件
駐車記録からナンバーをすべて抜き出し、目撃証言に合う車両を1台に絞り込む。
夕暮れどき、カン班長が濡れた傘を振りながら事務所の扉を押し開けます。「昨夜の22時45分ごろ、地下駐車場の出口でひき逃げだ。目撃者はナンバーを全部は見てないそうでな」所長はキーボードから手を離さずに答えます。「全部見てないなら、見たぶんだけパターンに書けばいい。証拠はパターンを残す」
目撃者の証言はこうです(ソウルの事件なので韓国語のままです)。
- 앞 두 자리는 12 아니면 17이었어요.
- 가운데 한글은 가, 거, 고 중 하나였어요.
- 마지막 숫자는 7이었어요.
- 차는 출구(OUT) 쪽으로 빠져나갔어요.訳すとこうなります。「最初の2桁は12か17でした」「真ん中のハングルは가、거、고のどれかでした」「最後の数字は7でした」「車は出口(OUT)のほうへ出ていきました」。韓国のナンバープレートは「数字2桁+ハングル1文字+数字4桁」という形をしています。
駐車場の入出庫記録です。カメラはときどきナンバーを読み間違えます。
2026-10-19 22:41 IN B1 12가3457
2026-10-19 22:43 IN B1 34나1207
2026-10-19 22:47 OUT B2 17거3407
2026-10-19 22:52 IN B1 12-3457
2026-10-19 22:58 OUT B2 56다9081
2026-10-19 23:05 IN B1 12a3457証言の1行1行が「この位置にはこの中のどれか」という条件になっています。今日学ぶ 文字クラス こそ、その条件を書くための文法です。
角かっこ [...] は 中に書いた文字のうちどれか1文字 を表します。目撃者が「가、거、고のどれか」と言ったので、その位置は [가거고] と書きます。
import re
re.findall("12[가거고]3457", "12가3457 12거3457 12나3457") # ['12가3457', '12거3457']
re.findall(r"[ab]c", "ac bc cc") # ['ac', 'bc']
re.search(r"[abc]", "xyz") # None[가거고] は3文字ではなく1文字です。[가거고] と [고거가] は同じクラスです。. が「任意の1文字」だったのに対し、角かっこは「選んだ文字のうちの1文字」です。証拠分析室
re.search(r"", text)
戻り値
数字10個を [0123456789] のように全部書くのは面倒です。角かっこの中で2つの文字のあいだに - を入れると 範囲 になります。[0-9] は0から9まで、[a-z] は英小文字すべてです。
re.findall(r"[0-9]", "B1") # ['1']
re.findall(r"[a-z0-9]", "B1 b2") # ['1', 'b', '2']
re.findall(r"[a-zA-Z]", "a1Z") # ['a', 'Z'][a-z0-9] は小文字または数字の1文字です。[a-z] に大文字は入りません。ハングルの完成形の文字は、Unicodeで 가(U+AC00)から 힣(U+D7A3)までの11,172文字が順番に並んでいます。そのため [가-힣] ひとつで、完成したハングル1文字をすべて捕まえられます。
print(hex(ord("가")), hex(ord("힣"))) # 0xac00 0xd7a3
re.findall(r"[가-힣]", "ㄱ ㅏ 가") # ['가']ㄱ、ㅏ のような単独の字母(子音・母音のパーツ)は別の区画にあるので、[가-힣] には入りません。ナンバーの真ん中の文字はいつも完成形なので、この範囲で十分です。
範囲を逆向きに書くと、パターンを作った時点でエラーになります。
re.compile("[a-Z]") # re.PatternError: bad character range a-Z at position 1証拠分析室
re.search(r"", text)
戻り値
ドットのひとこと
範囲はコードポイントの小さいほうから大きいほうへしか書けないんです。大文字も小文字もまとめて捕まえようと [a-Z] と書くとエラーになりますよ。aは97番、Zは90番で、逆向きですからね。両方ほしいなら [a-zA-Z] みたいに範囲を2つ書いてください。
角かっこのすぐ内側、先頭に ^ を書くと意味がひっくり返ります。[^0-9] は 数字ではない1文字 です。
re.findall(r"[^0-9]", "12가34") # ['가']
re.findall(r"[0-9^]", "2^3") # ['2', '^', '3']
re.findall(r"[^0-9]", "1\n2") # ['\n']^ が否定になるのは 先頭にあるときだけ です。[0-9^] のように後ろにあれば、ただの ^ という文字です。[^0-9] もやはり 1文字分を使います。「数字がない場所」ではなく「数字ではない文字1つ」です。[^0-9] に引っかかります。駐車場のカメラは、ときどき数字を英字と読み間違えます。1 を I に、5 を S に、2 を Z に、といった具合です。数字でもハングルでも空白でもない文字を探せば、読み間違えた箇所が浮かび上がります。
ドットのひとこと
ぼくは窓(改行)を越えられないけど、[^0-9] は越えちゃいます。「数字ではない文字」には改行も入るからです。複数行の記録で [^...] を使うときは、改行まで捕まえていないか必ず確かめてくださいね。
証拠分析室
re.search(r"", text)
戻り値
よく使うクラスは、バックスラッシュと英字1文字で短く書けます。大文字にすると意味が逆になります。
| 記法 | 意味 | だいたい同じクラス |
|---|---|---|
\d | 数字1文字 | [0-9] |
\D | 数字ではない1文字 | [^0-9] |
\w | 文字・数字・アンダースコアの1文字(ハングルや日本語も含む) | [a-zA-Z0-9_] より広い |
\W | \w ではない1文字 | |
\s | 空白類の1文字(スペース、タブ、改行など) | [ \t\n\r\f\v] より広い |
\S | 空白類ではない1文字 |
re.findall(r"\d", "B1 12가") # ['1', '1', '2']
re.findall(r"\D", "12가-3") # ['가', '-']
re.findall(r"\w", "a_1 가!") # ['a', '_', '1', '가']
re.findall(r"\W", "a_1 가!") # [' ', '!']
re.findall(r"\s", "a b\tc\nd") # [' ', '\t', '\n']
re.findall(r"\S", "a b\tc") # ['a', 'b', 'c']省略記法は、角かっこの中でほかの文字と混ぜて使えます。[\d,.] は「数字かカンマかピリオド」の1文字です。
re.findall(r"\d[\d,.]\d", "1,2 3.4 5-6 789") # ['1,2', '3.4', '789']では、ナンバーを省略記法で書いてみましょう。真ん中を \D(数字ではないもの)にすると楽そうに見えますが、カメラが読み間違えた 12-3457 や 12a3457 まで通ってしまいます。真ん中は「数字ではない何か」ではなく「ハングル1文字」でなければなりません。
証拠分析室
re.search(r"", text)
戻り値
角かっこの中では、ほとんどのメタ文字が力を失います。[.*+?] はピリオド、アスタリスク、プラス、クエスチョンマークのどれか1文字にすぎません。バックスラッシュを付ける必要もありません。
角かっこの中でも特別なまま残るのは、次の4文字だけです。
\ :エスケープと、\d のような省略記法] :クラスの終わり。ただし 先頭 にあれば文字そのもの^ :先頭 にあれば否定- :範囲。ただし 先頭か末尾 にあれば文字そのものre.findall(r"[.*+?]", "a.b*c+d?") # ['.', '*', '+', '?']
re.findall(r"[]a]", "a]b") # ['a', ']']
re.findall(r"[-+]", "3-2+1") # ['-', '+']
re.findall(r"[a\-z]", "a-mz") # ['a', '-', 'z']
re.findall(r"[\]\\]", r"a]b\c") # [']', '\\'][a\-z] は範囲ではなく、a、-、z の3文字です。だから m は捕まりません。位置のルールで迷ったら、素直にバックスラッシュを付けたほうが読みやすくなります。
Pythonのstrパターンは、デフォルトでUnicodeを扱います。そのため省略記法は思ったより広い範囲を捕まえます。\w がハングルを捕まえるのはすでに見ました。\d はアラビア・インド数字の ٣(U+0663、数字の3)も数字とみなします。\s は、日本語の文書でもおなじみの全角スペース(U+3000)も捕まえます。
re.findall(r"\d", "3٣") # ['3', '٣']
print(int("٣")) # 3
re.findall(r"\s", "a\u3000b") # ['\u3000']\d の代わりに [0-9] を使います。re.ASCII フラグは11限目で学びます。最後にもうひとつ。角かっこの中で [[、--、&&、~~、|| のように同じ記号を重ねて書くと、FutureWarningが出ます。Pythonが将来、クラス同士の集合演算(和集合、積集合など)に使うためにとってある書き方だからです。
import warnings
warnings.simplefilter("always")
re.compile(r"[[a]") # FutureWarning: Possible nested set at position 1
re.compile(r"[a&&b]") # FutureWarning: Possible set intersection at position 2今は動きますが、将来は意味が変わるかもしれません。そうした文字がどうしても必要なら、[\[a]、[a&\&b] のようにどちらか一方をエスケープします。12限目でもう一度整理します。
駐車記録が parking という文字列に入っているとしましょう。まず本物のナンバーだけをすべて抜き出します。カメラが読み間違えた2行は外れます。
plates = re.findall(r"\d\d[가-힣]\d\d\d\d", parking)
print(plates) # ['12가3457', '34나1207', '17거3407', '56다9081']次に、目撃証言を1桁ずつクラスに置き換えます。最初の2桁は 1[27]、真ん中は [가거고]、最後は 7 です。
print(re.findall(r"1[27][가거고]\d\d\d7", parking)) # ['12가3457', '17거3407']
print(re.findall(r"OUT B\d 1[27][가거고]\d\d\d7", parking)) # ['OUT B2 17거3407']候補は2台ですが、12가3457 は事故の前に入ったきり、出た記録がありません。事故の2分後に出口から出ていった車は 17거3407 の1台だけです。
事件解決報告書 — 22:47にB2出口から出た17거3407が、目撃証言とすべて一致。カン班長に車両照会を依頼した。
カン班長が帰ると、所長が画面のパターンを指でなぞります。「\d\d[가-힣]\d\d\d\d。同じものを4回も書いてる。それに最近のナンバーは、頭が3桁のものもある」次の回では、この長いパターンを短くする方法を学びます。
B1、B2 のような出入口の名前だけを探すパターンを書いてみましょう。B[12] と B\d はどこが違うでしょうか。[^\d\s] は何を見つけるでしょうか。上のカメラ記録 12가3457 I2가3457 12가34S7 1Z거3407 で、まず予想してから実行してみましょう。22:41 のような「2桁:2桁」の形を探すパターンを、省略記法で書いてみましょう。所長の手帳
[abc] は a、b、c のどれか1文字。何文字入れても1文字分しか使わない。
[a-z0-9] のように、範囲はコードポイント順。ハングルの完成形は [가-힣]。[a-Z] はエラー。
[^...] は先頭の ^ のときだけ否定。改行も捕まえる。
\d \w \s と、その逆の \D \W \S。[\d,.] のようにクラスの中に混ぜて使える。
クラスの中では . * + ? は文字そのもの。特別なのは \ ] ^ - の4つだけ。
strパターンの \d \w \s はUnicode全体が対象。0〜9だけなら [0-9]。
新しい授業・講座のお知らせを受け取る
この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。
R
ReflexKR
触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。
ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する
アフィリエイトと支援について© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
質問と回答
この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。