R

ReflexKR

Reflex公式ドキュメント
目で見て覚える正規表現
3限目

時間 1:40–2:30

破れた電話帳:量指定子

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

同じ文字が何回続くかを書く文法を 量指定子 と呼びます。今日は量指定子でこの名簿を整理します。

3-1. ちょうど何回:{m} と {m,n} (7分)

量指定子は、直前のひとかたまり を何回くり返すかを決めます。\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} のように、カンマの前後にスペースを入れません。スペースが入ると量指定子ではなくなります(下のドットのひとことを参照)。
  • 郵便番号は数字5桁なので \d{5} と書きます。

証拠分析室

re.search(r"", text)

戻り値

2限目の長いパターンです。\d{2,3} と \d{4} で短くして、新型の3桁ナンバーまで通してみましょう。

3-2. 以上、以下:{m,} と {,n} (6分)

{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つ入れたら、エラーも出ないまま、こっそり見当違いのものを探していました。量指定子の形じゃないと、ただの文字だと思われちゃうんです。エラーが出なかったからって、正しいパターンとは限りませんよ。波かっこの中には数字とカンマだけを書きましょう。

3-3. よく使う3つ:* + ? (8分)

いちばんよく使う回数には、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)

戻り値

ハイフンしか受け付けないパターンです。区切りの位置を [-. ]? に変えて、スペース、ピリオド、区切りなしまで受け入れてみましょう。

3-4. 量指定子は直前のひとかたまりにだけ (5分)

量指定子は、直前の ひとかたまり にだけ付きます。ひとかたまりとは、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)

戻り値

ha+ は a だけをくり返します。丸かっこでくくって、ha 全体をくり返してみましょう。(findallに切り替えると結果が妙に見えますが、その理由は5限目に出てきます。)

3-5. 量指定子は欲張り (4分)

量指定子は、基本的に 欲張り(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限目で事務所を止めてしまう原因になります。少なめに取る怠惰な量指定子 +? も、そのときに学びます。

3-6. すべて探す:findall と finditer (10分)

いよいよ名簿全体から見つけたものを集めます。上の名簿が 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)
  • イテレータは必要になったときに1つずつ探すので、とても長い文字列でもメモリを節約できます。
  • イテレータは一度使い切ったらおしまいです。もう一度回すには、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)

戻り値

\d* は文字のない場所ごとに空文字列を残します。* を + に変えて、数字のかたまりだけを集めてみましょう。

事件解決 (10分)

電話番号は、市外局番や頭の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)

戻り値

事件解決のパターンです。最後の行のハイライトが 1 を飛ばして始まっていることを確かめてみましょう。この問題は4限目で直します。

事件解決報告書 — OCR名簿から電話番号4件、郵便番号4件を復元し、花屋の店主に渡した。カード番号の一部が電話番号として誤検出される問題は未解決のまま。

所長が最後の行をじっと見つめます。「数字の真ん中から始めてる。このパターンは、番号がどこで始まってどこで終わるのかを知らないんだ」次の回では、その 境界 をパターンに書く方法を学びます。

練習問題

  1. \d{2,3}[-. ]?\d{3,4}[-. ]?\d{4} が 01000001234(区切りなしの番号)から何を見つけるか、予想してから実行してみましょう。
  2. re.findall(r"\d?", "a1") の結果を予想してみましょう。空文字列はいくつ出るでしょうか。
  3. 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回を許すパターンは空マッチ '' を生む。集めるときは + を使う。

← 前へ

2限目 · ひき逃げ車両:文字クラス

次へ →

4限目 · 偽の会員登録:アンカーと検証

新しい授業・講座のお知らせを受け取る

この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。

収集項目:メールアドレス・目的:この講座の新しい授業・公開のお知らせ・保有期間:解除のご依頼まで。他の目的には使わず、第三者にも提供しません。お知らせメールに返信いただければすぐに削除します。

質問と回答

この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。

リンクや書式なしで、そのままの文字で表示されます。

R

ReflexKR

触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。

ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する

アフィリエイトと支援について
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex