Files
fangke/server/tracker/services/geo.py
T
浅念 138b472485 feat: ip2region 离线地理解析(优雅降级)
GeoResolver 进程级单例,c_buffer 模式一次读入内存(线程安全)。
适配 py-ip2region 3.0.4 实际 API:new_with_buffer(version, buff) 需
Version 对象,由 xdb 头 256 字节探测(2.0 老库自动归 IPv4)。
数据段布局按结构版本分派:3.0 新库 国家|省|城市|ISP|国家代码,
2.0 老库 国家|区域|省|市|ISP——错位会把 ISP 串进 city。
包/xdb 缺失或损坏时 resolve 一律返回 None,测试全绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-15 12:28:01 +08:00

79 lines
3.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import functools
import ipaddress
from django.conf import settings
def parse_region(raw, new_layout):
"""ip2region 五段地区串 → {"country", "province", "city"};不足 4 段/空串返回 None。
两种数据布局(以 xdb 文件头的结构版本区分):
- 旧版 2.0 结构:"国家|区域|省|市|ISP",省=parts[2]、市=parts[3];
- 新版 3.0 结构(现行 v4/v6 库):"国家|省/区域|城市|ISP|国家代码",
省=parts[1]、市=parts[2],ISP 段在 parts[3]——映射错位会把 ISP 串进 city。
缺失段为 ip2region 的 "0" 占位,原样保留,由展示层决定如何呈现。
"""
parts = (raw or "").split("|")
if len(parts) < 4:
return None
if new_layout:
country, province, city = parts[0], parts[1], parts[2]
else:
country, province, city = parts[0], parts[2], parts[3]
return {"country": country, "province": province, "city": city}
class GeoResolver:
"""离线 IP 地理解析(ip2region xdb,进程级单例)。
py-ip2region 3.0.4 实际 API(以其源码为准,与官方旧 README 不同):
- ``searcher.new_with_buffer(version, c_buffer)`` 需要先传 ``util.IPv4``/``util.IPv6``
版本对象(由 xdb 文件头 256 字节经 ``util.Header`` + ``util.version_from_header``
探测;2.0 结构老库自动归为 IPv4);
- ``Searcher`` 不支持 with 上下文管理器(无 __enter__/__exit__);
- c_buffer 模式下 ``search(ip)`` 是纯内存切片(无文件 IO、无共享可变状态,
官方声明线程安全),返回上述五段字符串。
包缺失 / xdb 未下载 / 文件损坏时优雅降级:resolve 一律返回 None。
"""
def __init__(self):
self._searcher = None
self._new_layout = True
try:
from ip2region import searcher as ip2region_searcher
from ip2region import util as ip2region_util
with open(settings.IP2REGION_XDB, "rb") as f:
buff = f.read()
if len(buff) < ip2region_util.HeaderInfoLength:
return # 文件过短,绝非合法 xdb
header = ip2region_util.Header(buff[: ip2region_util.HeaderInfoLength])
version = ip2region_util.version_from_header(header)
if version is None:
return # 无法识别的 xdb 结构/IP 版本
# 结构版本同时决定数据段布局:2.0 旧布局 / 3.0 新布局
self._new_layout = header.version >= ip2region_util.XdbStructure30
self._searcher = ip2region_searcher.new_with_buffer(version, buff)
except Exception:
self._searcher = None
@functools.lru_cache(maxsize=10000)
def resolve(self, ip: str):
"""返回 {"country", "province", "city"},内网/环回/非法 IP 或降级时返回 None。
结果被 lru_cache 缓存(键含 self,单例常驻故无泄漏问题),调用方勿修改返回的 dict。"""
if not ip:
return None
try:
if ipaddress.ip_address(ip).is_private:
return None
except ValueError:
return None
if not self._searcher:
return None
try:
raw = self._searcher.search(ip)
return parse_region(raw, self._new_layout)
except Exception:
return None