R

ReflexKR

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

時間 8:20–9:10

読めない正規表現:フラグとコンパイル

CASE-011

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

依頼書

読めない正規表現

依頼人

ハン・ビョル所長

証拠品

脅迫状3通の最後の行と、事務所4階の入退室記録。

解決条件

3行をつないで1行の正規表現にし、それを読める形に書き直して、何を探す正規表現なのかを明らかにする。

夜11時、事務所には明かりが1つだけ灯っています。所長が古い機械式キーボードの前に座り、3通の手紙の最後の行を打ち写しています。「1行で書いた正規表現は、書いた本人にしか読めない。3か月もたてば、その本人にも読めなくなる」

手紙3通の末尾を順に書き写したファイルと、所長が黙って取り出してきた事務所4階の入退室記録です。正規表現のかけらに door、4f、badge が見えたからです。

<<(?mi)^(\d{4})([-./])(\d\d)\2(\d\d)[ T]
(0[0-4]):(\d\d)\s+(?:door|gate)=4f\s+
badge=(f-0[12])\b(?!.*denied)>>
2026-10-19 22:41 door=4F badge=F-01 result=ok
2026-10-20 02:13 door=4F badge=S-07 result=ok
2026-10-20 02:58 door=4F badge=F-02 result=denied
2026-10-20 03:02 door=4F badge=F-02 result=ok
2026.10.20 03:05 gate=4F badge=f-02 result=ok
2026-10-20 09:00 door=4F badge=F-01 result=ok

正規表現の先頭にある (?mi) が最初の手がかりです。パターンの外側から、パターンの読まれ方を変えるスイッチ、フラグです。今日はフラグを1つずつオンにしながら、この1行を解きほぐします。

11-1. 大文字小文字と行:re.I、re.M (8分)

入退室記録を見ると、裏口(gate)の機械だけが入館証の番号を小文字の f-02 で記録しています。re.I(re.IGNORECASE)を付けると、大文字と小文字を区別しなくなります。

import re

re.findall(r"badge=F-02", door_log)          # ['badge=F-02', 'badge=F-02']
re.findall(r"badge=F-02", door_log, re.I)    # ['badge=F-02', 'badge=F-02', 'badge=f-02']

re.M(re.MULTILINE)は4限目でちょっと味見しただけでした。本来 ^ は文字列の先頭、$ は末尾でしかマッチしません。re.M をオンにすると、行ごとに先頭と末尾でマッチします。ここで door_log は上の入退室記録6行です。

re.findall(r"^\S+ 0[0-4]:\d\d", door_log)          # []
re.findall(r"^\S+ 0[0-4]:\d\d", door_log, re.M)
# ['2026-10-20 02:13', '2026-10-20 02:58', '2026-10-20 03:02', '2026.10.20 03:05']

re.findall(r"\w+$", door_log)          # ['ok']
re.findall(r"\w+$", door_log, re.M)    # ['ok', 'ok', 'denied', 'ok', 'ok', 'ok']

re.M がないと、1行目が夜10時なので深夜の記録が1つも出てきません。行単位のログを扱うとき、re.M はほぼ必須です。

証拠分析室

re.search(r"", text)

フラグ

戻り値

パターンは直さずに、フラグのボタンだけを押してみましょう。re.M をオンにすると行ごとに ^ $ がマッチし、re.I をオンにすると小文字の f-02 の行も入ってきます。

11-2. 窓を開ける re.S (6分)

次は、手紙の末尾3行を1つにつなげなければなりません。<< から >> までを捕まえればよさそうですが、何も出てきません。

m = re.search(r"<<(.*)>>", tails)
print(m)    # None

1限目からドットが言い続けてきたことです。. は改行を越えられません。<< の後ろの .* は1行目の終わりで止まり、そこには >> がありません。

所長が椅子を回して、窓を開けます。「ドット、出てごらん」。re.S(re.DOTALL)をオンにすると、. は改行も含めた本当にどんな文字でもになります。

m = re.search(r"<<(.*)>>", tails, re.S)
one_line = m.group(1).replace("\n", "")
print(one_line)
# (?mi)^(\d{4})([-./])(\d\d)\2(\d\d)[ T](0[0-4]):(\d\d)\s+(?:door|gate)=4f\s+badge=(f-0[12])\b(?!.*denied)

ドットが窓枠を蹴って外へ飛び出し、隣の窓から戻ってきて机の上に座ります。3行が1行になりました。

ドットのひとこと

窓が開きました! re.S をオンにすると、ぼくは改行も越えて次の行、そのまた次の行まで行けるんです。でも気をつけてくださいね。.* はもともと欲張りなので、窓が開くとテキストの終わりまでひとっ走りしちゃいます。複数行にまたがって短く捕まえたいなら .*? を使ってください。

