CASE-007
グレップ・アンド・コー データ探偵事務所
依頼書
依頼人
流出事件を追うオ記者
証拠品
匿名の情報提供者から届いた顧客名簿ファイル。マーケット・チョンの会員リストらしい。区切り文字が行ごとにバラバラ。
解決条件
明日の朝刊に載せられるよう、電話番号の真ん中を伏せ、日付を 2026-10-20 の形に直し、行ごとに項目を切り分ける。
夜9時、事務所のドアが勢いよく開きます。オ記者が息を切らしながらノートパソコンを広げます。「タレコミが入りました。マーケット・チョンの顧客名簿です。明日の朝には記事を出さなきゃいけないんですが、個人情報をそのまま載せるわけにはいかないでしょう」
所長が画面をのぞき込みます。6限目に突き止めた、午前3時のあの会員リストです。「探すのはもう終わった。今日は書き換えて、切り分ける日だよ」
id, name; phone | joined
1001,kim,010-0000-5678,20/10/2026
1002; lee; 010-0000-1234; 19/10/2026
1003 | park | 010-0000-9012 | 03/09/2026
1004, choi;010-0000-3456 |28/02/2026
# (a+)+$最後の行に妙な文字が見えますが、締め切りに追われるオ記者はそのまま読み飛ばします。
re.sub(パターン, 置換文字列, 元の文字列) は、パターンにマッチした箇所をすべて書き換えた新しい文字列を返します。元の文字列はそのままです。Python の文字列は変更できない値だからです。
import re
re.sub(r"\d{4}", "****", "010-0000-5678") # '010-****-****'4桁の数字が2か所あるので、末尾まで伏せられてしまいました。先頭の3桁と末尾の4桁を残して真ん中だけ伏せたいなら、残したい部分をグループで捕まえておき、置換文字列の中で呼び出します。\1 は1番グループが捕まえた文字です。
re.sub(r"(\d{3})-\d{4}-(\d{4})", r"\1-****-\2", "010-0000-5678") # '010-****-5678'置換文字列にもバックスラッシュが入るので、パターンと同じく r"..." で書きます。
証拠分析室
re.search(r"", text)
戻り値
名簿の日付は 20/10/2026 のように日/月/年の順です。記事には 2026-10-20 の形で載せなければなりません。グループの順番を入れ替えればよいのです。
re.sub(r"(\d\d)/(\d\d)/(\d{4})", r"\3-\2-\1", "20/10/2026") # '2026-10-20'
re.sub(r"(\d\d)/(\d\d)/(\d{4})", r"\g<3>-\g<2>-\g<1>", "20/10/2026") # '2026-10-20'\3 と \g<3> は同じ意味です。6限目の名前付きグループは \g<名前> で呼び出します。順番を覚えなくて済むので、ミスが減ります。
re.sub(r"(?P<d>\d\d)/(?P<m>\d\d)/(?P<y>\d{4})", r"\g<y>-\g<m>-\g<d>", "20/10/2026") # '2026-10-20'\g<0> はマッチ全体です。見つけたものを消さずに、囲むだけのときに使います。
re.sub(r"\d{3}-\d{4}-\d{4}", r"[\g<0>]", "1001,kim,010-0000-5678") # '1001,kim,[010-0000-5678]'グループ番号のすぐ後ろに数字を続けたいときは要注意です。\10 は「1番グループの後ろに 0」ではなく、10番グループになります。こういうときは \g<1>0 のように山かっこで番号を閉じます。
re.sub(r"(\d+)", r"\g<1>0", "kim 5") # 'kim 50'
re.sub(r"(\d+)", r"\10", "kim 5") # re.PatternError: invalid group reference 10 at position 1置換文字列の中でも \n は改行文字になります。一方、\q のように意味のない英字エスケープはエラーです。
re.sub(r",", r"\n", "a,b") # 'a\nb'
re.sub(r",", r"\q", "a,b") # re.PatternError: bad escape \q at position 0re.PatternError は 3.13 から使われている名前で、昔の名前 re.error と同じ例外です。詳しくは12限目で扱います。
証拠分析室
re.search(r"", text)
戻り値
置換文字列の代わりに関数を渡すこともできます。re.sub はマッチが見つかるたびにその Match オブジェクトを関数に渡し、関数が返した文字列で置き換えます。名前の最初の1文字だけを残し、残りの長さぶん伏せ字にしてみましょう。決まった型の置換文字列ではできない芸当です。
def hide(m):
name = m.group(1)
return "," + name[0] + "*" * (len(name) - 1) + ","
re.sub(r",([a-z]+),", hide, "1003,park,010-0000-9012,03/09/2026") # '1003,p***,010-0000-9012,03/09/2026'
re.sub(r"[a-z]+", lambda m: m[0].upper(), "kim and lee") # 'KIM AND LEE'TypeError になります。count は、先頭から何か所まで書き換えるかを決めます。既定値の 0 は「すべて」です。re.subn は書き換えた文字列と書き換えた回数をタプルで返します。冒頭の名簿が leak という文字列に入っているとしましょう。
re.sub(r"\d{3}-\d{4}-\d{4}", "***", leak, count=1).splitlines()[1:3]
# ['1001,kim,***,20/10/2026', '1002; lee; 010-0000-1234; 19/10/2026']
re.subn(r"\d{3}-\d{4}-\d{4}", "***", leak)[1] # 4これで記事に「電話番号4件を伏せた」と正確な数字を書けます。
名簿は行ごとに区切り文字が違います。カンマ、セミコロン、縦棒が混ざり、前後の空白もまちまちです。str.split が受け取れる区切り文字は1つだけ。re.split ならパターンで切り分けられます。
re.split(r"\s*[,;|]\s*", "a, b;c |d") # ['a', 'b', 'c', 'd']
re.split(r"\s*[,;|]\s*", "a, b;c |d", maxsplit=2) # ['a', 'b', 'c |d']\s* が空白まで区切りとして飲み込むので、きれいな項目が取り出せます。maxsplit=2 は2回までしか切らず、残りは最後の項目にそのまま残します。パターンにキャプチャグループがあると、切り取った区切り文字も結果のリストに挟み込まれます。どの区切り文字だったかを記録したいときに使います。不要なら (?:...) で書きます。
re.split(r"\s*([,;|])\s*", "a, b;c |d") # ['a', ',', 'b', ';', 'c', '|', 'd']文字を消費しない空マッチでも切り分けられます。\b は単語の境界の位置にだけマッチするので、単語と空白の間ごとに切れます。両端にも境界があるため、空文字列ができます。
re.split(r"\b", "a b") # ['', 'a', ' ', 'b', '']証拠分析室
re.search(r"", text)
戻り値
re.sub の4番目の位置は count です。フラグを渡すつもりでうっかり re.I を4番目に書くと、フラグではなく回数として渡ってしまいます。re.I の実際の値が 2 だからです。
re.sub("a", "b", "aAaA", re.I) # 'bAbA' ← 大文字小文字は無視されず、2回だけ置換
re.sub("a", "b", "aAaA", flags=re.I) # 'bbbb'このミスがあまりに多いので、Python 3.13 からは count、maxsplit、flags を位置引数で渡すと DeprecationWarning が出るようになりました。上の1行目は 'count' is passed as positional argument という警告を出します。re.split の maxsplit、flags も同様です。いつでも count=1、maxsplit=2、flags=re.I のように名前を付けて書きましょう。
ドットのひとこと
ぼくだって「おすわり」と言われたら座りますけど、ただ数字の2を呼ばれても、何をすればいいのかわかりません。関数も同じなんです。re.sub("a", "b", s, re.I) は「2回だけ書き換えて」と受け取っちゃいます。flags=re.I って名前で呼んであげてくださいね。
伏せる、日付を直す、切り分ける、を順につなぎます。最後の行と見出し行は除き、顧客の行だけを切り分けます。
masked = re.sub(r"(\d{3})-\d{4}-(\d{4})", r"\1-****-\2", leak)
fixed = re.sub(r"(?P<d>\d\d)/(?P<m>\d\d)/(?P<y>\d{4})", r"\g<y>-\g<m>-\g<d>", masked)
for row in fixed.splitlines()[1:-1]:
print(re.split(r"\s*[,;|]\s*", row))
# ['1001', 'kim', '010-****-5678', '2026-10-20']
# ['1002', 'lee', '010-****-1234', '2026-10-19']
# ['1003', 'park', '010-****-9012', '2026-09-03']
# ['1004', 'choi', '010-****-3456', '2026-02-28']オ記者が表を受け取って立ち上がろうとしたとき、所長が手を上げます。「待って。最後の行」
print(fixed.splitlines()[-1]) # # (a+)+$名簿のデータではありません。誰かがわざと残した正規表現です。この文字をそのまま探すには、1限目で学んだとおりメタ文字を一つずつ無効にしなければなりません。
print(re.escape("(a+)+$")) # \(a\+\)\+\$
re.search(r"\(a\+\)\+\$", leak) # <re.Match object; span=(177, 183), match='(a+)+$'>証拠分析室
re.search(r"", text)
戻り値
「これは何です?」とオ記者が尋ねます。所長はしばらく黙ってから答えます。「引き返しの罠。これを読んだ正規表現エンジンは、同じ道を何度も何度も引き返して、そのうち止まってしまう。明日の記事からは外しておいて」
オ記者が帰ったあと、所長は引き出しから色あせた写真立てを取り出します。事務所を開いた最初の年の写真です。しばらく見つめてから、写真立てを机の上に伏せて置きます。
事件解決報告書 — 流出名簿4件の電話番号の真ん中を伏せ、日付を年-月-日に統一して記者に渡した。名簿の末尾に、意図的に仕込まれた正規表現 (a+)+$ を発見。出所不明。別途調査が必要。
1001〜1004 を、#1001 のように先頭に # を付けた形に書き換えてみましょう。\g<0> を使えば1行で書けます。re.split(r"(\s*)([,;|])(\s*)", "a, b") の結果を予想してみましょう。グループが3つあると、いくつの項目が割り込むでしょうか。"price 1500 and 300" の数字をすべて2倍に書き換えてみましょう。関数は文字列を返す必要があることを忘れずに。所長の手帳
re.sub(パターン, 置換文字列, 元の文字列) は新しい文字列を返す。元はそのまま。
置換文字列では \1 = \g<1>、名前は \g<name>、全体は \g<0>。数字が続くなら \g<1>0(\10 は10番グループ)。
置換文字列の \n は改行、\q のような未知のエスケープはエラー。
関数による置換:Match を受け取って文字列を返す関数。count で回数を制限、re.subn は(結果, 回数)。
re.split はパターンで切り分ける。maxsplit あり。キャプチャグループがあると区切り文字も結果に入る。
count、maxsplit、flags はキーワードで。位置引数は 3.13 から DeprecationWarning。re.I が count=2 として渡る事故に注意。
新しい授業・講座のお知らせを受け取る
この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。
R
ReflexKR
触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。
ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する
アフィリエイトと支援について© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
質問と回答
この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。