0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonの正規表現で日本語の電話番号・郵便番号をマスクするときの落とし穴 \b・\d・NFKC・ハイフン11種を実際に試した

0
Posted at

何を確かめたか

生成AIに文章を貼る前に、電話番号や郵便番号を正規表現で [電話] などに置き換える前処理はよく使われます。ところが、英語圏の例をそのまま日本語の文章に使うと、一致すべきものが一致しない・一致してほしくないものが一致することがあります。

この記事では、Python標準の re と unicodedata で、日本語の文章に特有の挙動を小さなコードで確かめました。対象は次の4つです。

  1. \b(単語境界)が日本語の直後で効かない
  2. \d が全角数字にも一致する
  3. NFKC正規化で直るハイフンと、直らないハイフン
  4. 数字の並びが本物らしいかを確かめる検査用数字(Luhn・マイナンバー)

数字やメールアドレスはすべて架空です。

環境

項目 内容
Python 3.13.5(標準ライブラリのみ)
OS macOS 26.6.2
実行日 2026-10-16

1. \b は日本語の直後で境界にならない

Pythonの re は、文字列パターンでは \w がUnicodeの文字(漢字・かなを含む)に一致します。\b は「\w と \W の境目」なので、「電話090-…」のように漢字と数字がくっついていると、そこは境界になりません。

import re

s = "電話090-1234-5678まで"
print(re.findall(r"\b090-\d{4}-\d{4}\b", s))        # []
print(re.findall(r"\b090-\d{4}-\d{4}\b", "電話 090-1234-5678 まで"))  # ['090-1234-5678']
print(re.findall(r"(?a)\b090-\d{4}-\d{4}\b", s))    # ['090-1234-5678']
print(re.findall(r"(?<!\d)090-\d{4}-\d{4}(?!\d)", s))  # ['090-1234-5678']
パターン 「電話090-1234-5678まで」 「電話 090-1234-5678 まで」
\b…\b 一致しない 一致
(?a)\b…\b(ASCIIモード) 一致 一致
(?<!\d)…(?!\d)(前後が数字でない) 一致 一致

英語の例では \b が「前後に余計な数字がない」ことの確認に使われますが、日本語ではスペースを入れない書き方が普通なので、そのままではマスクが素通りします。意図が「前後に数字が続かない」なら、(?<!\d) と (?!\d) で書くほうが意味どおりです。(?a) は \w もASCIIだけになるので、パターン全体の意味が変わる点に注意します。

2. \d は全角数字にも一致する

s = "〒123-4567"
print(re.findall(r"\d{3}-\d{4}", s))        # ['123-4567']
print(re.findall(r"[0-9]{3}-[0-9]{4}", s))  # []

\d はUnicodeの10進数字(全角数字やアラビア文字の数字など)すべてに一致します。マスクの目的なら全角も拾えるので都合がよい場面もありますが、[0-9] と書き換えた瞬間に全角が素通りします。どちらの意味で書いているかを決めて、全角を半角にそろえてから [0-9] で書くほうが、後から読んでも挙動が分かります。

3. NFKC正規化で直るハイフン、直らないハイフン

全角を半角にそろえるには unicodedata.normalize("NFKC", s) が定番です。全角数字・全角英字・全角の @ . は半角になります。

import unicodedata
unicodedata.normalize("NFKC", "yamada@example.com")  # 'yamada@example.com'
unicodedata.normalize("NFKC", "〒123-4567")                    # '〒123-4567'

ただし、ハイフンに見える文字の多くはNFKCでも - になりません。電話番号「03?1234?5678」の ? に11種類のハイフン類を入れて、半角ハイフンだけのパターンで一致するかを確かめました。

import re, unicodedata

H = ["-","‐","‑","‒","–","—","―","−","ー","-","ー"]
cls = "[-‐-―−ー-ー]"
pat_plain = re.compile(r"(?<!\d)0\d{1,4}-\d{1,4}-\d{4}(?!\d)")
pat_wide = re.compile(rf"(?<!\d)0\d{{1,4}}{cls}\d{{1,4}}{cls}\d{{4}}(?!\d)")

for h in H:
    s = f"03{h}1234{h}5678"
    n = unicodedata.normalize("NFKC", s)
    print(f"U+{ord(h):04X}", unicodedata.name(h), f"NFKC->U+{ord(n[2]):04X}",
          bool(pat_plain.search(s)), bool(pat_plain.search(n)), bool(pat_wide.search(s)))