証拠分析室

re.search(r"", text)

フラグ

戻り値

re.S のボタンを押して窓を開けてみましょう。. が改行を越えると、3行がまとめて捕まります。

re.M と re.S は名前が似ていてよく混同されますが、役割はまったく違います。

  • re.M は、^ と $ がマッチする位置を変えます(行ごと)。
  • re.S は、. が食べられる文字を変えます(改行を含む)。
re.findall(r"^.+$", "a\nb", re.M)    # ['a', 'b']
re.findall(r"^.+$", "a\nb", re.S)    # ['a\nb']

バックトラッカーの正規表現が (?mi) だけをオンにして s をオンにしていないのにも理由があります。末尾の (?!.*denied) は「この行の後ろのほうに denied があってはならない」という意味です。s をオンにすると .* が次の行まで越えていくので、下のどの行に denied があっても弾いてしまいます。

11-3. 書き下す:re.X と (?#...) (9分)

1行にはなりましたが、まだ読めません。re.X(re.VERBOSE)をオンにすると、パターンの中の空白と改行は無視され、# から行末まではコメントになります。だから、パターンを複数行に分けて書き、説明を添えられます。

所長がひとかけらずつ区切って打ち直します。番号付きグループには名前を付け、\2 は10限目で学んだ (?P=sep) に置き換えます。

DOOR = re.compile(r'''
    ^
    (?P<year>\d{4}) (?P<sep>[-./]) (?P<month>\d\d) (?P=sep) (?P<day>\d\d)   # 日付。区切り文字は1種類
    [ T]                                   # 日付と時刻の間:空白または T
    (?P<hour>0[0-4]) : (?P<minute>\d\d)    # 深夜0時台から4時台まで
    \s+ (?:door|gate) = 4f                 # 4階の表口または裏口
    \s+ badge = (?P<badge>f-0[12]) \b      # 入館証 F-01 または F-02
    (?! .* denied )                        # 拒否された記録は除く
''', re.I | re.M | re.X)

for m in DOOR.finditer(door_log):
    print(m["hour"], m["minute"], m["badge"])
# 03 02 F-02
# 03 05 f-02

同じ正規表現なのに、今度は1行ずつ読めます。深夜0時〜4時台に、4階のドアから、F-01 か F-02 の入館証で実際に入った記録を探す正規表現だったのです。

re.X にも例外があります。本物の空白や # を探したいときは、エスケープするか文字クラスに入れます。文字クラスの中の空白は消されません。上のパターンの [ T] がそれです。

re.fullmatch(r"\d{4} - \d\d", "2026-10", re.X)   # <re.Match object; span=(0, 7), match='2026-10'>
re.fullmatch(r"a b", "a b", re.X)                # None
re.fullmatch(r"a\ b", "a b", re.X)               # <re.Match object; span=(0, 3), match='a b'>
re.fullmatch(r"a[ ]b", "a b", re.X)              # <re.Match object; span=(0, 3), match='a b'>
re.fullmatch(r"a\#b", "a#b", re.X)               # <re.Match object; span=(0, 3), match='a#b'>

re.X なしで、パターンの途中に短いコメントだけを入れたいときは (?#...) を使います。かっこの中はまるごと無視されます(下の例のコメントは韓国語で「創立メンバーの入館証」)。

re.findall(r"F-0[12](?#창립 멤버 출입증)", door_log, re.I)
# ['F-01', 'F-02', 'F-02', 'f-02', 'F-01']

証拠分析室

re.search(r"", text)

フラグ

戻り値

空白を入れて書いたパターンです。re.X がないと空白も文字として扱われるので、何も見つかりません。re.X をオンにしてみましょう。[ ] の中の空白は re.X でも残ります。

所長は書き下したパターンを長いあいだ見つめています。「変数名ひとつなく、全部1行。このコメントを付ける癖……いや、コメントを付けない癖。あいつもいつも……」。そこで言葉を切ります。机の上に伏せてあった古い写真立てを引き出しにしまい、もう一度キーボードに手を置きます。「続けよう」

11-4. 文字の範囲:re.A、re.U、re.L (5分)

2限目で、str パターンの \w はハングルも、\d はアラビア数字の ٣ も含むと説明しました。Unicode が既定だからです。re.A(re.ASCII)をオンにすると、\w \W \d \D \s \S \b \B が ASCII の文字だけを見るようになります。

re.findall(r"\w+", "café 한글 F_02")          # ['café', '한글', 'F_02']
re.findall(r"\w+", "café 한글 F_02", re.A)    # ['caf', 'F_02']
re.findall(r"\d", "٣3")                       # ['٣', '3']
re.findall(r"\d", "٣3", re.A)                 # ['3']

(한글 は「ハングル」という意味の韓国語です。)

  • re.U(re.UNICODE)は str パターンの既定値なので、わざわざ書く必要はありません。古いコードとの互換性のために残っています。
  • re.L(re.LOCALE)は bytes パターンでしか使えず、コンピューターの地域設定によって結果が変わるので推奨されません。str パターンに付けるとエラーになります。
re.compile(r"\w", re.L)
# ValueError: cannot use LOCALE flag with a str pattern

証拠分析室

re.search(r"", text)

フラグ

戻り値

re.A をオンにしたりオフにしたりして、\w+ が何を取りこぼすかを見てみましょう。パターンを \d に変えて、アラビア数字の ٣ も確かめてみてください。

11-5. フラグを組み合わせる、オンにする、オフにする (6分)

フラグは | で組み合わせます。値は re.RegexFlag という列挙型で、整数のように扱えます。フラグなしを意味する re.NOFLAG(3.11 から、値は 0)もあるので、関数の既定値に使うと便利です。

flags = re.I | re.M
print(flags)          # re.IGNORECASE|re.MULTILINE
print(type(flags))    # <flag 'RegexFlag'>
print(int(flags))     # 10

def find_all(pattern, text, flags=re.NOFLAG):
    return re.findall(pattern, text, flags)

バックトラッカーのように、パターンの先頭に (?aiLmsux) のうち必要な文字を書いても構いません。(?im) は re.I | re.M と同じです。先頭以外に書くとエラーになります。

re.findall(r"(?im)^\S+ 0\d:\d\d(?=.*f-02)", door_log)
# ['2026-10-20 02:58', '2026-10-20 03:02', '2026.10.20 03:05']

re.compile(r"badge=(?i)f-02")
# re.PatternError: global flags not at the start of the expression at position 6

パターンの一部だけにフラグをかけたいなら、(?i:...) のようにかっこで範囲を区切ります。(?-i:...) はその範囲でだけオフにし、(?i-s:...) は i をオンにして s をオフにします。

re.findall(r"badge=(?i:f)-02", door_log)        # ['badge=F-02', 'badge=F-02', 'badge=f-02']
re.findall(r"(?i)BADGE=(?-i:F)-02", door_log)   # ['badge=F-02', 'badge=F-02']
re.fullmatch(r"(?s)a.(?i-s:b.)", "a\nB\n")      # None
re.fullmatch(r"(?s)a.(?i-s:b.)", "a\nBc")       # <re.Match object; span=(0, 4), match='a\nBc'>

a、L、u は文字の範囲を決めるフラグなので、範囲の中でオンにはできても、オフにはできません。

re.findall(r"(?a:\w+)", "café")    # ['caf']
re.compile(r"(?-a:\w+)")
# re.PatternError: bad inline flags: cannot turn off flags 'a', 'u' and 'L' at position 4

最後に、re.DEBUG は re がパターンをどう理解したかをそのまま表示してくれます。バックトラッカーの署名と、そこから外側の繰り返しを取り除いたパターンを比べてみましょう。以下は出力の最初の部分で、その下に実行命令のリストが続きます。

re.compile(r"(a+)+$", re.DEBUG)
# MAX_REPEAT 1 MAXREPEAT
#   SUBPATTERN 1 0 0
#     MAX_REPEAT 1 MAXREPEAT
#       LITERAL 97
# AT AT_END

re.compile(r"a+$", re.DEBUG)
# MAX_REPEAT 1 MAXREPEAT
#   LITERAL 97
# AT AT_END

繰り返し(MAX_REPEAT)の中に、また繰り返しが入っています。8限目の挑戦状の遅いパターンが、まさにこの形です。

11-6. re.compile と Pattern オブジェクト (6分)

所長が書き下したパターンを DOOR という名前に入れたように、re.compile はパターンを Pattern オブジェクトにします。モジュール関数と同じメソッド search match fullmatch findall finditer sub split を持ち、さらに検索範囲 pos と endpos も受け取ります。

p = re.compile(r"\d\d:\d\d")
line = "2026-10-20 03:02 door=4F"
print(p.search(line))              # <re.Match object; span=(11, 16), match='03:02'>
print(p.match(line))               # None
print(p.match(line, 11))           # <re.Match object; span=(11, 16), match='03:02'>
print(p.fullmatch(line, 11, 16))   # <re.Match object; span=(11, 16), match='03:02'>
print(p.findall(line, 0, 15))      # []
print(len(list(p.finditer(line, 11))))   # 1

pos は文字列を切り取るのとは違います。^ は pos の位置ではなく、文字列の本当の先頭(re.M なら行の先頭)でしかマッチしません。

q = re.compile(r"^\d\d")
print(q.match(line, 11))     # None
print(q.match(line[11:]))    # <re.Match object; span=(0, 2), match='03'>

Pattern オブジェクトは自分自身の情報も持っています。str パターンの flags には re.UNICODE(32)が自動で加わります。

print(DOOR.groups)       # 7
print(DOOR.groupindex)
# {'year': 1, 'sep': 2, 'month': 3, 'day': 4, 'hour': 5, 'minute': 6, 'badge': 7}
print(DOOR.flags)        # 106
print(re.RegexFlag(DOOR.flags))   # re.IGNORECASE|re.UNICODE|re.MULTILINE|re.VERBOSE
print(re.compile("x").flags)      # 32
print(re.compile(one_line).pattern == one_line)   # True
  • モジュール関数 re.search(パターン, ...) も、内部でコンパイルしています。最近使ったパターンは内部キャッシュに残るので、再コンパイルはされません。re.purge() はこのキャッシュを空にします(使う場面はまれです)。
  • ですから、re.compile の本当の利点は速度よりも名前です。DOOR と名付けたパターンは、コードの中で読めるものになります。
  • 型ヒントは re.Pattern[str]、re.Match[str] と書きます(bytes パターンなら [bytes])。
def night_entries(p: re.Pattern[str], log: str) -> list[re.Match[str]]:
    return list(p.finditer(log))

print(len(night_entries(DOOR, door_log)))   # 2

事件解決 (10分)

書き下したパターンで、入退室記録を読み直します。

for m in DOOR.finditer(door_log):
    print(f"{m['year']}-{m['month']}-{m['day']} {m['hour']}:{m['minute']} {m['badge'].upper()}")
# 2026-10-20 03:02 F-02
# 2026-10-20 03:05 F-02

手紙から取り出した元の1行も、まったく同じ2行を見つけます。意味は同じで、違うのは読めるかどうかだけです。

[m.group(7) for m in re.finditer(one_line, door_log)]   # ['F-02', 'f-02']

10月20日の深夜、02:58 に表口で一度拒否された F-02 の入館証が、4分後にもう一度かざされて中に入り、その3分後には裏口を通っていました。バックトラッカーは「おれがいつ来たのか、読めるものなら読んでみろ」と、この正規表現を送ってきたのです。

所長が入館証の名簿を広げます。「F-01 は私のだ。F-02 は……」。名簿のその欄には、名前の代わりに二重線が引かれています。「10年前に無効にしておくべきだった。できなかったんだよ」

事件解決報告書 — 3通の手紙の末尾は、1つの正規表現だった。書き下した結果、2026-10-20 03:02〜03:05 に入館証 F-02 が事務所4階に出入りしたことを確認。F-02 は創立当時に発行された入館証で、本日付で停止した。

所長は最後に、元の1行を画面に映したまま言います。「同じ正規表現だ。片方は書いた本人にしか読めず、もう片方は誰にでも読める。私たちは、誰にでも読めるほうで書く」

練習問題

  1. 4限目のIDのルール [a-z][a-z0-9_]{3,15} を re.X で書き下し、かけらごとにコメントを付けてみましょう。
  2. re.compile(r"\d+") の pos を使って、"no.17 at 03:02" から at の後ろに出てくる数字だけを探してみましょう。
  3. re.compile(r"badge=(?i)f-02") のエラーを確かめたあと、範囲フラグ (?i:...) を使って同じ意味になるように直してみましょう。

バックトラッカーの痕跡

(a+)+$

書き下したか。次のは書き下すことすらできないだろう。明日の朝9時、メールボックスを開け。

所長の手帳

  • re.I は大文字小文字を無視、re.M は ^ $ が行ごと、re.S は . が改行まで。M と S は役割が違う。

  • re.X:空白と # コメントを無視する。文字クラスの中の空白と \ (バックスラッシュ+空白)は残る。(?#...) はどこでもコメント。

  • re.A:\w \d \s \b を ASCII に。str は re.U が既定、re.L は bytes 専用で非推奨。

  • 組み合わせは re.I | re.M(re.RegexFlag)、なしは re.NOFLAG。インラインの (?im) は先頭にだけ。

  • 範囲フラグ (?i:...) (?-i:...) (?i-s:...)。a L u はオフにできない。re.DEBUG でパターンの中身を見る。

  • re.compile は名前を付ける道具。pos endpos を受け取るが、^ は本当の先頭でだけ。属性は flags groups groupindex pattern。

← 前へ

10限目 · 同じ手口:後方参照と条件付きパターン

次へ →

12限目 · バックトラッカーの正体:総まとめとトークナイザー

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

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

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

質問と回答

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

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

R

ReflexKR

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

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

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

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

Built with Reflex