R

ReflexKR

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

時間 2:30–3:20

偽の会員登録:アンカーと検証

CASE-004

グレップ・アンド・コー データ探偵事務所

依頼書

偽の会員登録

依頼人

ネットショップ「マーケット・チョン」のチョン社長

証拠品

会員登録の申請履歴と、IDのルールをチェックするPython関数1つ。

解決条件

ルールに合わないIDをすべてはじき出し、チェッカーがなぜ突破されたのかを明らかにする。

昼どき、チョン社長がノートパソコンを抱えて入ってきます。「IDのルールはちゃんと決めてあるんです。なのに、おかしなIDがどんどん入ってきて」所長はしばらく画面を眺めてから、短く言います。「ルールは合ってる。聞き方が間違ってるんだ」

マーケット・チョンのIDルールとチェック関数はこうです。

아이디 규칙: 영문 소문자로 시작하고, 소문자·숫자·밑줄(_)만 써서 4~16자

ルールの中身は「英小文字で始まり、小文字・数字・アンダースコア(_)だけを使って4〜16文字」です。

import re

ID = r"[a-z][a-z0-9_]{3,15}"

def is_valid_id(uid):
    return re.search(ID, uid) is not None

登録申請の履歴です。

2026-10-18 14:02:51 signup id=jeong_fan
2026-10-19 03:11:02 signup id=ok_id!!!
2026-10-19 03:11:04 signup id=_admin
2026-10-19 03:11:05 signup id=ABC123
2026-10-19 03:11:07 signup id=a1
2026-10-19 09:40:13 signup id=minsu99

4-1. 事故の再現:searchはどこからでも探す (5分)

まず、チョン社長のチェッカーが何を通してしまうのかを見てみます。

print(is_valid_id("ok_id!!!"))   # True   ← ビックリマークがあるのに通る
print(is_valid_id("_admin"))     # True   ← アンダースコアで始まるのに通る
print(is_valid_id("ABC123"))     # False

re.search は「この文字列の どこかに ルールに合う部分があるか」を尋ねます。ok_id!!! の中には ok_id が、_admin の中には admin があるので成功です。

re.search(ID, "ok_id!!!")    # <re.Match object; span=(0, 5), match='ok_id'>
re.search(ID, "!!!ok_id")    # <re.Match object; span=(3, 8), match='ok_id'>

入力の検証では、別の質問をしなければなりません。「この文字列の 全体が ルールに合っているか」。そのためには、パターンに「ここが始まり、ここが終わり」と書く必要があります。

4-2. ^ と $:始まりと終わりに打ち込む (8分)

角かっこの外の ^ は 文字列の始まり、$ は 文字列の終わり を表します。こうした記号を アンカー(anchor、いかり)と呼びます。

re.search(r"^id", "id=kim")       # <re.Match object; span=(0, 2), match='id'>
re.search(r"^id", "my id=kim")    # None
re.search(r"kim$", "id=kim")      # <re.Match object; span=(3, 6), match='kim'>
re.search(r"kim$", "id=kim!")     # None
  • アンカーは文字を消費しません。文字と文字の あいだの位置 にマッチします。だから ^id のマッチは、あくまで id の2文字です。
  • 2限目の [^...] と混同しないでください。^ は角かっこの先頭なら否定、角かっこの外なら始まりです。

チェッカーのパターンの前後に ^ と $ を付ければ、ルールに合う部分が文字列の 最初から最後まで 続いていなければならなくなります。

証拠分析室

re.search(r"", text)

戻り値

解決条件はsearchでチェックします。パターンの前に ^、後ろに $ を付けて、偽のIDを防いでみましょう。

4-3. match と fullmatch (8分)

アンカーを自分で書かなくても、どこで照らし合わせるかを関数で選べます。

関数尋ねることアンカーで書くと
re.searchどこかに合う部分があるか(アンカーなし)
re.match先頭から 合う部分があるか\Aパターン
re.fullmatch全体が 合っているか\Aパターン\Z

\A と \Z は ^、$ に似たアンカーですが、その違いは4-4で見ます。