文字 Unicode名 NFKC後 半角パターン NFKC後に半角パターン ハイフン類を並べたパターン
U+002D HYPHEN-MINUS U+002D 一致 一致 一致
U+2010 HYPHEN U+2010 × × 一致
U+2011 NON-BREAKING HYPHEN U+2010 × × 一致
U+2012 FIGURE DASH U+2012 × × 一致
U+2013 EN DASH U+2013 × × 一致
U+2014 EM DASH U+2014 × × 一致
U+2015 HORIZONTAL BAR U+2015 × × 一致
U+2212 MINUS SIGN U+2212 × × 一致
U+30FC 長音符「ー」 U+30FC × × 一致
U+FF0D 全角ハイフン「-」 U+002D × 一致 一致
U+FF70 半角長音符「ー」 U+30FC × × 一致

元から半角の - を除く10種類のうち、NFKCで - になったのは全角の「-」(U+FF0D)だけでした。 日本語入力で起きやすい長音符「ー」(「090ー1234ー5678」)や、Word・PDFからのコピーで混ざりやすいU+2010・U+2212は、正規化しても残ります。マスクのパターンでは、ハイフンの位置をハイフン類の文字クラスにしておくのが確実です。

DASH = "[-‐-―−ー-ー]"

長音符「ー」を含めると、カタカナ語の中の数字(例:型番)を拾う誤検知が増えることがあります。前後を (?<!\d) (?!\d) で区切り、桁数も固定して、誤検知用の文章で試してから使います。

4. 検査用数字で「本物らしさ」を確かめる

12桁や16桁の数字は、マイナンバーやカード番号だけでなく、注文番号や品番にもあります。桁数だけで判定すると誤検知が増えるので、検査用数字(チェックディジット)が合うものだけをマスクするという絞り込みが使えます。

def luhn_ok(num: str) -> bool:
    """クレジットカード番号などのLuhn検査"""
    total = 0
    for i, c in enumerate(reversed(num)):
        d = int(c)
        if i % 2:
            d *= 2
            if d > 9:
                d -= 9
        total += d
    return total % 10 == 0

def mynumber_ok(num: str) -> bool:
    """個人番号(12桁)の検査用数字。11桁目までから12桁目を計算して比べる"""
    p = [int(c) for c in num[:11]][::-1]          # 右から1桁目をP1とする
    s = sum(p[i] * (i + 2 if i <= 5 else i - 4) for i in range(11))  # Q1..6=n+1, Q7..11=n-5
    r = s % 11
    check = 0 if r <= 1 else 11 - r
    return check == int(num[11])

print(luhn_ok("4111111111111111"), luhn_ok("4111111111111112"))   # True False
print(mynumber_ok("123456789018"), mynumber_ok("123456789012"))   # True False

4111111111111111 はカード会社が公開しているテスト用番号、123456789018 は検査用数字が合う架空の番号です。検査用数字は「書き間違いを見つける」ための仕組みなので、合うからといって実在の番号とは限りません。逆に、検査用数字で絞ると、書き間違えた本物の番号はマスクされずに残ります。 漏れを減らしたいのか、誤検知を減らしたいのかで、使うかどうかを決めます。

まとめ

落とし穴 起きること 対処
\b 「電話090-…」のように日本語と数字がくっつくと一致しない (?<!\d) (?!\d) で前後を区切る
\d と [0-9] \d は全角にも一致、[0-9] は一致しない NFKCで半角にそろえてから [0-9]
ハイフン類 NFKCで - になるのは半角以外の10種中「-」だけ ハイフン類の文字クラスを使う
桁数だけの判定 注文番号・品番を誤検知する 検査用数字で絞る(漏れとの引き換え)

限界

  • 確かめたのはPython 3.13の re だけです。JavaScriptの \b や、regex モジュールでは挙動が違います。
  • ここで示したのは個々の挙動です。実際の文章で何割を消せるかは、文章の書式の分布で変わります。
  • 人名・会社名・住所は正規表現ではほとんど消せません。この記事の対象外です。

日本語の架空データ300件で、正規表現マスクの漏れと誤検知を測った結果は、Zennの記事(無料)にまとめています: https://zenn.dev/kagaage/articles/9534e4e8407417

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?