ちくわ君と学ぶ正規表現

森の研究所で、ちくわ君は大量のテキストファイルに囲まれて、めずらしく弱った顔をしていた。机の上には赤い木の実が散らばっていて、いつも以上に根を詰めて作業をしているのがうかがえる。

「ちくわさん、おはようございます!」はんぺん君が元気よく研究所に飛び込んできた。「今日はなんだか大変そうですね」

「おはよう、はんぺん君」ちくわ君は疲れた表情で振り返った。「実はね、何千行もあるログファイルの中から、エラーの記録だけを拾い出したいんだ。でも目で探して手で写していたら、日が暮れてしまうよ」

「うわぁ、それは気が遠くなりますね……。何か近道はないんですか?」

「あるんだよ。今日はとっておきの道具を教えよう」ちくわ君は木の実を一つ口に含んで、にやりと笑った。「『正規表現』というんだ。文字列の中から『こんな形のもの』を探し出す、魔法みたいな書き方さ」

正規表現ってなんだろう

「魔法、ですか?」はんぺん君は目を丸くした。

「大げさに聞こえるかもしれないけれどね」ちくわ君はホワイトボードに向かった。「たとえば『メールアドレスを全部見つけたい』『日付の形をした文字を探したい』というとき、ふつうの検索だと一つひとつ言葉を指定しないといけない。でも正規表現を使えば、『@の前後に文字が並んでいるもの』みたいに“形”そのものを指定できるんだ」

「形を指定する……。なんだか想像がつきません」

「百聞は一見にしかず、だね。さっそく一番やさしい例を動かしてみよう」ちくわ君はPythonを起動した。Pythonでは re という標準の道具箱を読み込むと、正規表現が使えるようになる。

import re

text = "今日はcat、昨日はcut、明日はcotの話です"
pattern = r'c.t'   # c と t の間に「何か1文字」
matches = re.findall(pattern, text)
print(matches)
# 出力: ['cat', 'cut', 'cot']

「おお!」はんぺん君は身を乗り出した。「catcutcot も、まとめて捕まえましたね。その真ん中の『.』が、『なんでもいい1文字』っていう意味なんですか?」

「その通り!のみ込みが早いね」ちくわ君は嬉しそうに頷いた。「正規表現には、こういう『特別な意味を持つ記号』がいくつもあってね。これを組み合わせて“形”を描いていくんだ。記号のことを『メタ文字』と呼ぶよ」

「何回くりかえす?」を表す記号

「次は、回数を表す記号を覚えよう」ちくわ君は新しい例を書いた。「* は『0回以上』、+ は『1回以上』、? は『0回か1回』。文字のうしろに付けて、その文字が何回くりかえせるかを表すんだ」

text = "a ab abb abbb"

print(re.findall(r'ab*', text))   # b が 0回以上
# 出力: ['a', 'ab', 'abb', 'abbb']

print(re.findall(r'ab+', text))   # b が 1回以上
# 出力: ['ab', 'abb', 'abbb']

print(re.findall(r'ab?', text))   # b が 0回か1回
# 出力: ['a', 'ab', 'ab', 'ab']

はんぺん君は必死にメモを取った。「* は『あってもなくてもいいし、たくさんあってもいい』、+ は『最低でも1つは欲しい』……。なるほど、欲ばりさんが + で、おおらかなのが * ですね」

「ふふ、そういう覚え方もいいね」ちくわ君は笑った。

「数字だけ」「英字だけ」を選ぶ

「もうひとつ大事なのが『文字クラス』だ」ちくわ君は続けた。「角かっこ [ ] の中に文字を並べると、『このどれか1文字』という意味になる。[abc] なら a か b か c だね。範囲も書けて、[0-9] は数字、[A-Za-z] は英字を表すよ」

「よく使うものには、もっと短い書き方もある。数字は \d、その後ろに + を付ければ『数字の並び』を丸ごと拾えるんだ」

text = "電話 03-1234-5678 / ID: ABC-123"

print(re.findall(r'\d+', text))          # 数字の並び
# 出力: ['03', '1234', '5678', '123']

print(re.findall(r'[A-Za-z]+', text))    # 英字の並び
# 出力: ['ID', 'ABC']

print(re.findall(r'[0-9]{2,4}', text))   # 2〜4桁の数字
# 出力: ['03', '1234', '5678', '123']