re.match(ID, "ok_id!!!")       # <re.Match object; span=(0, 5), match='ok_id'>
re.match(ID, "!!!ok_id")       # None
re.fullmatch(ID, "ok_id!!!")   # None
re.fullmatch(ID, "ok_id")      # <re.Match object; span=(0, 5), match='ok_id'>
  • re.match は名前に反して「全体が合っているか」までは見ません。先頭部分さえ合えば成功なので、ok_id!!! を通してしまいます。
  • 入力検証の正解は re.fullmatch です。パターンにアンカーを付け忘れても安全で、次の節の落とし穴も避けられます。

証拠分析室

re.search(r"", text)

戻り値

search、match、fullmatch を順に押してみましょう。テキストを !!!ok_id や ok_id に書き換えながら、3つの関数の違いを確かめてください。

4-4. $ の落とし穴と \A、\Z、\z (6分)

$ には隠れたルールがひとつあります。文字列の本当の終わりだけでなく、いちばん最後の改行の直前 でもマッチするのです。そのため ^abc$ は、末尾に改行の付いた "abc\n" も通してしまいます。

re.search(r"^abc$", "abc\n")       # <re.Match object; span=(0, 3), match='abc'>
re.fullmatch(r"abc", "abc\n")      # None
re.search(r"\Aabc\Z", "abc\n")     # None
  • \A は文字列のいちばん最初でだけ、\Z は文字列のいちばん最後でだけマッチします。改行の例外はありません。
  • Python 3.14からは \z も使えます。Pythonでは \Z とまったく同じで、ほかの言語の正規表現と書き方をそろえるために追加されました。
re.search(r"\Aabc\z", "abc")      # <re.Match object; span=(0, 3), match='abc'>
re.search(r"\Aabc\z", "abc\n")    # None

ドットのひとこと

ファイルから1行ずつ読んだ文字には、末尾に改行が付いていることが多いんです。^...$ でチェックすると、その改行を見て見ぬふりして通しちゃいます。するとIDの末尾に改行が付いたまま保存されちゃいますよ。検証には fullmatch か、\A...\Z を使ってくださいね。

4-5. 単語境界:\b と \B (8分)

3限目の宿題が残っています。電話番号のパターンが、1234-5678-9012 の ど真ん中 の 234 からマッチを始めてしまった事故です。今回は文字列全体ではなく、長い文章の中で 番号1つの始まりと終わり を教えてくれるアンカーが必要です。

\b は 単語境界 です。\w(文字・数字・アンダースコア)と \W(それ以外の文字)のあいだ、または文字列の始まり・終わりのうち \w の隣にあたる位置にマッチします。\B はその逆で、境界 ではない 位置です。どちらも文字を消費しません。

re.findall(r"\bid\b", "id=kim userid=lee id_card")   # ['id']
re.findall(r"\Bid\b", "id=kim userid=lee id_card")   # ['id']
re.findall(r"\bid\B", "id=kim userid=lee id_card")   # ['id']

3つの結果はどれも ['id'] ですが、見つけた位置が違います。

  • \bid\b :前後がどちらも境界である、id=kim の id
  • \Bid\b :前が境界ではない、userid の id
  • \bid\B :後ろがアンダースコア(\w)なので境界ではない、id_card の id

電話番号の前後に \b を付ければ、数字の真ん中からは始められなくなります。1 と 2 のあいだは、どちらも \w なので境界ではないからです。

re.findall(r"\b\d{2,3}[-. ]?\d{3,4}[-. ]?\d{4}\b", phonebook)
# ['010-0000-1234', '010 0000 9876', '02-0000-5678', '010.0000.4321']

ひとつ注意があります。Pythonではハングル(そして日本語のかなや漢字)も \w です。そのため、ハングルのすぐ後ろにくっついた数字の前には境界がありません。下の例の 전화 は「電話」という意味の韓国語です。

re.findall(r"\b\d{2,3}-\d{3,4}-\d{4}\b", "전화010-0000-1234")    # []
re.findall(r"\b\d{2,3}-\d{3,4}-\d{4}\b", "전화 010-0000-1234")   # ['010-0000-1234']

証拠分析室

re.search(r"", text)

戻り値

3限目の電話番号パターンです。前後に \b を付けて、カード番号と長すぎる番号を防いでみましょう。

