“谁想要本船长的电话号码?”——这句话放在开发群里,一般没人真敢要,但放到测试环境里,几乎每个后端、App开发、运营系统都会遇到同一个需求:大批量、可重复、不惹麻烦的测试手机号。
很多第一次写测试脚本的人,直接random.randint(13000000000, 19999999999)一梭子下去,结果发现:数据是生成了,但有的号码一看就不合法,有的号段根本不存在,还有的重复率高得离谱。更麻烦的是,如果这些“假号码”被当成真实数据写进线上库,后面做手机号正则校验、运营触达、用户隐私合规,每一环都会出问题。
这篇文章不聊段子,聊清楚三件事:手机号生成背后有哪些规则,如何用 Python 写一个能直接用的批量生成工具,以及测试数据在生产环境里到底该怎么处理。读完你可以直接复制代码,在本地跑通一个虚拟号码生成器。
1. 这篇文章真正要解决的问题
先说痛点。开发过程中“需要手机号”的场景比想象中多:注册登录联调、短信验证码测试、用户画像造数、批量导入测试、活动系统压测、支付流程模拟,哪一个都绕不开。
传统的做法有三种,各有各的坑:
第一,从网上找现成的测试手机号清单。这种清单要么老,要么格式乱,要么就是已经被无数人用烂的固定号码。联调时偶尔用一次还行,压测和造数时分分钟露馅。
第二,手工拼一个看起来像手机号的数字。比如13800138000,这类号码在测试文档里很常见,但一个项目里反复用同一个号,接口层根本看不出问题,到数据处理层就暴露了:归属地查询为空、运营商识别失败、数据库唯一索引冲突。
第三,直接用真实手机号。这是最危险的做法。只要涉及真实个人信息,就要考虑授权、脱敏、存储责任。测试环境里用真实号码,一旦日志外泄或库被拖走,麻烦不是一句“只是测试数据”能带过的。
所以本文要解决的是:如何通过一段可控、可解释、可维护的 Python 代码,快速生成符合规则的虚拟手机号,并且能按前缀、按数量、按输出格式灵活调整。核心判断是:虚拟手机号生成不是 random 一把梭,它是一道数据构造规范题。
2. 手机号生成的基础规则与常见误解
在写代码之前,先弄清楚要生成的目标长什么样。
中国大陆手机号有两个硬规则和一个软规则:
2.1 硬规则:长度与首位
手机号必须是 11 位数字,这基本没有争议。第一位目前是 1,这也是通信行业多年形成的惯例。
很多脚本在定义长度时喜欢用范围随机,比如random.randint(10000000000, 19999999999),这种写法能凑出 11 位,但容易生成出10000000000这种一看就有问题的号码。
更合理的写法是先确定首位数,再生成剩余位数。
import random def generate_mobile_simple(): // 前 2 位固定为 1 + 随机第二位 first = "1" second = random.choice("3456789") rest = "".join(random.choice("0123456789") for _ in range(9)) return first + second + rest2.2 软规则:第二位和号段
当前开放的手机号第二位范围大致覆盖 3 到 9,但不同地区、不同年份会动态调整。这意味着代码里最好不要写死“只有 13、15、18 才算合法”的老规则。
一个比 second 是你该在工具里维护一份前缀库。比如把139、138、188、199等常见前缀放在配置文件或代码常量中,生成时从前缀库中随机选一个开头,再补 8 位随机数。
这种做法比纯随机更贴近真实数据,方便 QA 和联调方一眼看出号码归属地基本符合预期。
2.3 误解:手机号可以完全随机
很多新手以为 11 位数字随便组合就是手机号。实际上号段资源受运营商管理,不存在的号段即使格式正确,在真实系统中也可能被判定为无效。
所以在工具中,前缀库的价值不仅是“看起来像真的”,而是让生成的测试数据更接近生产环境的数据分布。你不需要覆盖所有真实号段,只需要覆盖当前项目用到的号段范围。
2.4 手机号校验兜底
除了生成,工具里最好带上校验函数。逻辑很简单:长度 11 位,首位 1,第二位 3-9,后 9 位数字。
import re MOBILE_PATTERN = re.compile(r"^1[3-9]\d{9}$") def is_valid_mobile(mobile: str) -> bool: if not isinstance(mobile, str): return False return bool(MOBILE_PATTERN.match(mobile))3. 环境准备与前置条件
本文示例基于 Python 3,不需要第三方框架。建议 Python 版本不低于 3.8,因为后续示例会用到secrets模块以及类型注解。
需要的工具:
- Python 3.8+ 解释器
- 命令行终端(Windows 可用 PowerShell,macOS/Linux 可用 bash)
- 一个空目录,用来放脚本文件
不需要安装任何第三方包,标准库足够完成核心功能。不过如果你希望命令行交互更顺滑,可以自行安装click,但这不是必须的。
建议在动手之前先确认 Python 环境。
python --version如果输出类似Python 3.10.12,即可继续。如果你机器上同时存在 python 和 python3,下文命令统一使用python3还是python,请以实际环境为准,这里不强行统一。
4. 核心流程拆解
我们要实现的小工具,整体流程可以分成四步:
4.1 定义前缀库
前缀库决定了生成的号码“像不像真的”。建议单独维护一个列表。实际项目中,前缀库可以放在配置文件里,由测试负责人维护,避免每次改代码。
PREFIXES = [ "130", "131", "132", "133", "134", "135", "136", "137", "138", "139", "150", "151", "152", "153", "155", "156", "157", "158", "159", "166", "170", "171", "175", "176", "177", "178", "180", "181", "182", "183", "184", "185", "186", "187", "188", "189", "198", "199" ]注意:前缀库不是越多越好,而是要根据你的测试目标决定。比如你主要测试某省用户,就应该配置该省放号较多的前缀。这里无法提供每个省份的准确号段,因为号段资源会动态变化,直接到运营商公开渠道查询或由团队内部维护更稳妥。
4.2 生成函数
生成函数负责做两件事:从前缀库选择一个前缀,再生成后续 8 位随机数字。
随机选择有两个模块可选:random和secrets。
如果是测试造数,对安全性没有要求,用random即可,性能更好。如果这个号码将来会用于生成验证码、密码、令牌等敏感场景,必须用secrets,因为random是伪随机,可预测,存在安全隐患。
4.3 批量去重
批量生成时最大的坑是重复。当生成量大到上万级别,纯随机方式的重复率会明显上升。工程上更稳妥的方式是用集合去重,数量不够就继续生成。
4.4 输出与校验
生成结果可以有两种出口:直接打印到命令行,或写入文件。文件格式常见有 CSV、JSON、纯文本一行一个。为了方便后续导入数据库,CSV 或 JSON 更合适。
生成结束后,必须用校验函数对结果做一遍全量校验,避免前缀库或随机逻辑引入脏数据。
5. 完整示例与代码实现
下面按照上面的设计,给出一个可以直接运行的完整脚本。为了方便阅读,我会拆成多个文件结构,但单文件方式也能用。
5.1 核心文件结构
fake_mobile/ ├── fake_mobile.py ├── prefixes.py └── output/prefixes.py只放前缀库:
# 文件路径:fake_mobile/prefixes.py # 注意:号段为常见号段示例,实际请按项目需要维护 PREFIXES = [ "130", "131", "132", "133", "134", "135", "136", "137", "138", "139", "150", "151", "152", "153", "155", "156", "157", "158", "159", "166", "170", "171", "175", "176", "177", "178", "180", "181", "182", "183", "184", "185", "186", "187", "188", "189", "198", "199" ]5.2 主脚本
fake_mobile.py提供四个能力:单个生成、校验、批量生成、命令行入口。
# 文件路径:fake_mobile/fake_mobile.py import csv import json import random import re import secrets from collections.abc import Iterable from prefixes import PREFIXES MOBILE_PATTERN = re.compile(r"^1[3-9]\d{9}$") DEFAULT_COUNT = 10 def random_number(length: int, secure: bool = False) -> str: """生成指定长度的数字字符串。 secure 为 True 时使用 secrets,适合对随机性有安全要求的场景。 """ if secure: source = secrets.choice("0123456789") return "".join(secrets.choice("0123456789") for _ in range(length)) return "".join(random.choice("0123456789") for _ in range(length)) def generate_one(prefix: str | None = None, secure: bool = False) -> str: """生成一个虚拟手机号。""" if prefix is None: prefix = random.choice(PREFIXES) if len(prefix) != 3 or not prefix.isdigit(): raise ValueError(f"非法前缀: {prefix}") tail = random_number(8, secure=secure) return prefix + tail def is_valid_mobile(mobile: str) -> bool: """校验手机号格式。""" if not isinstance(mobile, str): return False return bool(MOBILE_PATTERN.match(mobile)) def generate_batch( count: int, prefixes: Iterable[str] | None = None, secure: bool = False, unique: bool = True, ) -> list[str]: """批量生成虚拟手机号。""" count = max(0, count) prefix_list = list(prefixes if prefixes is not None else PREFIXES) if not prefix_list: raise ValueError("前缀列表不能为空") result = set() if unique else [] retry = 0 max_retry = count * 20 + 100 while len(result) < count: if retry > max_retry: break prefix = random.choice(prefix_list) mobile = generate_one(prefix, secure=secure) if unique: result.add(mobile) else: result.append(mobile) retry += 1 return list(result) def export_csv(mobiles: list[str], filepath: str) -> None: """导出为 CSV。""" with open(filepath, "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow(["mobile"]) for m in mobiles: writer.writerow([m]) def export_json(mobiles: list[str], filepath: str) -> None: """导出为 JSON。""" with open(filepath, "w", encoding="utf-8") as f: json.dump({"mobiles": mobiles}, f, ensure_ascii=False, indent=2) def main(): import argparse parser = argparse.ArgumentParser(description="生成虚拟手机号") parser.add_argument("-n", "--count", type=int, default=DEFAULT_COUNT, help="生成数量") parser.add_argument("--prefix", action="append", help="指定前缀,可多次传入") parser.add_argument("--secure", action="store_true", help="使用安全随机") parser.add_argument("--output", help="输出文件路径,支持 .csv / .json / .txt") parser.add_argument("--no-unique", action="store_true", help="不去重") args = parser.parse_args() prefixes = args.prefix or None mobiles = generate_batch( count=args.count, prefixes=prefixes, secure=args.secure, unique=not args.no_unique, ) invalid = [m for m in mobiles if not is_valid_mobile(m)] if invalid: print(f"发现非法号码 {len(invalid)} 个,请检查前缀库") for m in invalid[:10]: print(m) return if args.output: if args.output.endswith(".csv"): export_csv(mobiles, args.output) elif args.output.endswith(".json"): export_json(mobiles, args.output) else: with open(args.output, "w", encoding="utf-8") as f: f.write("\n".join(mobiles) + "\n") print(f"已生成 {len(mobiles)} 个号码 -> {args.output}") else: for m in mobiles: print(m) if __name__ == "__main__": main()这份代码有几个值得注意的地方:
generate_batch里去重逻辑使用集合,保证返回结果没有重复。retry上限是为了避免极端情况下无限循环。- 导出函数区分了 CSV、JSON、普通文本三种格式,方便不同测试工具读取。
- 校验逻辑放在导出前,能及时暴露前缀库或生成逻辑的问题。
5.3 命令行使用示例
生成 5 个号码:
python fake_mobile.py -n 5生成 200 个号码,固定使用 139、188 前缀,并导出为 CSV:
python fake_mobile.py -n 200 --prefix 139 --prefix 188 --output output/demo.csv生成 1000 个号码并导出为 JSON:
python fake_mobile.py -n 1000 --output output/demo.json5.4 作为模块导入
如果你不想用命令行,也可以在其他 Python 项目中直接导入。
# 文件路径:fake_mobile/use_example.py from fake_mobile import generate_batch, is_valid_mobile mobiles = generate_batch(20, prefixes=["139", "138"]) print(mobiles) print(all(is_valid_mobile(m) for m in mobiles))6. 运行结果与效果验证
运行生成命令后,正常情况下会输出指定数量的 11 位号码,比如:
python fake_mobile.py -n 3 --prefix 139预期输出:
13948592014 13967201548 13935882017这里注意一点:由于是随机生成,每次运行输出的具体号码都不同,这属于正常现象。判断工具是否成功,不依赖具体号码值,而看三点:
第一,号码长度是否全部为 11 位。
第二,号码是否全部通过正则校验。
第三,指定前缀时,号码开头是否与传入前缀一致。
如果导出到文件,可以直接用 wc 或 Python 打开检查。
python fake_mobile.py -n 1000 --prefix 139 --output output/demo.csv wc -l output/demo.csv因为 CSV 带了表头,所以wc -l结果应为 1001,而不是 1000。如果你不需要表头,可以把代码中的writer.writerow(["mobile"])注释掉。
如果运行失败,第一步先看命令行是否报语法错误,重点检查 Python 版本是否低于 3.8。str | None这种类型注解语法在 Python 3.10 之前不可用,如果你用的版本较低,需要改成Optional[str]并导入typing。
7. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 生成数量大于请求数量 | 去重逻辑导致异常 | 检查前缀列表是否太小 | 增加前缀库或允许不去重 |
| 生成的号码校验失败 | 前缀库包含非法前缀 | 打印前缀列表,检查长度和内容 | 只保留 3 位数字前缀 |
| 导出 CSV 打开乱码 | 编码问题 | 用文本编辑器查看文件编码 | 使用utf-8-sig编码打开,或调整代码 |
| 类型注解报错 | Python 版本过低 | 执行 python --version | 升级 Python 或改写注解 |
| 生成速度慢 | 生成量过大且前缀少 | 查看去重重试次数 | 扩大前缀库,关闭唯一约束 |
| 号码看起来全是假号 | 随机尾号太规整 | 观察输出数据的分布 | 使用更分散的前缀库 |
这里补一个容易忽视的坑:如果前缀列表里只有一两个前缀,又要生成几万条不重复号码,那么最终的随机空间被严重压缩,脚本会陷入长时间重试。更合理的做法是同时放宽唯一性要求,或大幅增加前缀范围。
8. 最佳实践与工程建议
8.1 测试数据不要直接污染生产库
生成虚拟手机号的目的是造测试数据,但造出来的数据一旦导入生产环境,就会变成脏数据。工程上建议在导入之前明确标记测试归属,比如在用户表的备注字段或独立测试标识中注明“虚拟数据”,方便后续清理。
8.2 不要在代码里硬编码前缀库
前缀库属于易变信息,建议放到配置中心、YAML 或数据库中,由测试负责人维护。代码里只保留一份兜底默认前缀,避免因业务变化反复改代码。
8.3 区分随机数安全级别
再次强调:random模块生成的数据不能用于密码、验证码、令牌等安全敏感场景。这类场景必须使用secrets。本文代码已经预留了secure参数,但默认仍走random,这是为了造数性能。将来如果需求变成“生成一批可用于风控测试的虚拟号码”,需要打开--secure。
8.4 考虑号码的敏感性和管理责任
虚拟手机号如果只是随机数字,理论上不构成真实个人信息,但要注意两个边界:
- 如果前缀库来自真实用户号段统计,并且生成结果刚好与某个真实号码撞车,那么在日志、监控等系统里,它看起来仍然像一个真实号码。虽然概率不高,但批量生成几百万条时,撞号风险会上升。
- 对于“号码是否真实存在、是否可注册、是否已注册”这类问题,不要在非授权情况下批量探测。批量注册探测、批量短信发送、营销骚扰,这些都是明确的高风险行为,本文代码仅用于开发和测试环境的数据构造,不能用于任何未经授权的线上行为。
8.5 日志与脱敏
在测试环境使用虚拟号码时,建议日志中统一加上前缀标识,比如TEST_13900000000或者在日志字段中增加data_type=test,避免运营和数据分析同事把测试数据当成真实用户。
8.6 与数据工厂结合
如果你所在团队已经使用了 Faker 这类造数工具,没必要重复造轮子,可以将其作为自定义 Provider 接入。Faker 本身也提供了手机号生成能力,但国内号段细节和自定义前缀扩展仍需要自己维护。本文示例代码的价值就在于此:逻辑简单,可定制性强,不依赖大量第三方库。
8.7 代码审查的小建议
这类工具虽然小,但也应该走代码审查。重点看三处:前缀库是否含有过期号段,随机逻辑是否用了安全模块,导出文件是否包含了多余的真实数据。哪怕它是“一次性脚本”,也建议放到仓库里管理,而不是存在某台机器的/tmp下。
9. 总结与后续学习方向
回到开头那句话:真正想要船长的电话号码的人不多,但需要一个“安全、合规、可批量生成虚拟手机号工具”的开发者很多。
本文从手机号格式规则入手,讲清楚了虚拟手机号生成的核心逻辑:前缀库决定真实性,随机策略决定性能与安全性,去重机制决定批量质量,校验函数决定最终可靠性。然后给了一个可以直接运行的 Python 命令行脚本,支持指定数量、指定前缀、CSV/JSON 导出,并在最后强调了测试数据管理的工程注意点。
如果你想继续深入,可以往这几个方向走:
第一,把生成器扩展成更通用的测试数据工厂,不仅生成手机号,还生成姓名、地址、邮箱、身份证脱敏数据,形成一套造数平台。
第二,学习如何使用 Faker 自定义 Provider,和本文的前缀库思路结合起来,让团队造数体验更好。
第三,研究测试数据生命周期管理,包括数据生成、数据标记、数据清理,以及如何在 CI 流水线里自动生成和销毁测试数据。
第四,如果你的工作涉及数据库大批量造数,可以结合copy、insert ... select等批量导入方式,把生成结果直接灌入测试库,但要注意充分利用事务和批量提交,避免导入性能问题。
最后提醒一句:虚拟手机号是测试工具的辅助手段,不是绕过验证、批量注册、短信轰炸的“万能钥匙”。把工具用在开发和测试的合规场景里,它能成为团队效率提升的好帮手;用在错误的地方,只会给项目和自己带来风险。这一点,比任何代码细节都重要。
建议先把这份脚本收藏起来,下次需要批量测试手机号时,直接改改前缀库就能跑。