「最後の {2,4} は何ですか?」はんぺん君が指さした。

「いい質問だ。さっきの *+ の仲間で、『2回以上4回以下』と回数を細かく決める書き方だよ。桁数が決まっているものを探すときに重宝するんだ」

実戦:メールアドレスを拾う

「道具がそろってきたね。じゃあ実際の文章から、メールアドレスを抜き出してみよう」ちくわ君は新しいファイルを開いた。

text = "問い合わせは sales@example.com か support@example.co.jp まで"

emails = re.findall(r'[\w.+-]+@[\w.-]+\.\w+', text)
print(emails)
# 出力: ['sales@example.com', 'support@example.co.jp']

「うわぁ、ちゃんと2つとも見つかりました!」はんぺん君は拍手した。「でも、この呪文みたいなのはどう読むんですか?」

「分解すれば怖くないよ」ちくわ君は一区切りずつ指でなぞった。「[\w.+-]+ が『@の前の名前の部分』。\w は英数字とアンダースコアのことだ。次に @、そして [\w.-]+ が『会社名の部分』、最後の \.\w+ が『.com や .jp』。形をそのまま言葉にしただけなんだ」

「本当だ……。一個ずつ見ると、ちゃんと意味がありますね」

グループ化で「部品」に分ける

「正規表現の強力なところは、見つけるだけじゃなく、見つけたものを『部品ごとに取り出せる』ことなんだ」ちくわ君は本題のログファイルを開いた。「丸かっこ ( ) で囲んだところは、あとから別々に取り出せる。これを『グループ化』というよ」

log = "2024-06-17 10:30:15 [ERROR] データベース接続失敗"

m = re.match(r'(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) \[(\w+)\] (.*)', log)
print(m.group(1))   # 日付
print(m.group(3))   # 種類
print(m.group(4))   # 本文
# 出力:
# 2024-06-17
# ERROR
# データベース接続失敗

「すごい!日付と、エラーの種類と、メッセージが、きれいに分かれて取り出せました」はんぺん君は感激した。「これなら、種類が ERROR の行だけ集める、なんてことも簡単そうですね」

「まさにそれが、僕が今日やりたかった作業なんだ」ちくわ君は満足そうに頷いた。「丸かっこで囲った順に、group(1)group(2)……と番号で呼び出せる。文章をきれいな表に整えるときの定番だよ」

置きかえる:re.sub

「最後に、見つけた部分を『書きかえる』方法も見ておこう」ちくわ君は付け加えた。「re.sub を使うと、形に合った場所を別の文字に置きかえられる。しかも、さっきのグループを使って並べ替えることもできるんだ」

text = "2024-06-17"

# \1 \2 \3 でグループを呼び出して並べ替え
print(re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\3/\2/\1', text))
# 出力: 17/06/2024

「年・月・日が、日・月・年の順に入れかわりました!」はんぺん君は目を輝かせた。「置きかえながら順番まで変えられるなんて、まるで手品です」

森にひろがる、効率の知恵

夕方になった頃、はんぺん君は満足そうに伸びをした。「今日は正規表現について本当にたくさん学べました。.*[ ]、それに丸かっこ……。一つひとつは小さな記号なのに、組み合わせるとこんなに賢く探せるんですね」

「そうなんだ。最初は記号の多さに圧倒されるかもしれないけれど、一度なじんでしまえば、テキスト処理の頼もしい相棒になるよ」ちくわ君は微笑んだ。「ただし、ひとつだけ忠告がある。なんでもかんでも正規表現で解こうとしないことだ。複雑になりすぎたら、プログラムで段階的に処理したほうが、ずっと読みやすいこともあるからね」

「なるほど、道具は使いどころが大事なんですね。家に帰ったら、さっそく自分のメモ帳のテキストで試してみます!」はんぺん君は意気込んだ。

「それは何よりだ。まずはやさしいパターンから始めて、少しずつ難しいものへ進んでいこう。正規表現は、結局のところ手を動かして覚えるのが一番なんだよ」

はんぺん君が帰ったあと、ちくわ君はさっきのログファイルに正規表現を走らせた。何時間もかかるはずだったエラー集めが、たった数秒で片づいてしまう。画面いっぱいに並んだ結果をながめて、ちくわ君は静かに木の実をかじった。森の研究所には、今日も小さな効率の知恵が満ちていた。

教育 正規表現 Python テキスト処理