CASE-003
グレップ・アンド・コー データ探偵事務所
依頼書
依頼人
路地の花屋「野の花」の店主
証拠品
雨に濡れて破れた常連客の電話帳を、OCRで読み取った名簿。区切り文字はバラバラで、関係のない数字も混じっている。
解決条件
名簿から電話番号と郵便番号をすべて集める。
朝、出勤すると、所長が昨日書いたナンバーのパターンを画面に映していました。「\d を4回書く代わりに、『4回』と書く方法がある。今日はそれを覚えて」そのとき扉が開き、路地の花屋の店主が濡れた紙の束を抱えて入ってきます。「常連さんの電話帳が雨でびしょびしょになっちゃって。スキャンして文字にしたんですけど、めちゃくちゃなんです」
OCRで読み取った名簿はこうです。番号ごとに区切り文字が違い、最後の行はどこか変です。
kim 010-0000-1234 04542
lee 010 0000 9876 04545
park 02-0000-5678 04538
choi 010.0000.4321 04539
yoon 1234-5678-9012同じ文字が何回続くかを書く文法を 量指定子 と呼びます。今日は量指定子でこの名簿を整理します。
量指定子は、直前のひとかたまり を何回くり返すかを決めます。\d{4} は \d\d\d\d と同じです。2限目のナンバーのパターンを短くしてみましょう。
re.findall(r"\d{2}[가-힣]\d{4}", "12가3457 123가4567") # ['12가3457', '23가4567']短くはなりましたが、問題が見えます。2019年から登場した新型ナンバーは頭が3桁です。123가4567 の先頭の 1 を捨てて、23가4567 だけを切り取ってしまいました。
{m,n} は m回以上n回以下 です。頭の部分を \d{2,3} に変えれば、2桁も3桁も受け入れます。
re.findall(r"\d{2,3}[가-힣]\d{4}", "12가3457 123가4567") # ['12가3457', '123가4567']{2,3} のように、カンマの前後にスペースを入れません。スペースが入ると量指定子ではなくなります(下のドットのひとことを参照)。\d{5} と書きます。証拠分析室
re.search(r"", text)
戻り値
{m,n} は、どちらか片方を空けておけます。
{m,} :m回以上、上限なし{,n} :0回以上n回以下。0回でもよいので、文字がひとつもない場所 にもマッチしますre.findall(r"a{2,}", "a aa aaaa") # ['aa', 'aaaa']
re.findall(r"\d{3,}", "1 22 333 4444") # ['333', '4444']
re.findall(r"\d{,2}", "123") # ['12', '3', '']最後の結果の末尾にある '' は、文字列のいちばん最後、数字が0個の場所でマッチしたものです。こうした 空マッチ は3-6で詳しく見ます。
{ が量指定子の形になっていなければ、ただの文字として扱われます。a{x} は a、{、x、} の4文字を探します。
re.findall(r"a{x}", "a{x} ax") # ['a{x}']
re.findall(r"a{2, 3}", "aa a{2, 3}") # ['a{2, 3}']ドットのひとこと
a{2, 3} みたいにカンマの後ろにスペースを1つ入れたら、エラーも出ないまま、こっそり見当違いのものを探していました。量指定子の形じゃないと、ただの文字だと思われちゃうんです。エラーが出なかったからって、正しいパターンとは限りませんよ。波かっこの中には数字とカンマだけを書きましょう。
いちばんよく使う回数には、1文字の名前が付いています。
| 量指定子 | 意味 | 同じ書き方 |
|---|---|---|
* | 0回以上 | {0,} |
+ | 1回以上 | {1,} |
? | 0回または1回(あってもなくてもよい) | {0,1} |
re.findall(r"ab*", "a ab abbb") # ['a', 'ab', 'abbb']
re.findall(r"ab+", "a ab abbb") # ['ab', 'abbb']
re.findall(r"ab?", "a ab abbb") # ['a', 'ab', 'ab']
re.findall(r"colou?r", "color colour colouur") # ['color', 'colour']? は「あってもなくてもよい文字」にぴったりです。名簿の携帯電話番号は、区切りがハイフン、スペース、ピリオドとバラバラです。区切りの位置を [-. ]? と書けば、3つのうちどれかがあっても、まったくなくてもかまいません。
re.findall(r"010[-. ]?\d{4}[-. ]?\d{4}", phonebook)
# ['010-0000-1234', '010 0000 9876', '010.0000.4321']2限目で学んだとおり、角かっこの中の . は本物のピリオドで、- は先頭にあるので文字そのものです。
証拠分析室
re.search(r"", text)
戻り値
量指定子は、直前の ひとかたまり にだけ付きます。ひとかたまりとは、1文字、角かっこのクラス1つ、\d のような省略記法1つのことです。だから ab+ は「abが何回も」ではなく「aのあとにbが何回も」です。
re.findall(r"ab+", "abbb abab") # ['abbb', 'ab', 'ab']
re.fullmatch(r"ab+", "abab") # None
re.fullmatch(r"(ab)+", "abab") # <re.Match object; span=(0, 4), match='abab'>複数の文字をまとめてくり返したいときは、丸かっこでくくってひとかたまりにします。(ab)+ は「abが1回以上」です。丸かっこには、くくる以外にももうひとつ大事な役目がありますが、それは5限目で学びます。
re.fullmatch は、文字列 全体 がパターンと一致したときだけマッチします。詳しい違いは4限目で扱います。
証拠分析室
re.search(r"", text)
戻り値
量指定子は、基本的に 欲張り(greedy)です。許された範囲の中で、できるだけたくさん取っていきます。
re.search(r"\d+", "tel 0100000") # <re.Match object; span=(4, 11), match='0100000'>
re.search(r"\d{2,4}", "123456") # <re.Match object; span=(0, 4), match='1234'>
re.findall(r"\d{2,4}", "123456") # ['1234', '56']\d{2,4} は2桁でもかまわないのに、まず4桁を取ります。後ろに続くパターンが失敗したときになって、ようやく1つずつ返していきます。この「返しながらやり直す」動きをバックトラックと呼び、8限目で事務所を止めてしまう原因になります。少なめに取る怠惰な量指定子 +? も、そのときに学びます。
いよいよ名簿全体から見つけたものを集めます。上の名簿が phonebook という文字列に入っているとしましょう。
re.findall は、マッチした文字を 文字列のリスト で返します。これはパターンに丸かっこがないときの動きで、丸かっこがあるときの動きは5限目で見ます。
re.findall(r"\d{5}", phonebook) # ['04542', '04545', '04538', '04539']re.finditer は、Matchオブジェクトを1つずつ取り出してくれるイテレータ を返します。位置(span)まで必要なときに使います。
for m in re.finditer(r"\d{5}", phonebook):
print(m.group(), m.span())
# 04542 (22, 27)
# 04545 (50, 55)
# 04538 (78, 83)
# 04539 (106, 111)re.finditer をもう一度呼びます。ここで気をつけたいのが 空マッチ です。* や ?、{,n} は0回も許すので、文字がひとつもない場所でもマッチに成功します。findallはその空マッチも '' として結果に入れます。
re.findall(r"\d*", "a12b") # ['', '12', '', '']
re.findall(r"\d+", "a12b") # ['12']a の前で空マッチ、12、b の前で空マッチ、文字列の末尾で空マッチが1つずつ出ました。何かを 集める ときは、0回を許さない + や {1,} を使います。
証拠分析室
re.search(r"", text)
戻り値
電話番号は、市外局番や頭の2〜3桁、真ん中の3〜4桁、最後の4桁でできています。区切りはあってもなくてもかまいません。郵便番号は5桁です。
phones = re.findall(r"\d{2,3}[-. ]?\d{3,4}[-. ]?\d{4}", phonebook)
print(phones)
# ['010-0000-1234', '010 0000 9876', '02-0000-5678', '010.0000.4321', '234-5678-9012']
postcodes = re.findall(r"\d{5}", phonebook)
print(postcodes) # ['04542', '04545', '04538', '04539']電話番号4件と郵便番号4件が集まりました。では、5つ目の電話番号 234-5678-9012 は何でしょう。花屋の店主が画面をのぞき込み、首を横に振ります。「それは電話番号じゃありません。私のカード番号のメモです」
re.search(r"\d{2,3}[-. ]?\d{3,4}[-. ]?\d{4}", "yoon 1234-5678-9012")
# <re.Match object; span=(7, 20), match='234-5678-9012'>パターンが 1234 の最初の 1 を飛ばし、数字のど真ん中 である7番目の位置からマッチを始めています。3-1の 23가4567 と同じ事故です。
証拠分析室
re.search(r"", text)
戻り値
事件解決報告書 — OCR名簿から電話番号4件、郵便番号4件を復元し、花屋の店主に渡した。カード番号の一部が電話番号として誤検出される問題は未解決のまま。
所長が最後の行をじっと見つめます。「数字の真ん中から始めてる。このパターンは、番号がどこで始まってどこで終わるのかを知らないんだ」次の回では、その 境界 をパターンに書く方法を学びます。
\d{2,3}[-. ]?\d{3,4}[-. ]?\d{4} が 01000001234(区切りなしの番号)から何を見つけるか、予想してから実行してみましょう。re.findall(r"\d?", "a1") の結果を予想してみましょう。空文字列はいくつ出るでしょうか。re.finditer で名簿の電話番号を探し、番号とその開始位置を1行ずつ出力してみましょう。所長の手帳
{m} はちょうどm回、{m,n} はm〜n回、{m,} はm回以上、{,n} は0〜n回。カンマの横にスペースは禁止。
* は0回以上、+ は1回以上、? は0回または1回。
量指定子は直前のひとかたまりにだけ付く。ab+ と (ab)+ は違う。
量指定子は欲張り。できるだけたくさん取っていく。
re.findall は文字列のリスト、re.finditer は Match のイテレータ。
0回を許すパターンは空マッチ '' を生む。集めるときは + を使う。
新しい授業・講座のお知らせを受け取る
この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。
R
ReflexKR
触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。
ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する
アフィリエイトと支援について© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
質問と回答
この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。