CHAPTER 5 ・ ファイルとテキストの実践 / Lesson 20
文字のパターンで探す(正規表現)
請求書番号・電話番号・金額を、文章の中から抜き出す。よく使う型だけを、使えるようにします。
このレッスンが終わったら
「正規表現は、文字の並びのパターンを書く方法。re.search で探し、() で必要な部分だけ取り出せる」と言える。
こんな場面、ありませんか?
営業のメモや、メールの文面の中から、請求番号・電話番号・金額だけを抜き出したい。そんな場面を考えます。次の notes.txt は、営業担当のメモです。
【10/03】アオイ電機 経理部 山田様 TEL 03-1234-5678 請求書 INV-2025-0001 を送付。金額は 1,320,000円 です。 【10/05】ミドリ商事 佐々木様 携帯 090-1111-2222 見積 Q-2025-014 の件。金額 484,000円(税込)。納期は 11月15日。 【10/09】サクラ印刷 田村様 06-6543-2109 請求書 INV-2025-0003 の入金予定は 10月29日。792,000円。 【10/12】タチバナ工業 中村様 06-6000-1234 INV-2025-0004 の金額が違うとの連絡あり。231,000円のはず。
請求番号(INV-2025-0001 のような形)は、基礎編の道具で取り出せるでしょうか。INV- の位置を find で探して、決まった長さで切り出してみます。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
請求番号は取れました。ただし、これは「INV- で始まる」「13文字」と分かっているから、うまくいっただけです。電話番号(03-1234-5678 も 090-1111-2222 もある)や、金額(桁数が違う)には、位置も長さも決まった目印がなく、この方法は使えません。
こうした「形は決まっているが、中身は毎回違う」文字を探すのが、正規表現(せいきひょうげん)の役目です。
パターンで探してみる
正規表現では、探したい文字の形(パターン)を書きます。「INV-、数字4桁、ハイフン、数字4桁」という形を、次のように書きます。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
3行とも、メモ全体から、形に合うものがすべて取り出されました。re.findall は「合うものを全部、リストで返す」道具です。最初の1つだけ欲しいときは re.search を使います。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
re.search は、見つかると「見つかった情報」(Match)を返し、.group() で中身の文字が取れます。
仕組みを読む
パターンは、記号を組み合わせた文字の並びの設計図です。INV-\d{4}-\d{4} を分けてみます。
INV-そのままの文字\d数字1文字{4}を、ちょうど4回-そのままの文字\d{4}数字4桁最初の r"..." の r は、バックスラッシュ(\)をそのままの文字として扱う目印(raw文字列)です。正規表現では \d を多用するので、r を付けて書くのが決まりです。
よく使う記号を、表にまとめます。
| 書き方 | 意味 | 例 |
|---|---|---|
\d | 数字1文字 | \d\d は「12」に合う |
\d+ | 数字が1文字以上続く | 「2025」全体に合う |
\d{3,4} | 数字が3〜4桁 | 「123」「1234」に合う |
[A-Z] | 英大文字1文字 | [A-Z]+ は「INV」に合う |
[\d,] | 数字かカンマ1文字 | [\d,]+ は「1,320,000」に合う |
( ) | 取り出す部分をまとめる | Step 4 で使います |
「または」は INV|Q のように縦線で書きます。Step 2 の1行目を、日本語にしてみます。
import re正規表現の道具(re)を使うre.findall(r"INV-\d{4}-\d{4}",「INV-」のあと、数字4桁、ハイフン、数字4桁、という形のものを全部 text)この文章の中から探して、リストで受け取る日本語にすると
記号は他にもたくさんありますが、実務で必要なのは、上の表の形がほとんどです。「近い形を見つけて少し直す」「AIに書いてもらって確かめる」(Step 6)で十分なので、全部は覚えなくて大丈夫です。
変えて、試す
① 一部分だけ取り出す(グループ)。 金額の「円」は不要です。取り出したい部分を ( ) で囲むと、グループになり、findall はその部分だけを返します。カンマを外して数字にし、合計も出してみます。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
② 名前を付けて取り出す/置き換える。 グループには (?P<名前>...) で名前を付けられ、group("名前") で取れます。re.sub は「合う部分を別の文字に置き換える」道具です。
▶ 実行を押すと、ここに結果が出ます
\1 \2 \3 は、1番目・2番目・3番目のグループの中身です。ここでは、電話番号のハイフンを取り除きました。
③ 欲張りマッチの落とし穴。 .(なんでも1文字)と *(0回以上)を組み合わせた .* は、できるだけ長く合わせようとします。「できるだけ短く」にしたいときは .*? と書きます。
▶ 実行を押すと、ここに結果が出ます
1行目は、最初の 【 から最後の 】 までを取ってしまいました。これが欲張りマッチです。
④ 実務の突き合わせ。 銀行の入金明細(payments.csv)の摘要から請求番号を取り出し、請求の一覧(invoices_billed.csv)と比べて、まだ入金のない請求を探します。(集合の差は Lesson 07 で学びます。)
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
4件の請求番号が、未入金として出ました。なお、摘要は半角カナ混じりですが、請求番号の部分は半角の英数字なので、そのまま探せています。
⑤ 見つからないとき。 re.search は、見つからないと None を返します。そのまま .group() を呼ぶと、エラーで止まります。
▶ 実行を押すと、ここに結果が出ます
AttributeError: 'NoneType' object has no attribute 'group' と出ます。④のように if m: で、見つかったか確かめてから使います。
自分で書く
お題① notes.txt から、請求番号(INV- で始まるもの)をすべて取り出して、リストで表示してください。(Q-2025-014 のような見積番号は含めません)
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
ヒントを見る
re.findall(r"INV-\d{4}-\d{4}", text) を print します。
答えの例を見る
import re
with open("notes.txt", encoding="utf-8") as f:
text = f.read()
print(re.findall(r"INV-\d{4}-\d{4}", text))お題② notes.txt に出てくる金額(「〇〇円」)の合計を、数字で表示してください。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
ヒントを見る
re.findall(r"([\d,]+)円", text) で、数字とカンマだけが取れます。replace(",", "") でカンマを外し、int() に直して足します。
答えの例を見る
import re
with open("notes.txt", encoding="utf-8") as f:
text = f.read()
amounts = [int(x.replace(",", "")) for x in re.findall(r"([\d,]+)円", text)]
print(sum(amounts))お題③ payments.csv の摘要のうち、請求番号が書かれていない行は何行ありますか。件数を表示してください。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
ヒントを見る
m = re.search(r"INV-\d{4}-\d{4}", row["摘要"]) として、if m is None: のときに count = count + 1 とします。
答えの例を見る
import csv
import re
count = 0
with open("payments.csv", encoding="utf-8") as f:
for row in csv.DictReader(f):
if re.search(r"INV-\d{4}-\d{4}", row["摘要"]) is None:
count = count + 1
print(count)仕事ならどう使う?
理解チェックre.findall(r"\d+", "INV-2025-0001") の結果は、どれでしょう?
理解チェックre.search で何も見つからなかったとき、返ってくるものは?