简介:面向 PHP 开发者的省份 IP 随机生成工具包,解决按地理位置模拟 IP 的需求,可用于网络测试、用户行为模拟及数据分析等场景。压缩包内共 85 个文件,以 PHP 类文件为主(83 个),另含说明文档 txt 与 README.md,整体仅 110KB,轻量易集成。该 PHP 类封装了省份 IP 段数据与随机生成逻辑,支持通过内网转换函数在指定省份范围内产生随机 IPv4 地址,并提供 ip_segment 等辅助模块,便于开发者快速调用。已有 300 人学习下载,适合需要快速实现地域 IP 模拟的初中级 PHP 开发者,借助附带的说明文档可快速理解类结构与应用方法。 最近整理了一个小工具:一个随机生成对应省份IP的PHP类,压缩包名字就叫“随机生成对应省份IP的PHP类.7z”。说它是“类”而不是“脚本”,是因为它设计得很干净,可以实例化、可以多次调用、可以集成到现有PHP项目里,而不是一次性跑完就扔的临时文件。
这个类解决什么问题?一句话:给定省份,给我一个属于该省份IP段的合法IP。比如你传一个“广东”,它能返回一个类似14.208.0.123这样的地址。它适合谁?后端开发、测试工程师、数据采集场景下需要构造带有地区属性的测试数据的人。如果你是做前端地区识别联调、CDN分布测试、或者要给演示环境造一批“各省用户”的数据,这个小工具能省掉不少搜索和造数的功夫。
需要先说清楚一点:这个类生成的是“看起来像某省份”的IP,它不能改变你真实的网络出口地址,也不能真的把你的请求变成从那个省份发出。它适合的是那些不依赖真实网络链路、只依赖IP字符串本身的应用场景,比如数据库灌数据、前端联调、演示PPT。别指望用它来伪造真实请求来源,那是另一套完全不同的方案,也不在本文讨论范围。
1. 项目概述:一个“随机IP生成器”的诞生理由
1.1 谁需要随机省份IP
我在实际项目中遇到的需求大概有这几类。
第一类是前后端联调。很多业务系统会调用IP归属地接口,根据访问者IP所在省份展示不同的内容,比如首页Banner、运费模板、限购策略。前端开发在本地调试时,IP固定是本地的,想验证“如果是浙江用户看到什么效果”就很不方便。如果后端能临时返回一个指定省份的IP,或者直接给前端工具提供一个随机省份IP的接口,联调效率会高很多。
第二类是造测试数据。测试环境需要模拟全国用户的行为数据,比如订单表中要求用户的IP归属地与收货地址在同一省份。手工去百度搜“广东IP段”再拼,费时费力还容易出错。用PHP类直接生成,灌一万条数据也就几秒钟的事。
第三类是演示和Demo。很多SaaS厂商做产品演示时,需要在后台展示“来自不同省份的访客概览”。真实流量不够,那就只能造。用这个类随机生成几十个省份IP,配上一个简单的饼图,演示效果就很真实了。
1.2 为什么做成PHP类而不是脚本
既然只是随机生成IP,写一个简单的函数不就行了吗?为什么还要封装成类?
我的考虑是:这个工具的核心不在“随机”这两个字,而在于“数据维护”和“接口复用”。把数据加载、CIDR解析、省份选择、IP生成、保留地址过滤这些事情封装成一个类,调用方不需要关心内部逻辑,只需要new RandomProvinceIp($data),然后调用getRandomIp('广东')就能拿结果。
另外,类的属性可以缓存解析后的IP段数据,避免每次调用都重复解析CIDR字符串。如果写成纯函数,要么每次传入完整数据,要么用全局变量,前者啰嗦,后者不优雅。类的方案最合适,打包成.7z之后,解压就能用,也方便在其他项目中复制。
1.3 方案选型:自己做还是调接口
在做这个类之前,我对比过三种方案。
方案一是调用在线IP库接口,比如某些IP归属地查询服务。优点是不用管数据,缺点是:每次生成都要发起网络请求,延迟高;大多数接口有QPS限制,批量生成根本扛不住;一旦接口挂了或者限流,整个功能就瘫痪了。
方案二是本地维护一份省份IP段数据,在代码中随机选取。优点是快、稳定、可控,缺点是数据需要定期更新,因为运营商的IP段会调整。
方案三是用随机数直接生成一个IP,然后反查归属地,如果匹配到目标省份就返回,否则重新生成。缺点是命中率太低。比如目标省份是西藏,全范围的32位IPv4地址有大约43亿个,随便生成一个IP落在西藏的概率可能不到千分之一,循环多少次才能命中一次?这个方案的效率完全不可接受。
最终我选了方案二,也是这个PHP类采用的方式:本地IP段数据 + 范围随机。这里的数据质量是关键,代码本身只是工具,数据才是决定成败的核心。
2. 核心细节解析:IP不是“随机数字”那么简单
2.1 从整数角度看IPv4地址
如果只停留在“IP是四个数字加三个点”的表面,就很难理解这个类的设计。
IPv4地址本质上是一个32位的无符号整数,范围是0到4294967295。我们平时看到的点分十进制表示法,比如220.181.108.0,只是把这个整数拆成4段展示给人看的。计算过程是:
220 * 256^3 + 181 * 256^2 + 108 * 256^1 + 0 * 256^0 = 3701997568
所以,随机生成一个IP,本质上就是在一个整数区间内随机抽取一个整数。而“某省份的IP段”,就是若干连续的整数区间。只要我们把省份的IP段转换为起始整数和结束整数,剩下的工作就是在一个区间内随机取一个数,再用long2ip()转成点分十进制。这就是这个PHP类最底层的逻辑。
理解了这个原理,很多问题就能解释了。比如为什么生成的IP不能真实发出请求?因为真实网络路由看的是你的网卡配置和网络服务商分配的出口IP,不是你代码里编造出来的字符串。IP在应用层只是一个标识,真正通信还是要底层网络协议栈根据路由表来走。
2.2 数据格式:CIDR与整数范围的转换
IP段数据在网上最常见的格式是CIDR,比如220.181.108.0/24。斜杠后面的数字表示网络前缀长度。/24表示前24位是网络号,后8位是主机号,这个段里可以用的IP范围是220.181.108.0到220.181.108.255,一共256个地址。
这个类需要支持直接输入CIDR格式的数据。转换逻辑如下:
$parts = explode('/', $cidr); $baseIp = ip2long($parts[0]); $mask = (int)$parts[1]; // 网络号 $network = ($baseIp >> (32 - $mask)) << (32 - $mask); // 广播地址 $broadcast = $network | ((1 << (32 - $mask)) - 1);这段代码的意思是:先拿到IP的整数形式,然后把低(32 - mask)位清零得到起始地址,再把低(32 - mask)位置1得到结束地址。比如220.181.108.0/24转换后就是3701997568到3701997823。
注意:当
mask为0时,1 << 32在32位系统上会溢出,实际使用中要加个判断。PHP在64位系统上通常没问题,但在32位环境下建议用bcmath或者直接把/0当作全范围处理。
2.3 为什么不能全范围随机再反向匹配
很多人第一次写这种工具,会尝试:long2ip(random_int(0, 4294967295)),然后调用IP库接口查归属地,符合就返回,不符合就继续。这个思路不能说完全错,但效率极低。
我算过一笔账:中国内地的可用公网IP大概有3到4亿个,但全范围的IPv4地址有43亿个。随机抽一个IP落在国内的命中率不到10%,落在指定省份的命中率更低。如果要生成100个广东省的IP,可能要循环几千次甚至上万次,CPU和网络开销都很大。
正确的做法是反向思维:先选省份,再从该省份的IP段中随机选地址。这样每次生成必中,不需要任何判断和重试。类里面的实现也是这个思路:
- 传入省份名称,直接定位到该省份的IP段集合。
- 从该集合中随机选一个IP段。
- 从该段的起始整数和结束整数之间随机取一个数。
- 将这个数转换为点分十进制,输出。
这也解释了为什么类的设计需要把IP段数据预先解析并缓存起来。如果每次生成都去解析CIDR字符串,虽然也能跑,但性能会下降几个数量级。我的做法是在loadData()时一次性把数据解析成整数范围数组,之后生成IP只是数组下标访问和随机数计算,非常快。
2.4 权重问题:不是每个省都一样
细心的读者会发现问题:不同省份的IP段数量差异很大。有些省内网段多、地址空间大,有些省可能只有几个段。如果简单地从“省份列表”里随机挑一个省,再从这个省的IP段里随机挑段,最终生成出来的IP在省份分布上是不均匀的。比如北京、广东的IP段多,抽到的概率就相对高,这其实是符合直觉的——现实世界也是人口多、企业多的省份IP段更密集。
但如果你的需求是“每个省份生成数量差不多”,那就需要做权重处理。不上复杂策略的话,最简单的做法是:外层不按“省份”随机,而是把“省份 + IP段”展开为一个一维列表,每个IP段作为一条独立记录,先随机选记录,再在记录对应的区间内取IP。这样IP段多的省份,自然被选中的次数就多;IP段少的省份,被选中的次数就少。
我在类里保留了一个getRandomProvince()方法,内部用的是array_rand,也就是等概率选省份。如果你需要按IP段数量加权,可以在此基础上改扩展。
3. 代码实现:一个可以抄作业的PHP类
3.1 类的基本结构
整个类的核心方法不超过6个:
<?php class RandomProvinceIp { protected array $ranges = []; protected bool $loaded = false; public function __construct(array $data = []) { if (!empty($data)) { $this->loadData($data); } } public function loadData(array $provinceCidrs): void { foreach ($provinceCidrs as $province => $cidrList) { foreach ($cidrList as $cidr) { $range = $this->cidrToRange($cidr); if ($range) { $this->ranges[$province][] = $range; } } } $this->loaded = true; } protected function cidrToRange(string $cidr): ?array { $parts = explode('/', $cidr); if (count($parts) !== 2) { return null; } $ipLong = ip2long($parts[0]); if ($ipLong === false) { return null; } $mask = (int)$parts[1]; if ($mask < 0 || $mask > 32) { return null; } $network = ($mask === 32) ? $ipLong : (($ipLong >> (32 - $mask)) << (32 - $mask)); $hostMax = ($mask === 32) ? 0 : (1 << (32 - $mask)) - 1; $broadcast = $network | $hostMax; return ['start' => $network, 'end' => $broadcast]; } public function getRandomIp(?string $province = null): string { if (!$this->loaded) { throw new RuntimeException('IP 段数据尚未加载,请先调用 loadData()'); } if ($province === null) { $province = $this->getRandomProvince(); } if (empty($this->ranges[$province])) { throw new InvalidArgumentException("省份不存在或没有IP数据: {$province}"); } $range = $this->ranges[$province][array_rand($this->ranges[$province])]; $ipLong = random_int($range['start'], $range['end']); return long2ip($ipLong); } public function getRandomProvince(): string { $provinces = array_keys($this->ranges); if (empty($provinces)) { throw new RuntimeException('没有任何省份的IP数据'); } return $provinces[array_rand($provinces)]; } public function getBatchIps(int $count, ?string $province = null): array { $result = []; for ($i = 0; $i < $count; $i++) { $result[] = $this->getRandomIp($province); } return $result; } }3.2 数据准备示例
使用这个类之前,需要准备一份省份CIDR数据。这里给一段简化版,演示数据格式就够了,完整数据量太大,不适合贴在这里。
$ipData = [ '北京' => [ '220.181.108.0/24', '114.244.0.0/16', ], '广东' => [ '14.208.0.0/16', '113.64.0.0/16', '119.120.0.0/16', ], '上海' => [ '101.86.0.0/16', '202.96.0.0/16', ], // ... 其他省份 ];实际使用中,数据来源有几个渠道:一是从公开的IP归属数据库导出的汇总数据,二是从运营商公布的路由表整理,三是使用一些联网API导出的离线数据包。需要注意,不同渠道的数据格式可能不一样,但最终都要转换为CIDR字符串才能喂给这个类。
3.3 调用示例
验证一下效果:
$gen = new RandomProvinceIp($ipData); // 指定省份 echo $gen->getRandomIp('北京') . PHP_EOL; echo $gen->getRandomIp('北京') . PHP_EOL; // 随机省份 echo $gen->getRandomIp() . PHP_EOL; // 批量生成,给个广东多个IP $ips = $gen->getBatchIps(5, '广东'); print_r($ips);输出效果类似:
114.244.152.18 220.181.108.76 202.96.32.115 [ 14.208.10.3, 119.120.88.210, 113.64.12.77, 14.208.199.1, 113.64.201.6, ]同一个省份连续调用多次,每次IP都不同,前两段基本固定,后两段在变化,这就对了。
4. 实操过程与踩坑记录
4.1 数据整理阶段
数据是工作中真实踩坑的重灾区。完整的省份IP段汇总,我从各种渠道收集来的时候格式五花八门:有的是起始IP和结束IP格式,有的是CIDR格式,还有的是IP段加运营商名称的混合文本。用脚本统一转换为CIDR格式之后,还需要做一轮容错处理。
第一,去掉重复项。同样的IP段可能在多个渠道出现,去重可以减小数据量,提高类加载后的内存效率。我可以直接在外部用array_unique处理,也可以在loadData里用isset做标记,这里根据数据量自行选择。
第二,过滤掉私网地址段。10.0.0.0/8、172.16.0.0/12、192.168.0.0/16这些是私有地址,不会出现在公网省份归属中。但因为数据清洗不严,很容易混进来。如果被随机到了,生成出来的IP就是内网IP,在测试环境里会造成混乱。我处理时会用一个小函数先过滤一遍保留地址、私网地址、以及0.x.x.x、255.x.x.x这类特殊段。
第三,注意端到端地址。每个IP段里,网络号和广播地址实际是不可分配给主机使用的。比如220.181.108.0/24里,.0是网络号,.255是广播地址。不过因为省份IP段是统计意义上的“归属省份”,即使抽到这两个地址也不影响业务逻辑。如果你追求严谨,可以在cidrToRange()后把start加1、end减1。
4.2 性能实测
数据加载完成后,我做了一个简单的压测:
$gen = new RandomProvinceIp($ipData); $start = microtime(true); for ($i = 0; $i < 100000; $i++) { $gen->getRandomIp('广东'); } $spent = microtime(true) - $start; echo "生成10万个IP耗时: {$spent} 秒" . PHP_EOL;在普通笔记本PHP 8.2环境下,生成10万个指定省份IP大约耗时0.5到0.9秒,内存占用在数据加载后大约几MB,性能上完全够用。如果瓶颈卡在random_int上,可以考虑切换为mt_rand,速度会略微提升,但随机数质量不如random_int。我现在倾向于random_int,因为生成的IP数量不会大到影响性能,还是以安全可靠优先。
注意:如果要并发调用,比如多个进程同时使用这个类生成IP,数据独立加载即可,不会互相干扰。但随机数种子不需要特殊处理,PHP的
random_int和mt_rand都会在进程启动时自动播种。
4.3 真实踩过的坑
这里记录几个在实际开发中遇到的问题,供参考。
坑1:用了过期的IP段数据。运营商调整IP段是常态化操作,有些省的归属变化很大。如果生成出来的IP拿去查归属地,发现和预期省份对不上,十有八九是数据太旧了。解决办法是定期更新数据,最好能做成一个脚本定时从源头拉取。
坑2:随机数函数参数顺序写反。random_int(int $min, int $max),最小值和最大值容易搞反。虽然PHP有报错提示,但如果两个参数都填了但范围不合法,就会一直异常。我习惯在代码里先把$start和$end输出日志确认一下,再批量跑。
坑3:只统计了省份IP段,没统计自治域/运营商。某些省的IP段其实归属于外省注册的公司,这会导致“生成的IP查出来是另一个省份”。这个问题无法靠工具本身完全规避,只能尽力提高数据源质量。
坑4:忽略了IPv6。很多省份已经大规模部署了IPv6,域名解析返回的IP可能是IPv6地址。这个类的实现基于IPv4,IPv6的地址结构完全不同,实现方式也会变化。如果业务场景涉及IPv6,需要在类里单独增加一套IPv6的随机生成逻辑。
5. 常见问题与排查技巧实录
这里把使用过程中可能遇到的问题整理成一个速查表。
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 生成的IP查归属地和目标省份不符 | IP段数据过期或来源不准确 | 更新数据源,验证最新IP归属信息 |
| 批量生成时出现重复IP | 某个IP段地址空间过小,随机范围窄 | 过滤掉/31、/32这类过小的段,或合并重复段 |
| 生成报错“省份不存在” | 传入的省份名和数据中的键名不一致 | 打印array_keys,检查省份名是否包含省、市后缀 |
| 加载数据后内存占用很大 | CIDR段数量过多且没有合并 | 合并连续IP段,比如1.1.1.0/24和1.1.2.0/24在条件允许时聚合 |
| 每天定时生成大量IP,需要避免完全随机 | 要求可复现的随机序列 | 提供可选的随机种子参数,或改用指定IP段索引的方式 |
| 想按市级粒度生成 | 数据只到省级 | 扩展数据结构,把市加入第二层键,改造getRandomIp |
生成的IP包含0.x.x.x或255.x.x.x | 数据清洗不严,有特殊地址段 | 在cidrToRange后统一过滤保留地址 |
排查时最常用也最有效的方法是:把待处理的CIDR数据先单独输出,用线上的IP归属查询工具抽查几条,确认数据可靠后再接入类。如果数据本身是脏的,代码再怎么优化也白搭。
另外一个独家技巧:生成完IP后,可以顺便存一份原始整数形式的记录。这样如果后续要按IP排序、去重或者区间判断,就不需要再把点分十进制转回整数了,效率更高。
关于“类”的扩展,如果你希望在项目中直接使用,可以考虑做成单例或者用服务容器注册。但我的建议是保持类本身简单纯粹,数据由调用方传入,不要在里面写死任何省份数据。这样无论是用配置文件、数据库还是外部接口提供数据源,都能做到灵活切换。
6. 安全与合规提醒,按需使用
写代码的时候,有一些红线需要再次强调。这个工具只能用在正当的开发测试场景,比如造数据、联调、演示。如果你试图用它来伪造访问者身份、绕过业务规则或者恶意刷量,这属于违法违规行为,一旦被发现,后果自负。我在项目的README里也明确写了这一点。
还有一点是IP段数据的版权问题。部分商业IP数据库是有版权的,直接打包进自己的项目再公开分发,存在法律风险。建议使用开放数据源或者购买授权,不要捡了芝麻丢了西瓜。这虽然不是技术问题,但值得开发者留意。
我在实际使用这个类时,整体体验是:代码本身代码量不大,最难的反而是数据准备和数据校验那一步。如果你正在为一个项目生成多地域测试数据,可以考虑把“省份IP段”的更新做成一个定时任务,这样省心很多。
最后再分享一个小技巧:如果你的项目对IP格式有严格要求,比如希望输出固定长度的字符串方便对齐,可以直接echo $gen->getRandomIp() . PHP_EOL;,如果还要校验是否合法,可以用filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)再确认一次。生成器本身用的是ip2long和long2ip,正常不会出现不合法IP,但加一层校验总没坏处。
本文还有配套的精品资源,点击获取