简介这是一套可部署的IP地址精准定位系统源码v1.0面向网站开发者、运维人员及位置服务学习者用来解决传统IP查询只能定位到市级、精度不足且不够直观的问题。其亮点在于查询结果可缩小到百米误差范围并直接在地图界面中标注位置尤其适合访问统计、用户地域分析、线上风控等需要地域信息的网络应用场景。资源压缩包共包含152个文件、大小1.51MB主要类型为74个png、30个jpg图片素材、21个js脚本和16个css样式表另有少量php后台文件和gif、文本说明其中js脚本承担地图展示与交互逻辑css负责整体页面与地图容器样式图片素材用于地图标记和UI装饰。项目基于Bootstrap、jQuery等常见前端框架结构简洁清晰便于直接部署测试目前已有1014人浏览学习。读者可通读源码了解IP定位与地图展示的典型实现路径掌握从IP请求到地图标点展示的完整流程并在此基础上按需扩展业务字段、调整界面或接入其他地图服务。1. IP地址精准定位系统源码 v1.0它在定位什么精度到哪一级收到一个陌生 IP先要知道它从哪个城市来再决定要不要拦、要不要查、要不要报警——这就是 IP地址精准定位系统源码 这类工程要解决的核心问题。很多人第一次接触它会误以为“精准”指的是 GPS 那种精确到几百米的位置实际上不是。基于 IP 的定位精度天花板通常在城市级到区县级能做到“从哪个运营商、哪个城市、哪个区出来”做不到“在哪栋楼”。这套源码的价值是把 IP 地址和地理经纬度、运营商、时区这些信息以可查询的方式组织起来给风控、运维、日志溯源提供一个高效的判定依据。适合谁用后端工程师、安全分析人员、做用户画像的产品技术团队还有那些需要在内网离线环境里完成 IP 归属判定的场景。2. 为什么离线库是主答案表结构设计与“IP段二分”的选型逻辑2.1 在线API vs 离线库四个维度的取舍做 IP 定位系统第一个交叉路口就是选在线 API 还是离线数据库。不要把这事想成“哪个好”而是要看你的查询环境、QPS 和故障容忍度。维度离线库在线API精度区县级为主依赖库的更新频率多数也是区县级部分支持高精度城市时延内存/数据库查询15ms一次 HTTP 往返通常 10100ms成本一次导入后续仅存储开销按调用量计费高并发成本显著可用性完全内网可用无外部依赖依赖公网与第三方可用性会有限流我一般会建议把离线库作为主链路在线 API 只做兜底。原因很直接——定位系统往往是风控链路里最前置的一环如果每一次判定都要先请求外部接口那第三方抖动一次你的整个查询链路就跟着抖而离线库把数据拉到本地查询变成一个纯本地过程稳定性和速度都可控。在线 API 的价值在于离线库没有覆盖到的地址段或者离线库更新不及时时的补位而不是主路。这里要澄清“精准”两个字。离线库里的数据本质上是“IP 段与地理区域”的映射来源是运营商的地址分配注册信息、路由公告、以及其他可公开的地理归因数据。它的精度上限取决于库作者对地址段的切分粒度。城市级是及格线区县级是良好局部能做到街道级但那是靠额外的商业数据源堆出来的。源码本身解决的是“如何高效查库、如何保证数据可更新、如何承载高并发”数据精度的上限由你选用的库决定。2.2 ipv4 与 ipv6 的两张表字段设计与关键索引离线库不管原始文件是 CSV、txt 还是二进制落到数据库里都要归一成区间模型。IP 地址的分配是按“段”来的一个 C 段 192.168.1.0/24 就是 256 个地址一个 B 段 172.16.0.0/16 就是 6 万多个地址。如果按单个 IP 存一条记录全世界 IPv4 地址 43 亿个没有任何表能扛住按段存全球的 IPv4 段记录数通常在几十万量级完全可以在内存里做二分。ipv4 段表的核心字段设计CREATE TABLE ipv4_segment ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, start_ip VARCHAR(15) NOT NULL COMMENT 起始IP点分十进制, end_ip VARCHAR(15) NOT NULL COMMENT 结束IP点分十进制, start_num INT UNSIGNED NOT NULL COMMENT 起始IP的32位整数, end_num INT UNSIGNED NOT NULL COMMENT 结束IP的32位整数, region_id INT UNSIGNED NOT NULL COMMENT 区域表外键, isp VARCHAR(32) DEFAULT COMMENT 运营商如电信/联通/移动, updated_at DATE NOT NULL COMMENT 该段数据的入库日期, UNIQUE KEY uk_range (start_num, end_num), KEY idx_start (start_num) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;start_num 和 end_num 是关键。把192.168.1.1这类点分十进制转成整数是为了让区间查找走数值比较而不是字符串比较速度快一个量级。INT UNSIGNED最大到 4294967295刚好装下 IPv4 全地址空间。如果某张旧表用了BIGINT来存也不是不行但在 InnoDB 里索引宽度翻倍只是多一些空间成本不影响正确性。我习惯统一用INT UNSIGNED因为定长且语义准确。region 表单独拆出来是为了避免每个 IP 段都重复存“省市区名称”这样冗长的字符串CREATE TABLE region ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, country VARCHAR(32) NOT NULL DEFAULT 中国, province VARCHAR(32) NOT NULL DEFAULT , city VARCHAR(32) NOT NULL DEFAULT , district VARCHAR(32) DEFAULT , lat DECIMAL(10, 6) DEFAULT 0 COMMENT 纬度, lng DECIMAL(10, 6) DEFAULT 0 COMMENT 经度, UNIQUE KEY uk_region (country, province, city, district) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;区域信息冗余在一个表里查询时用 JOIN 一次拿全。经纬度字段能扩展出地图打点、距离计算等能力但注意它代表的是该区域的城市中心坐标不是该 IP 的实际设备位置。还有一张 ipv6_segment 表结构几乎一样只是地址用VARBINARY(16)存储、对应的整数位宽改成DECIMAL(39,0)或拆两个BIGINT这部分在避坑章节会展开说明。2.3 用“IP段区间”而不是“逐IP”存数据定位查询的本质定位查询的本质是一个有序区间查找给定一个目标 IP 的整数形式 target_num在所有[start_num, end_num]区间里找到唯一一个满足start_num target_num end_num的记录。暴力做法是逐条扫描段记录几十万条时单次查询几毫秒到几十毫秒低并发还能勉强接受QPS 一起来就崩。所以成熟的实现都走二分。先把全表按 start_num 排序然后在数组上二分查最后一个 start_num 小于等于目标值的索引再检查目标值是否落在该段的 end_num 范围内。排序后的段序列: [0] start16777216 end16777471 ... [1] start16777472 end16778239 ... [2] start16778240 end16779263 ... 查询 16778200: 二分定位到最后一个 start 16778200 的段 [1] 检查 end16778239 16778200 成立 命中段 [1]归属该区域这段逻辑就是整个离线定位系统的核心。它解释了为什么建表时要保证段不重叠、数据是闭区间而非开区间。很多源码在实际运行时“查不到”或“查错”的根源就是导入时不注意段的连续性有的库给的是[start, end]全闭区间有的给的是[start, end)半开区间混着处理就会漏地址。在导入脚本里统一把 end 按闭区间落库查询时用 start_ip AND end_ip永久解决边界问题。到这里原理层面的选型已经立住了离线库为主、区间模型存储、二分查找定位。接下来就是把这个模型落成能跑的源码。3. 把源码跑起来从建库、导入到查询接口的最小实现3.1 建表ipv4_segment 与 region 表先执行上一章的两段建表 SQL。这里有一个额外建议生产环境把 ipv4_segment 和 ipv6_segment 分表而不是塞进同一张表加 type 字段区分。因为 IPv4 查询和 IPv6 查询走的是完全不同的二分逻辑混在一张表里会让索引和条件都变复杂查询分支多一道判断性能白白损失。建表之后马上要对start_num建一个覆盖索引。如果常用查询只取start_num, end_num, region_id, isp这几个字段可以考虑把索引建为ALTER TABLE ipv4_segment ADD INDEX idx_range_cover (start_num, end_num, region_id, isp);这个索引让查询过程中不需要回表取数据InnoDB 在二级索引里就能完成区间过滤。它的代价是每次数据更新时索引维护成本略高但对一个以查询为主的定位系统来说这个交换非常划算。3.2 导入离线数据CSV 入库脚本拿到离线库原始文件后常见格式是一行一个地址段示例start_ip,end_ip,province,city,district,isp 1.0.0.1,1.0.0.255,中国,福建省,福州市,电信 1.0.1.1,1.0.3.255,中国,福建省,福州市,电信下面这段 Python 脚本用来把 CSV 灌进 MySQL。它比直接在 SQL 客户端里用LOAD DATA更可控因为你可以顺手完成 IP 转整数、闭区间修正、region 去重这几件脏活。import csv import ipaddress import mysql.connector conn mysql.connector.connect( host127.0.0.1, userroot, passwordyour_password, databaseip_locator ) cursor conn.cursor() # region 表用字典去重避免同一个区域插入多条记录 region_cache {} def ip_to_int(ip_str): return int(ipaddress.IPv4Address(ip_str)) with open(ip_segment.csv, encodingutf-8) as f: reader csv.reader(f) next(reader) # 跳过表头 batch [] for row in reader: start_ip, end_ip, province, city, district, isp row region_key (province, city, district) if region_key not in region_cache: cursor.execute( INSERT INTO region (country, province, city, district) VALUES (%s, %s, %s, %s), (中国, province, city, district) ) region_cache[region_key] cursor.lastrowid region_id region_cache[region_key] start_num ip_to_int(start_ip) end_num ip_to_int(end_ip) batch.append((start_ip, end_ip, start_num, end_num, region_id, isp)) # 每 5000 行提交一次减少事务日志压力 if len(batch) 5000: cursor.executemany( INSERT INTO ipv4_segment (start_ip, end_ip, start_num, end_num, region_id, isp) VALUES (%s, %s, %s, %s, %s, %s), batch ) conn.commit() batch.clear() if batch: cursor.executemany( INSERT INTO ipv4_segment (start_ip, end_ip, start_num, end_num, region_id, isp) VALUES (%s, %s, %s, %s, %s, %s), batch ) conn.commit() cursor.close() conn.close()这段脚本里值得注意的参数是两个batch批大小和region_cache字典。批大小设 5000是因为单条提交在几十万行数据下会慢到无法接受而单次事务插入太多又会拖长锁时间5000 是一个在机械硬盘和 SSD 上都表现稳定的折中值。region_cache是必须的如果不用它你会对同一地区反复执行 INSERTregion 表膨胀到十几万行JOIN 时会拖慢查询。导入完成后检查一下是否有段重叠这是很多线上翻车事故的源头SELECT COUNT(*) FROM ipv4_segment a, ipv4_segment b WHERE a.id b.id AND a.start_num BETWEEN b.start_num AND b.end_num;正常结果应该是 0。如果大于 0说明原始数据里有重叠段这种段会导致查询命中两条记录结果不稳定。处理办法是把重叠段按更细粒度拆分或者直接信任后导入的记录。最省事的方案是导入前按 start_num 排序相邻两条的前一条 end_num 不小于后一条 start_num 时把前一条的 end_num 改成后一条 start_num 减一。这种问题一旦发生用 SQL 修数据比重新导入还折腾所以我在脚本里直接做了防呆处理但代码里没体现建议你按自己的数据源情况补上这一段校验。3.3 查询接口SQL 查找 内存缓存查询接口承担的是高频读任务。最常见且简单可靠的做法是先用一层内存缓存承接热 IP缓存未命中再走 MySQL。下面的 PHP 代码实现了一个同时支持“纯 SQL 查库”和“Redis 缓存”的查询接口适用大多数 LNMP 部署环境。?php require_once db.php; function ipToInt($ip) { return sprintf(%u, ip2long($ip)); } function queryIpv4($ip) { $ipNum ipToInt($ip); $cacheKey ipv4: . $ipNum; // 第一层Redis 缓存 $redis new Redis(); $redis-connect(127.0.0.1, 6379); $cached $redis-get($cacheKey); if ($cached ! false) { return json_decode($cached, true); } // 第二层数据库区间查找 $pdo new PDO(mysql:host127.0.0.1;dbnameip_locator, root, your_password); $stmt $pdo-prepare( SELECT se.isp, r.country, r.province, r.city, r.district, r.lat, r.lng FROM ipv4_segment se JOIN region r ON se.region_id r.id WHERE se.start_num :ipnum AND se.end_num :ipnum ORDER BY se.start_num DESC LIMIT 1 ); $stmt-execute([ipnum $ipNum]); $row $stmt-fetch(PDO::FETCH_ASSOC); if ($row) { // 缓存 12 小时TTL 按业务调整 $redis-setex($cacheKey, 43200, json_encode($row)); } return $row; } $ip $_GET[ip] ?? 127.0.0.1; if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { http_response_code(400); exit(invalid ipv4 address); } header(Content-Type: application/json; charsetutf-8); echo json_encode([ip $ip, result queryIpv4($ip)]);这里的 SQL 用了ORDER BY se.start_num DESC LIMIT 1逻辑上等价于二分查找但这个排序由 MySQL 的次级索引完成。当 start_num 走 idx_range_cover 索引定位到起始位置后再按 end_num 过滤整体耗时在毫秒级。Redis 缓存键用整数形式的 IP 而不是点分十进制是因为ip2long后的整数能保证同一个 IP 只对应一个缓存键不会出现文本格式差异导致的重复缓存。这段代码有个隐藏的性能点如果查询的是一个完全没有命中任何段的 IP比如某些保留地址LIMIT 1依然会扫描到start_num ipnum的最后一条记录并检查 end_num这本身是索引上的范围扫描代价可接受。但如果你的业务场景里“查不到”的请求特别多建议在缓存层额外记录一个空结果标记避免每次都打库。3.4 在线API兜底接口超时与降级离线库覆盖面再广也可能缺最新分配的地址段。兜底方案是查不到时调用在线 IP 定位接口。这里的关键不是接口本身的准确度而是你不能让兜底请求阻塞主流程太久。function queryOnlineFallback($ip) { $url https://ip-api.example.com/json/ . $ip . ?langzh-CN; $ch curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER true, CURLOPT_CONNECTTIMEOUT 300, // 连接超时 300ms CURLOPT_TIMEOUT 800, // 总超时 800ms CURLOPT_NOSIGNAL true, ]); $res curl_exec($ch); $errno curl_errno($ch); curl_close($ch); if ($errno ! 0) { return null; } $data json_decode($res, true); if (!isset($data[status]) || $data[status] ! success) { return null; } return [ isp $data[isp] ?? , province $data[regionName] ?? , city $data[city] ?? , ]; }CURLOPT_CONNECTTIMEOUT和CURLOPT_TIMEOUT这两个参数是兜底的生命线。连接超时设 300ms是给握手留出的合理时间总超时设 800ms是为了防止接口响应慢时把上游请求全部拖死。CURLOPT_NOSIGNAL在 PHP 里用于避免 DNS 解析时产生信号中断多线程环境下尤其要加。失败时返回 null调用方继续走“未知地区”的默认策略而不是报错这一点是降级的核心原则。4. 参数怎么调缓存、并发与命中率的三个关键旋钮4.1 缓存 TTL 与失效策略定位数据的更新频率天然是“天级”的单一 IP 的归属在一天内几乎不会变化。所以缓存 TTL 可以放心给长一点默认 12 到 24 小时。我在上一节代码里用的 43200 秒12 小时是兼顾新鲜度和命中率的结果。设太短比如 5 分钟每次更新高频 IP 都会穿透到数据库设太长比如 7 天运营商重新分配地址段后缓存里会一直保留旧归属风控判定就会误伤。缓存失效不只是 TTL 的事还要处理“数据源更新后缓存不刷新”的坑。做法是给缓存键加一个版本号后缀比如ipv4:v1:16778200当全量更新离线库时把缓存版本从 v1 改成 v2旧缓存自然失效。这个版本号可以放在 Redis 的一个全局 key 里ipv4_cache_version 1 缓存键 ipv4:{version}:{ipNum}更新数据时执行INCR ipv4_cache_version所有旧缓存瞬间变成永不命中的空键等 Redis 的惰性淘汰慢慢清掉。这比遍历删除几千上万个 key 快得多也是我生产环境里用过最省心的后悔药方案。如果你不想动 Redis可以在业务层面给查询结果里的updated_at加判断数据库中更新的行才会覆盖缓存但实现上多一道 JOIN我一般不推荐。4.2 内存二分 vs 数据库索引并发上来后的选择数据库索引方案在 qps 几百时没有问题但到了几千甚至上万MySQL 的连接数和 InnoDB 的 buffer pool 会先后成为瓶颈。这时候该把整张 ipv4_segment 表加载进内存在应用层直接二分。内存二分的实现要点是把表里的start_num, end_num, region_id, isp按start_num升序放成一个数组然后用库里的二分函数查找。PHP 里没有内置的二分查找函数常见做法是自己写一个低边界查找function binarySearchSegments($segments, $ipNum) { $lo 0; $hi count($segments) - 1; $ans -1; while ($lo $hi) { $mid intdiv($lo $hi, 2); if ($segments[$mid][start_num] $ipNum) { $ans $mid; $lo $mid 1; } else { $hi $mid - 1; } } if ($ans 0 $segments[$ans][end_num] $ipNum) { return $segments[$ans]; } return null; }这段代码查找的是“最后一个 start_num 小于等于目标值的段”。如果命中再检查 end_num。这个写法的边界条件很微妙$ans只有在 start_num 不大于目标时才更新所以最终得到的一定是最大的那个可能起点初始值设为 -1是为了处理目标值比第一段的 start_num 还小时循环结束后$ans保持 -1直接判空。内存加载时机建议在 worker 进程启动时拉一次然后靠定时任务每 10 分钟重新加载避免每次请求都读文件。从数据库索引切到内存二分的判断线我一般看两个指标单机 QPS 超过 2000或者平均查询延迟超过 20ms。如果 QPS 已经这个量级但并发主要集中在一批固定 IP 上还是应该先优化缓存命中率如果缓存命中率已经过 90%数据库仍扛不住切内存二分就是正解。内存二分的消耗是一次全表加载几十万行大约占几十 MB 内存对现代服务器来说几乎可以忽略换来的收益是单次查询 1ms 以内的稳定时延且不依赖数据库连接池。4.3 在线兜底的超时与阈值参数兜底接口最容易出问题的不是“偶尔失败”而是“雪崩”。当离线库大面积缺失某段地址时会有大流量同时穿透到在线 API第三方限流就会把错误码甩回来。所以兜底链路必须有一组熔断参数参数建议值含义connect_timeout300ms建立连接的超时total_timeout800ms包括请求与响应的总超时failure_threshold5次连续失败多少次触发熔断open_duration30s熔断后多久尝试恢复max_concurrency200同一时刻最多发出多少个在线请求这组参数的工程意义是在线兜底只承担“救火”角色不能让它成为主链路的放大器。连续失败 5 次就打开熔断开关之后 30 秒内的查询直接返回未知不再击穿到在线接口30 秒后放一个测试请求成功就关闭熔断失败就再续 30 秒。这类逻辑如果你不想自己实现可以直接借成熟的限流熔断库但参数一定得按你的第三方服务实际表现来调。没有哪个厂商的 SLO 能保证 100% 可用你自己的降级策略才是可用性的最后一道防线。5. 避坑指南五个 IP 定位翻车现场5.1 定位偏到隔壁城市离线库过期还是 IP 被重新分配现象某个固定 IP 之前定位一直正常某天突然从“北京市海淀区”变成了“北京市朝阳区”再查又变了位置在几个区之间横跳。原因运营商会对地址段做动态规划IP 段可能在多个区域间迁移另一个常见原因是离线库太久没更新。如果你用的库还停留在半年前很多段的归属早就变了。当然也可能是库的某个段本来就把一个跨区的大段划给了单一区域这是粒度问题。解决先确认离线库的发布时间戳。如果库已经超过一个季度没更新优先换新库。如果新库仍然漂移用该 IP 的 whois 信息和在线接口交叉比对确认哪个来源更可信。风控场景里对这类“地理漂移”的 IP 应该单独标记而不是强硬地只信一个库。5.2 内网/保留地址被当成公网地址现象查询192.168.1.1、10.0.0.5、127.0.0.1这类 IP居然返回了某个省份的城市。原因导入数据时没有过滤保留地址段。某些库会把私有段、链路本地段、组播地址段也写进段表或者你的查询接口没做白名单拦截。解决在查询接口入口处先判断 IP 是否属于保留地址。IPv4 最少要拦这几段10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.0/8、169.254.0.0/16、0.0.0.0/8。用 PHP 可以这样快速判断function isReservedIpv4($ip) { $num ip2long($ip); return ( ($num 0xFF000000) 0 // 0.0.0.0/8 || ($num 0xFF000000) 0x0A000000 // 10.0.0.0/8 || ($num 0xFFF00000) 0xAC100000 // 172.16.0.0/12 || ($num 0xFFFF0000) 0xC0A80000 // 192.168.0.0/16 || ($num 0x7F000001) // 127.0.0.1 测试用 ); }这段位运算比strpos字符串匹配快得多。对应地IPv6 需要拦::1、fc00::/7、fe80::/10这几段。留着这些保留地址不管最直接的后果是日志分析时把内网扫描识别成了某个公网城市然后引发一串误报工单。别问我为什么知道一线踩过的坑都长这样。5.3 IPv6 查询全部落空现象接口对 IPv4 全正常切到 IPv6 就返回 null日志里全是“未命中”。原因很多离线库源码只实现了 IPv4 的表结构IPv6 段表压根没建或者只建了 VARCHAR 字段存文本查询时用字符串比较去匹配区间结果永远不对。解决IPv6 段表需要单独建地址转数值用DECIMAL(39,0)或拆成两个BIGINT高低位。比较推荐的做法是用VARBINARY(16)存地址查询时把目标 IP 也转成 16 字节二进制然后在BETWEEN条件里直接用二进制比较SELECT r.country, r.province, r.city, se.isp FROM ipv6_segment se JOIN region r ON se.region_id r.id WHERE se.start_bin ? AND se.end_bin ? LIMIT 1;MySQL 对VARBINARY的比较是逐字节按字典序进行的这正好符合 IP 地址的数值顺序不需要额外的转换函数。在 Python 或 PHP 端把 IPv6 转 16 字节用inet_pton/ip2bin这类现成函数别自己拼字符串很容易在压缩写法::上翻车。5.4 高并发下查询突然变慢现象上线初期一天几十万查询好好的某天流量增长了 10 倍数据库 CPU 飙到 100%接口 P99 延迟从 5ms 涨到 2 秒。原因几乎所有流量都在打数据库ipv4_segment表不在 InnoDB buffer pool 里每次查询都走磁盘索引。或者查询没有走覆盖索引命中后还要回表读start_ip、end_ip等字段。解决先确认 buffer pool 容量。几十万行段的表只有几十 MBInnoDB buffer pool 通常默认 128MB理论上装得下。但如果你表里还塞了isp、updated_at等字段行宽变大后可能超出缓冲池导致频繁换页。最直接的解法是把start_ip、end_ip从主查询里拿掉只保留start_num、end_num、region_id、isp覆盖索引里的字段或者干脆走第 4 章的内存二分方案彻底绕开 MySQL。真正压垮数据库的往往不是总数据量而是索引设计里差一个字段覆盖让执行计划多了一次回表。5.5 离线库的许可证与商用边界现象系统上线后收到版权方的合规问询或者发现自己用的库是某商业产品扒出来的数据。原因IP 定位数据是典型的“数据有版权、来源有区分”。免费的公共库有各自的许可证有的允许商用但需署名有的仅限个人学习商业库按年授权。很多源码包里集成的是从别家抓来的数据用了就是给自己埋雷。解决确认你用的离线库许可证条款。对商用来说优先选择明确标注可商用的数据源每年花点预算采购对学习或内部工具用免费库但保留数据来源说明。系统架构上要把“数据源”和“代码”解耦——库文件单独放不跟源码打包分发更新时只替换数据文件。这样即便后期换库代码层改动也能控制在导入脚本和字段映射上。在合规这件事上没有后悔药上生产前就把授权问题定死。6. 最后一步用自测脚本验证精度并配上增量更新习惯6.1 造一批“已知坐标”的样本算城市命中率定位系统上线前必须回答一个问题这套库到底准不准。空口说“感觉挺准”没用用下面这段脚本拉一批你确定归属的 IP 样本跑一遍就能得到城市级命中率import csv import subprocess def query_api(ip): # 替换成你自己的查询接口地址 out subprocess.run( [curl, -s, fhttp://127.0.0.1:8080/ip?ip{ip}], capture_outputTrue, textTrue ) return out.stdout with open(sample_city.csv, encodingutf-8) as f: reader csv.DictReader(f) hit 0 total 0 for row in reader: ip row[ip] expected row[city] resp query_api(ip) if expected in resp: hit 1 total 1 print(f城市命中率: {hit}/{total} {hit/total:.2%})样本怎么造常见做法是拿你业务里真实出现的公网 IP人工标注它们的城市凑 1000 ~ 3000 条另一个来源是知名云厂商的 DNS 出口 IP、电商物流测试 IP。命中率超过 95%说明这份库和数据预处理链路基本可上线低于 90%优先检查导入脚本是否正确处理了段边界再考虑换数据源。注意样本里的城市要精确到市级区县差异不能算 miss这一版系统定位到市级才是及格线。6.2 增量更新与灰度切换给定位系统留一剂后悔药离线库更新不能“一把梭”覆盖线上。我自己的习惯是新库导入到独立表ipv4_segment_new跑验证脚本把新旧库抽样对比一致率确认问题后再切换。切换可以靠改表名实现MySQL 的RENAME TABLE是原子操作不会在切换瞬间出现查询失败RENAME TABLE ipv4_segment TO ipv4_segment_old, ipv4_segment_new TO ipv4_segment;切换后保留旧表一周一旦新库有明显的漂移问题立刻改名切回。配合第 4 章的缓存版本号机制让旧缓存一并失效。这套流程做完定位系统才算真正达到可运维状态。我踩过的最大一个坑就是刚上线时为图省事直接覆盖原表结果新库的城市归属全偏线上误判持续了半天才追回来。从那以后每次换库都老老实实走“导入新表 → 抽样对比 → 原子切换 → 保留旧表”这条路这几乎成了我做所有数据依赖型系统的一致习惯。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
BP-Adaboost实战:用神经网络作可微弱学习器的集成方法 简介:本资源是一个基于C#实现的BP-AdaBoost集成学习算法项目,面向机器学习初学者与Windows平台开发者,聚焦于强分类器构建与皮肤病变预测任务。项目通过融合反向传播神经网络(BP)与AdaBoost自适应加权机制,… · 2026/9/26 4:25:51
机器学习大作业实战:分类回归聚类全流程代码与避坑指南 简介:本资源是一套面向高校机器学习初学者的课程实践项目合集,覆盖分类、回归与聚类三大核心任务,适用于期末大作业、课程设计及算法入门实战。压缩包共48个文件,以28个Jupyter Notebook(.ipynb)为主体&… · 2026/9/26 4:25:51
SSM图书管理系统毕设实战:可部署源码+论文写作指南 简介:本资源是一套面向计算机专业本科生的毕业设计实战项目,聚焦图书馆信息化管理场景,提供从需求分析到系统落地的完整实践范例。资源包含可运行的图书管理系统源代码与配套毕业论文,覆盖图书入库/借阅/归还/查询等核心业务逻辑&… · 2026/9/26 4:25:51
超声波气象站完整工程实现:从时差法原理到现场部署 做气象监测这块的人应该都有体会,真正让项目运维头疼的往往不是数据处理算法,而是机械风速仪上那三个风杯——轴承磨损、沙尘堵转、冬天结冰卡死,一年下来维护工时比传感器本身的采购价还贵。所以当超声波方案逐渐成熟之后,我身边… · 2026/9/26 6:21:29
2026最新油猴脚本实测:替代PanDownload实现百度网盘极速下载 现代生活中文件往来越来越频繁,无论是工作资料还是学习文档,网盘都成为了我们获取资源的重要途径。然而很多朋友在日常使用中总会碰到速度迟缓的问题,明明自己的宽带额度并不低,但实际的传输体验却远远没有达到预期,这… · 2026/9/26 6:21:29
ESP32 -O2优化崩溃根因与LAN8720稳定方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 6:21:29
2026最新PanDownload复活版评测:告别百度网盘限速实测可用 平时我们在使用网盘保存或者获取文件时,经常会遇到进度条走得特别缓慢的情况。看着原本不大的文件需要耗费很长时间才能保存到本地,确实很容易让人感到焦急。很多人遇到这种情况,第一反应往往会觉得是远端服务器不稳定,但实际上很… · 2026/9/26 6:21:29
金融科技平台落地:账户体系、资金安全与合规设计 说实话,"financial-services"这个名字一看就是个筐,什么都能往里装。我刚接手这类项目时也犯过迷糊,以为金融服务就是把支付接口对接一下、做个账本、挂个后台管理页面就算完事。真正扎进去才发现,这个领域的水深在业务… · 2026/9/26 6:21:29
SpringBoot+Vue校园足球俱乐部管理系统设计与实现全解析 开说。SpringBoot、Vue、校园足球俱乐部管理系统,这三个关键词拼在一起,基本就能猜到这是在搞什么了——毕业设计里出场率极高的“前后端分离管理系统”路子。我当年带过的实习生里,差不多十个有八个选题都是这类:要么是体育馆管理… · 2026/9/26 6:21:23
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46