Fakerで作った見本CSV、company_emailの100行はどれも例示用ドメインではなかった

Fakerで作った見本CSV、company_emailの100行はどれも例示用ドメインではなかった

C001,中村 直樹,[email protected]。商品に同梱するサンプルCSVの1行目です。名前もアドレスも作り物なのに、abe.comは誰かが持っているドメインかもしれません。購入者が試しにメールを送ったら、届くのは見知らぬ他人です。

実際の販売や購入者対応の記録ではありません。Faker(ダミーの名前や住所を作るPythonのライブラリ)で100行のCSVを作り、メール欄のドメインを調べた検証メモです。

3つのメソッドで同じ100行を作る

環境は、uv 0.12.7、Python 3.14.7、Faker 40.44.0です。作業用のディレクトリで仮想環境を作ります。

mkdir sample-check && cd sample-check
uv venv -q .venv
uv pip install -q --python .venv/bin/python faker

インストール後にfaker.VERSIONで確かめたところ、Fakerは40.44.0、Pythonは3.14.7でした。

CSVを作るスクリプトはmake_sample.pyとして保存します。名前はfake.name()で固定し、メールだけをメソッドごとに差し替えます。seedも固定です。

make_sample.py の全文
import csv
import sys
from faker import Faker

mode = sys.argv[1]  # company / free / default
fake = Faker("ja_JP")
Faker.seed(20261104)

makers = {
    "company": fake.company_email,
    "free": fake.free_email,
    "default": fake.email,
}

with open(f"customers_{mode}.csv", "w", newline="", encoding="utf-8") as f:
    w = csv.writer(f)
    w.writerow(["customer_id", "name", "email"])
    for i in range(1, 101):
        w.writerow([f"C{i:03d}", fake.name(), makers[mode]()])
print(f"customers_{mode}.csv: 100 rows")
for m in company free default; do .venv/bin/python make_sample.py $m; done

3つのCSVが100行ずつできます。

companyの先頭3行は次のとおりです。

customer_id,name,email
C001,中村 直樹,[email protected]
C002,小林 明美,[email protected]
C003,村上 洋介,[email protected]

defaultの1行目は、名前もローカル部分も同じまま[email protected]です。変わるのはドメインだけでした。

予約済みドメインかどうかを機械で見る

目で見て「それっぽい」と判断しても当てになりません。基準はRFC 2606です。example.com、example.net、example.orgと、.test、.example、.invalid、.localhostは、誰かへ割り当てない決まりです。この一覧の外のドメインは、見本から外す規則にしました。

check_domains.py の全文
import csv
import socket
import sys
from collections import Counter

RESERVED_EXACT = ("example.com", "example.net", "example.org")
RESERVED_SUFFIX = (".test", ".example", ".invalid", ".localhost")


def is_reserved(domain):
    if domain in RESERVED_EXACT:
        return True
    if domain.endswith(tuple("." + d for d in RESERVED_EXACT)):
        return True
    return domain.endswith(RESERVED_SUFFIX)


def resolves(domain):
    try:
        socket.getaddrinfo(domain, None)
        return True
    except socket.gaierror:
        return False


path = sys.argv[1]
with open(path, encoding="utf-8", newline="") as f:
    rows = list(csv.DictReader(f))

domains = Counter(r["email"].rsplit("@", 1)[1] for r in rows)
bad = {d: n for d, n in domains.items() if not is_reserved(d)}
bad_rows = sum(bad.values())
print(f"{path}: rows={len(rows)} distinct_domains={len(domains)} non_reserved_rows={bad_rows}")
if bad:
    live = [d for d in bad if resolves(d)]
    print(f"  non_reserved_domains={len(bad)} name_resolves={len(live)}")
    for d, n in sorted(bad.items(), key=lambda x: (-x[1], x[0]))[:5]:
        print(f"  {d} x{n} resolves={resolves(d)}")
sys.exit(1 if bad else 0)

予約外のドメインが1つでもあれば終了コード1で止めます。resolvesはDNSで名前が引けるかを見るだけの補助で、合否には使っていません。

for m in company free default; do .venv/bin/python check_domains.py customers_$m.csv; echo "exit=$?"; done
customers_company.csv: rows=100 distinct_domains=70 non_reserved_rows=100
  non_reserved_domains=70 name_resolves=49
  yamamoto.com x4 resolves=True
  abe.com x3 resolves=True
  kobayashi.com x3 resolves=False
  suzuki.com x3 resolves=True
  tanaka.jp x3 resolves=False
exit=1
customers_free.csv: rows=100 distinct_domains=3 non_reserved_rows=100
  non_reserved_domains=3 name_resolves=3
  hotmail.com x37 resolves=True
  yahoo.com x34 resolves=True
  gmail.com x29 resolves=True
exit=1
customers_default.csv: rows=100 distinct_domains=3 non_reserved_rows=0
exit=0

company_emailは100行すべてが予約外でした。70種のドメインのうち、49種は名前が引けます。free_emailの3種はGmailなどの無料メールなので、当然です。通ったのはemail()だけ。

名前から安全は読み取れない

メソッド名だけ見ると、3つは同じ仲間です。ところがemailの引数にはsafe: bool = Trueがあり、デフォルトでexample.orgなどに寄せます。company_emailとfree_emailにはその引数がありません。

.venv/bin/python -c "
import inspect
from faker.providers.internet import Provider
print('email', inspect.signature(Provider.email))
print('company_email', inspect.signature(Provider.company_email))
print('free_email', inspect.signature(Provider.free_email))
"
email (self, safe: bool = True, domain: str | None = None) -> str
company_email (self) -> str
free_email (self) -> str

「ダミー用のライブラリだから、どれを呼んでも安全」とは言えませんでした。会社らしさを出したくてcompany_emailを選ぶほど、実在のドメインに近づきます。使うならemail()のデフォルトのままにするか、domain="example.com"のように明示します。作る側のスクリプトとは別に、検査を挟めば、予約外のドメインを公開前に見つけられます。

名前が引けない21種は、安全とは限らない

company_emailの70種のうち、名前が引けなかった21種はkobayashi.com、tanaka.jpなどです。これは「誰も持っていない」と同じ意味ではありません。登録済みでもAレコードを置いていない、このPCのDNSが一時的に答えなかった、という場合でもresolves=Falseになります。

数字がずれた件も書いておきます。名前を作らずcompany_email()を100回呼ぶだけの使い捨てスクリプトで同じseedを使うと、66ドメインのうち51種で名前が引けました。上の結果は70と49です。乱数の消費順が変わるので、同じseedでも、呼び出しの組み合わせが違えば行が変わります。

だから、検査は同梱するCSVそのものに回します。スクリプトを回し直して確かめた気になるのは危険です。seedを固定しても、結果は呼び出し方で変わります。

今回見ていないこと

  • 名前列: 同姓同名の人は実在しうるので、fake.name()の出力は検査していません
  • メールのローカル部分: itoakiraのような部分も実在の誰かと重なりえます。ドメインが予約済みなら、届く先がないので許容しました
  • 電話番号と住所: Fakerは作れますが、今回は一切確認していません
  • AIの生成物: 今回はFakerだけです。AIに「ダミーの顧客リストを作って」と頼んだ出力は試していません

AIに作らせたCSVにも同じ検査を当てられます。生成元が変わればドメインの出方も変わるのか。そこはまだ分かりません。

出典

この記事をシェア