起因很简单:博客侧边栏的天气卡片,明明人在成都,却总是显示"北京"。
一、问题:天气定位为什么老不准?
我的博客(Sky-Luxas 主题)天气卡片用的是 IP 定位 → 城市 → 查天气 的链路。第一步 IP 定位原本依赖第三方接口 pconline.xoku.cn,但这个接口越来越不靠谱:
- 经常返回反爬 HTML 页面而不是 JSON(
document.getElementById('trace')...那种),前端解析直接失败; - 请求超时、被拦截是家常便饭;
- 一旦失败,代码就降级到兜底默认值——配置里写死的 "北京"。
于是:定位失败 → 降级北京 → 缓存 30 分钟 → 一整天都显示北京。
二、选型:翻遍第三方接口后的结论
我实测了一批主流 IP 定位服务,结果如下:
| 接口 | 实测结果 | 评价 |
|---|---|---|
pconline.xoku.cn(原用) |
返回反爬 HTML | ❌ 不可靠 |
ip-api.com |
稳定、字段全、有经纬度 | ✅ 好,但免费版不支持 HTTPS,且限频 |
ipinfo.io |
稳定 | ✅ 但免费无鉴权限流 |
whois.pconline.com.cn |
返回 GBK 编码 JSON | ⚠️ 要转码,麻烦 |
| 某淘宝 IP 库翻版 | HTTPS 证书过期,只能走 HTTP | ❌ 国外只有国家/大洲粒度 |
结论:第三方在线接口要么不稳定、要么有限流、要么国外定位粗糙。不如自建——把 IP 库放在自己服务器上,毫秒级响应、不限流、不出网。
三、选型:为什么是 ip2region
ip2region(19k+ star 的老牌开源项目):
- 完全离线:自带
xdb数据文件,本地查询,不依赖任何在线 API; - 速度快:微秒级查询(官方号称 10 微秒量级);
- 精度好:国内精确到区县级(中文),国外到国家+州(英文);
- 多语言:官方提供 Python / Node / Go / Java 等 14 种客户端;
- 体积小:IPv4 库才 11MB。
完美契合"自建 IP 查询 API"的需求。
四、实现:FastAPI + py-ip2region
服务用 Python 写,核心就几十行:
# app.py (核心部分)
import ip2region.util as util
import ip2region.searcher as xdb
# 启动时把整个 xdb 载入内存,全局复用
SEARCHER_V4 = xdb.new_with_buffer(util.IPv4, util.load_content_from_file("ip2region_v4.xdb"))
@app.get("/api/search")
def search(ip: str = None):
region = SEARCHER_V4.search(ip)
parts = region.split("|")
return {
"ip": ip,
"region": region,
"country": parts[0], "province": parts[1],
"city": parts[2], "isp": parts[3], "iso_code": parts[4],
}
要点:
- 全内存缓存(
new_with_buffer):查询完全在内存里做,无磁盘 IO,可安全并发; - 自动判别 IPv4 / IPv6;
- 支持无参数查询(查访问者真实 IP,兼容
X-Forwarded-For反代头); - 附带一个
/healthz健康检查接口,供 Docker 探活。
五、部署:Docker Compose 一键起
# docker-compose.yml
services:
ip2region-api:
build: .
image: ip2region-api:latest
container_name: ip2region-api
restart: unless-stopped
ports:
- "8764:8080"
healthcheck:
test: ["CMD", "python3", "-c", "import urllib.request;urllib.request.urlopen('http://127.0.0.1:8080/healthz', timeout=3)"]
镜像基于 python:3.11-slim,requirements.txt 固定版本,数据文件打进镜像。服务器上 docker compose up -d --build 一条命令搞定。
六、踩坑:CORS 和缓存
坑 1:CORS(最关键的坑)
部署好后 curl 一切正常,但浏览器里天气还是北京。排查发现:页面在 blog.luxas.top,请求的是 api.luxas.top——跨域!我的服务没配 CORS 头,浏览器直接拦截了 fetch,JS 拿不到结果 → 降级北京。
修复:加 FastAPI 的 CORSMiddleware,白名单只放行自己的站点:
app.add_middleware(
CORSMiddleware,
allow_origins=["https://blog.luxas.top", "https://luxas.top"],
allow_methods=["GET", "POST"],
)
坑 2:天气缓存
就算定位修好了,浏览器 localStorage 里还躺着 30 分钟的旧缓存(sky_weather_cache_v13)。改完代码还得清缓存/等过期,不然怎么看都是旧结果。
七、API 用法
服务部署在 https://api.luxas.top/ip2region/,用法如下:
① 查询指定 IP
curl "https://api.luxas.top/ip2region/api/search?ip=110.110.110.110"
{
"ip": "110.110.110.110",
"region": "中国|黑龙江省|哈尔滨市|铁通|CN",
"country": "中国",
"province": "黑龙江省",
"city": "哈尔滨市",
"isp": "铁通",
"iso_code": "CN"
}
② 查访问者自己的 IP(不带参数)
curl "https://api.luxas.top/ip2region/api/search"
③ 健康检查 / 版本信息
curl "https://api.luxas.top/ip2region/healthz" # ok
curl "https://api.luxas.top/ip2region/api/version"
④ 浏览器可视化:直接打开 https://api.luxas.top/ip2region/,有简单的查询页面。
八、效果
- 天气卡片定位终于显示成都了;
- 顺带一提:排查 CPU 问题时还发现天气卡片有一堆
blur-2xl + animate-pulse的持续动画在烧 CPU,一并修掉了——主页单核占用直接降下来; - 这个 API 后续还能复用到其他场景(评论区归属地展示等),也算给
api.luxas.top子域开张了。
之前还做过一个网易云音乐在线解析的api:https://netease-api2.luxas.top,但是得时不时去更新cookie,懒得折腾,遂弃之。
以后如果再做点什么API也加到这个api.luxas.top域名吧。