4-6. 複数行で1行ずつ:re.MULTILINE をちょっと味見 (5分)

登録履歴のように複数の行が1つの文字列に入っていると、^ は 最初の行の始まり でしかマッチしません。各行の先頭にある日付を抜き出したいなら、re.MULTILINE(略して re.M)フラグを渡します。すると ^ と $ が 行ごとに 始まりと終わりにマッチします。

re.findall(r"^\d{4}-\d\d-\d\d", signups)
# ['2026-10-18']
re.findall(r"^\d{4}-\d\d-\d\d", signups, re.MULTILINE)
# ['2026-10-18', '2026-10-19', '2026-10-19', '2026-10-19', '2026-10-19', '2026-10-19']

re.M を渡しても、\A と \Z はあいかわらず文字列全体の最初と最後でしかマッチしません。

re.findall(r"^\d", "1\n2", re.M)    # ['1', '2']
re.findall(r"\A\d", "1\n2", re.M)   # ['1']

フラグは11限目でまとめて正式に扱います。今日は「^ と $ を行単位に切り替えるスイッチがある」とだけ覚えておいてください。

証拠分析室

re.search(r"", text)

フラグ

戻り値

re.M ボタンをオンにしたりオフにしたりしてみましょう。オンにすると ^ が行ごとにマッチします。^ を \A に変えるとどうなるでしょうか。

事件解決 (10分)

チェッカーの re.search を re.fullmatch に変えます。パターンはそのままです。

def is_valid_id(uid):
    return re.fullmatch(r"[a-z][a-z0-9_]{3,15}", uid) is not None

for line in signups.splitlines():
    uid = line.split("id=")[1]
    if not is_valid_id(uid):
        print(line)
# 2026-10-19 03:11:02 signup id=ok_id!!!
# 2026-10-19 03:11:04 signup id=_admin
# 2026-10-19 03:11:05 signup id=ABC123
# 2026-10-19 03:11:07 signup id=a1

はじかれた4件はどれもルール違反です。ok_id!!! はビックリマーク、_admin はアンダースコアで始まり、ABC123 は大文字、a1 は短すぎます。

事件解決報告書 — IDチェッカーがre.searchで「一部が合えば通す」状態になっていた。re.fullmatchに直し、ルール違反の登録4件をブロックした。

ところが所長が、出力された4行の時刻を指でなぞっていきます。03:11:02、03:11:04、03:11:05、03:11:07。「5秒で4回。それも午前3時に。人間の手じゃない」チョン社長の顔がこわばります。偽アカウントは同じ日の明け方、いっせいに押し寄せていたのです。誰が送り込んだのかは、まだ誰も知りません。

ここまでが第1部です。1文字、1つの位置、回数、境界まで、「何を探すか」を書く方法はすべて学びました。第2部では、見つけたものから 必要なかけらを取り出す 方法を学びながら、この明け方の客を追いかけます。

練習問題

  1. re.match(r"\d{4}", "2026-10-19") と re.match(r"\d{4}", "#2026") の結果を予想してから、実行してみましょう。
  2. IDのルールに「アンダースコアで終わってはいけない」を加えるとしたら、パターンをどう変えますか。(ヒント:最後の文字を別に書きましょう)
  3. re.findall(r"\bcat\b", "cat concat cats") と re.findall(r"\Bcat", "cat concat cats") は、それぞれどのcatを見つけるでしょうか。

所長の手帳

  • ^ は始まり、$ は終わり。アンカーは文字ではなく位置にマッチする。

  • re.search はどこでも、re.match は先頭だけ、re.fullmatch は全体。入力検証は fullmatch。

  • $ は最後の改行の前でもマッチする。改行の例外なしで終わりを見るなら \Z(3.14からは \z も同じ)。

  • \b は \w と \W のあいだの境界、\B は境界ではない位置。ハングルも \w。

  • re.M を渡すと ^ $ が行ごとにマッチする。\A \Z は変わらず文字列全体が基準。

← 前へ

3限目 · 破れた電話帳:量指定子

次へ →

5限目 · バラバラな日付:グループと選択

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

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

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

質問と回答

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

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

R

ReflexKR

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

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

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

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

Built with Reflex