更新
This commit is contained in:
@@ -0,0 +1,508 @@
|
||||
# +======================================================================
|
||||
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
|
||||
# +----------------------------------------------------------------------
|
||||
# | Copyright (c) 2021-2026 小蚂蚁云团队 版权所有
|
||||
# +----------------------------------------------------------------------
|
||||
# | 开源协议: Apache-2.0
|
||||
# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
|
||||
# +----------------------------------------------------------------------
|
||||
# | 官方网站: https://www.djangoadmin.cn
|
||||
# +----------------------------------------------------------------------
|
||||
# | 研发团队: 小蚂蚁云团队 荣誉出品
|
||||
# +======================================================================
|
||||
# | 版权和免责声明:
|
||||
# | 一、知识产权声明
|
||||
# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
|
||||
# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
|
||||
# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
|
||||
# | 二、使用限制与禁止条款
|
||||
# | 您理解并同意,禁止将本产品用于以下任何行为:
|
||||
# | 1. 任何违反中华人民共和国法律法规的用途;
|
||||
# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
|
||||
# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
|
||||
# | 三、责任豁免
|
||||
# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
|
||||
# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
|
||||
# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
|
||||
# | 四、违约与法律责任
|
||||
# | 如发现任何违反本声明的行为,本团队有权:
|
||||
# | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
|
||||
# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
|
||||
# | 3. 保留进一步采取刑事控告等法律措施的权利。
|
||||
# | 五、许可范围说明
|
||||
# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
|
||||
# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
|
||||
# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
|
||||
# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
|
||||
# | 成的一切经济损失及商誉损害。
|
||||
# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
|
||||
# | 许可协议》附件为准。
|
||||
# +======================================================================
|
||||
|
||||
"""
|
||||
IP地理位置解析模块
|
||||
=============================================================
|
||||
|
||||
该模块提供根据IP地址获取地理位置信息的功能。
|
||||
支持多种IP数据库格式(如纯真IP数据库、GeoIP2等),
|
||||
并提供缓存机制,减少重复查询。
|
||||
|
||||
主要功能:
|
||||
1. 根据IP地址获取省市信息
|
||||
2. 支持多种IP数据库格式
|
||||
3. 提供缓存机制,提高查询效率
|
||||
4. 支持本地数据库和在线API两种方式
|
||||
|
||||
依赖模块:
|
||||
- requests: 调用在线API(可选)
|
||||
- ipip-ipdb: 支持IPIP.NET数据库(可选)
|
||||
- geoip2: 支持MaxMind GeoIP2数据库(可选)
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import hashlib
|
||||
from functools import lru_cache
|
||||
from typing import Optional, Tuple, Dict
|
||||
|
||||
import requests
|
||||
from django.core.cache import cache
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# =============================================================
|
||||
# 配置项
|
||||
# =============================================================
|
||||
|
||||
# IP数据库类型配置
|
||||
# 可选值: 'local' - 本地数据库, 'api' - 在线API
|
||||
IP_LOCATION_SOURCE = getattr(__import__('config.env', fromlist=['IP_LOCATION_SOURCE']),
|
||||
'IP_LOCATION_SOURCE', 'api')
|
||||
|
||||
# 本地数据库文件路径
|
||||
IP_DATABASE_PATH = getattr(__import__('config.env', fromlist=['IP_DATABASE_PATH']),
|
||||
'IP_DATABASE_PATH',
|
||||
os.path.join(os.path.dirname(__file__), 'data', 'qqwry.ipdb'))
|
||||
|
||||
# 在线API配置
|
||||
IP_API_URL = getattr(__import__('config.env', fromlist=['IP_API_URL']),
|
||||
'IP_API_URL', 'https://ip.taobao.com/outGetIpInfo')
|
||||
IP_API_TIMEOUT = 5 # API请求超时时间(秒)
|
||||
|
||||
# 缓存配置
|
||||
IP_CACHE_ENABLED = True # 是否启用缓存
|
||||
IP_CACHE_TIMEOUT = 86400 # 缓存时间(秒),默认1天
|
||||
|
||||
# 默认返回信息(当无法获取时)
|
||||
DEFAULT_LOCATION = "未知"
|
||||
DEFAULT_PROVINCE = "未知"
|
||||
DEFAULT_CITY = "未知"
|
||||
|
||||
|
||||
# =============================================================
|
||||
# IP数据库加载器
|
||||
# =============================================================
|
||||
|
||||
class IPDatabaseLoader:
|
||||
"""
|
||||
IP数据库加载器
|
||||
|
||||
支持多种IP数据库格式:
|
||||
1. 纯真IP数据库(QQWry)
|
||||
2. IPIP.NET数据库
|
||||
3. MaxMind GeoIP2数据库
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
_db = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
self._db = None
|
||||
self._load_database()
|
||||
|
||||
def _load_database(self):
|
||||
"""加载IP数据库"""
|
||||
if IP_LOCATION_SOURCE != 'local':
|
||||
return
|
||||
|
||||
try:
|
||||
# 尝试加载IPIP.NET数据库(.ipdb格式)
|
||||
if os.path.exists(IP_DATABASE_PATH) and IP_DATABASE_PATH.endswith('.ipdb'):
|
||||
import ipip
|
||||
self._db = ipip.IPDatabase.open(IP_DATABASE_PATH)
|
||||
logger.info(f"加载IPIP数据库成功: {IP_DATABASE_PATH}")
|
||||
|
||||
# 尝试加载纯真IP数据库(.dat格式)
|
||||
elif os.path.exists(IP_DATABASE_PATH) and IP_DATABASE_PATH.endswith('.dat'):
|
||||
import qqwry
|
||||
self._db = qqwry.QQWry(IP_DATABASE_PATH)
|
||||
logger.info(f"加载纯真IP数据库成功: {IP_DATABASE_PATH}")
|
||||
|
||||
# 尝试加载GeoIP2数据库
|
||||
elif os.path.exists(IP_DATABASE_PATH) and IP_DATABASE_PATH.endswith('.mmdb'):
|
||||
import geoip2.database
|
||||
self._db = geoip2.database.Reader(IP_DATABASE_PATH)
|
||||
logger.info(f"加载GeoIP2数据库成功: {IP_DATABASE_PATH}")
|
||||
|
||||
else:
|
||||
logger.warning(f"IP数据库文件不存在或格式不支持: {IP_DATABASE_PATH}")
|
||||
|
||||
except ImportError as e:
|
||||
logger.warning(f"IP数据库依赖未安装: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"加载IP数据库失败: {e}")
|
||||
|
||||
def get_location(self, ip: str) -> Tuple[str, str, str]:
|
||||
"""
|
||||
从本地数据库获取IP地理位置
|
||||
|
||||
Args:
|
||||
ip: IP地址
|
||||
|
||||
Returns:
|
||||
Tuple[str, str, str]: (省份, 城市, 详细地址)
|
||||
"""
|
||||
if not self._db:
|
||||
return DEFAULT_PROVINCE, DEFAULT_CITY, DEFAULT_LOCATION
|
||||
|
||||
try:
|
||||
# IPIP.NET数据库查询
|
||||
if hasattr(self._db, 'find'):
|
||||
info = self._db.find(ip)
|
||||
if info:
|
||||
# IPIP.NET返回格式: ['中国', '广东', '深圳', '电信']
|
||||
province = info[1] if len(info) > 1 else DEFAULT_PROVINCE
|
||||
city = info[2] if len(info) > 2 else DEFAULT_CITY
|
||||
location = f"{province} {city}".strip()
|
||||
return province, city, location
|
||||
|
||||
# 纯真IP数据库查询
|
||||
elif hasattr(self._db, 'lookup'):
|
||||
result = self._db.lookup(ip)
|
||||
if result:
|
||||
# 解析纯真数据库返回的地址
|
||||
location_str = str(result)
|
||||
parts = location_str.split()
|
||||
province = parts[0] if parts else DEFAULT_PROVINCE
|
||||
city = parts[1] if len(parts) > 1 else DEFAULT_CITY
|
||||
return province, city, location_str
|
||||
|
||||
# GeoIP2数据库查询
|
||||
elif hasattr(self._db, 'city'):
|
||||
response = self._db.city(ip)
|
||||
province = response.country.name if response.country.name else DEFAULT_PROVINCE
|
||||
city = response.city.name if response.city.name else DEFAULT_CITY
|
||||
location = f"{province} {city}".strip()
|
||||
return province, city, location
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"IP数据库查询失败: {ip}, 错误: {e}")
|
||||
|
||||
return DEFAULT_PROVINCE, DEFAULT_CITY, DEFAULT_LOCATION
|
||||
|
||||
|
||||
# =============================================================
|
||||
# 在线API查询器
|
||||
# =============================================================
|
||||
|
||||
class IPAPILoader:
|
||||
"""
|
||||
在线IP查询API加载器
|
||||
|
||||
支持多种在线API服务:
|
||||
1. 淘宝IP地址库(默认)
|
||||
2. 百度IP定位API
|
||||
3. 高德IP定位API
|
||||
4. ip-api.com
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_location(ip: str) -> Tuple[str, str, str]:
|
||||
"""
|
||||
通过在线API获取IP地理位置
|
||||
|
||||
Args:
|
||||
ip: IP地址
|
||||
|
||||
Returns:
|
||||
Tuple[str, str, str]: (省份, 城市, 详细地址)
|
||||
"""
|
||||
# 本地IP不需要查询
|
||||
if ip in ['127.0.0.1', 'localhost', '::1']:
|
||||
return "本地", "本地", "本地"
|
||||
|
||||
# 内网IP不需要查询
|
||||
if ip.startswith(('192.168.', '10.', '172.16.', '172.17.', '172.18.',
|
||||
'172.19.', '172.20.', '172.21.', '172.22.', '172.23.',
|
||||
'172.24.', '172.25.', '172.26.', '172.27.', '172.28.',
|
||||
'172.29.', '172.30.', '172.31.')):
|
||||
return "内网", "内网", "内网IP"
|
||||
|
||||
# 尝试多种API
|
||||
apis = [
|
||||
IPAPILoader._query_taobao,
|
||||
IPAPILoader._query_ip_api,
|
||||
IPAPILoader._query_pconline,
|
||||
]
|
||||
|
||||
for api in apis:
|
||||
try:
|
||||
province, city, location = api(ip)
|
||||
if province != DEFAULT_PROVINCE:
|
||||
return province, city, location
|
||||
except Exception as e:
|
||||
logger.debug(f"API查询失败 {api.__name__}: {e}")
|
||||
continue
|
||||
|
||||
return DEFAULT_PROVINCE, DEFAULT_CITY, DEFAULT_LOCATION
|
||||
|
||||
@staticmethod
|
||||
def _query_taobao(ip: str) -> Tuple[str, str, str]:
|
||||
"""
|
||||
淘宝IP地址库API
|
||||
API文档: https://ip.taobao.com/
|
||||
"""
|
||||
url = f"https://ip.taobao.com/outGetIpInfo"
|
||||
params = {
|
||||
'ip': ip,
|
||||
'accessKey': 'alibaba-inc'
|
||||
}
|
||||
|
||||
response = requests.get(url, params=params, timeout=IP_API_TIMEOUT)
|
||||
data = response.json()
|
||||
|
||||
if data.get('code') == 0:
|
||||
ip_data = data.get('data', {})
|
||||
province = ip_data.get('region', DEFAULT_PROVINCE)
|
||||
city = ip_data.get('city', DEFAULT_CITY)
|
||||
location = f"{province} {city}".strip()
|
||||
if location == " XX":
|
||||
location = province
|
||||
return province, city, location
|
||||
|
||||
raise Exception(f"淘宝API返回错误: {data.get('msg')}")
|
||||
|
||||
@staticmethod
|
||||
def _query_ip_api(ip: str) -> Tuple[str, str, str]:
|
||||
"""
|
||||
ip-api.com API (免费版)
|
||||
API文档: http://ip-api.com/
|
||||
"""
|
||||
url = f"http://ip-api.com/json/{ip}"
|
||||
params = {
|
||||
'lang': 'zh-CN',
|
||||
'fields': 'status,country,regionName,city'
|
||||
}
|
||||
|
||||
response = requests.get(url, params=params, timeout=IP_API_TIMEOUT)
|
||||
data = response.json()
|
||||
|
||||
if data.get('status') == 'success':
|
||||
province = data.get('regionName', DEFAULT_PROVINCE)
|
||||
city = data.get('city', DEFAULT_CITY)
|
||||
location = f"{data.get('country', '')} {province} {city}".strip()
|
||||
return province, city, location
|
||||
|
||||
raise Exception(f"ip-api返回错误")
|
||||
|
||||
@staticmethod
|
||||
def _query_pconline(ip: str) -> Tuple[str, str, str]:
|
||||
"""
|
||||
太平洋电脑网IP查询API
|
||||
"""
|
||||
url = f"https://whois.pconline.com.cn/ipJson.jsp"
|
||||
params = {
|
||||
'ip': ip,
|
||||
'json': 'true'
|
||||
}
|
||||
|
||||
response = requests.get(url, params=params, timeout=IP_API_TIMEOUT)
|
||||
# 太平洋API返回的是GBK编码
|
||||
response.encoding = 'gbk'
|
||||
data = response.text
|
||||
|
||||
# 解析JSONP格式的返回
|
||||
if data.startswith('try{'):
|
||||
data = data[4:-2]
|
||||
|
||||
result = json.loads(data)
|
||||
|
||||
if result:
|
||||
province = result.get('pro', DEFAULT_PROVINCE)
|
||||
city = result.get('city', DEFAULT_CITY)
|
||||
location = result.get('addr', DEFAULT_LOCATION)
|
||||
return province, city, location
|
||||
|
||||
raise Exception("太平洋API返回错误")
|
||||
|
||||
|
||||
# =============================================================
|
||||
# 主入口函数
|
||||
# =============================================================
|
||||
|
||||
def get_ip_location(ip: str, use_cache: bool = True) -> Dict[str, str]:
|
||||
"""
|
||||
根据IP地址获取地理位置信息
|
||||
|
||||
Args:
|
||||
ip: IP地址字符串
|
||||
use_cache: 是否使用缓存,默认True
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 地理位置信息,包含以下字段:
|
||||
- province: 省份
|
||||
- city: 城市
|
||||
- location: 详细地址
|
||||
- ip: 原始IP地址
|
||||
|
||||
使用示例:
|
||||
>>> location = get_ip_location('183.14.30.110')
|
||||
>>> print(location['province']) # 广东省
|
||||
>>> print(location['city']) # 深圳市
|
||||
>>> print(location['location']) # 广东省 深圳市
|
||||
|
||||
Notes:
|
||||
1. 支持IPv4和IPv6地址
|
||||
2. 内网IP返回"内网"标识
|
||||
3. 查询结果会缓存,提高性能
|
||||
"""
|
||||
# 参数校验
|
||||
if not ip or ip == 'unknown':
|
||||
return _build_location_result(DEFAULT_PROVINCE, DEFAULT_CITY, DEFAULT_LOCATION, ip)
|
||||
|
||||
# 检查缓存
|
||||
cache_key = f"ip_location_{ip}"
|
||||
if use_cache and IP_CACHE_ENABLED:
|
||||
cached = cache.get(cache_key)
|
||||
if cached:
|
||||
logger.debug(f"从缓存获取IP地理位置: {ip} -> {cached}")
|
||||
return cached
|
||||
|
||||
try:
|
||||
# 根据配置选择查询方式
|
||||
if IP_LOCATION_SOURCE == 'local':
|
||||
# 使用本地数据库
|
||||
loader = IPDatabaseLoader()
|
||||
province, city, location = loader.get_location(ip)
|
||||
else:
|
||||
# 使用在线API
|
||||
province, city, location = IPAPILoader.get_location(ip)
|
||||
|
||||
# 构建结果
|
||||
result = _build_location_result(province, city, location, ip)
|
||||
|
||||
# 缓存结果
|
||||
if use_cache and IP_CACHE_ENABLED:
|
||||
cache.set(cache_key, result, IP_CACHE_TIMEOUT)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取IP地理位置失败: {ip}, 错误: {e}")
|
||||
return _build_location_result(DEFAULT_PROVINCE, DEFAULT_CITY, DEFAULT_LOCATION, ip)
|
||||
|
||||
|
||||
def get_ip_province(ip: str) -> str:
|
||||
"""
|
||||
获取IP所在省份
|
||||
|
||||
Args:
|
||||
ip: IP地址
|
||||
|
||||
Returns:
|
||||
str: 省份名称
|
||||
"""
|
||||
location = get_ip_location(ip)
|
||||
return location.get('province', DEFAULT_PROVINCE)
|
||||
|
||||
|
||||
def get_ip_city(ip: str) -> str:
|
||||
"""
|
||||
获取IP所在城市
|
||||
|
||||
Args:
|
||||
ip: IP地址
|
||||
|
||||
Returns:
|
||||
str: 城市名称
|
||||
"""
|
||||
location = get_ip_location(ip)
|
||||
return location.get('city', DEFAULT_CITY)
|
||||
|
||||
|
||||
def _build_location_result(province: str, city: str, location: str, ip: str) -> Dict[str, str]:
|
||||
"""
|
||||
构建地理位置结果字典
|
||||
|
||||
Args:
|
||||
province: 省份
|
||||
city: 城市
|
||||
location: 详细地址
|
||||
ip: IP地址
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 标准化的地理位置信息
|
||||
"""
|
||||
# 清理数据
|
||||
province = province.strip() if province else DEFAULT_PROVINCE
|
||||
city = city.strip() if city else DEFAULT_CITY
|
||||
location = location.strip() if location else f"{province} {city}".strip()
|
||||
|
||||
# 如果省份和城市相同,只显示一个
|
||||
if province == city:
|
||||
location = province
|
||||
|
||||
return {
|
||||
'province': province,
|
||||
'city': city,
|
||||
'location': location,
|
||||
'ip': ip
|
||||
}
|
||||
|
||||
|
||||
def batch_get_ip_locations(ips: list) -> Dict[str, Dict[str, str]]:
|
||||
"""
|
||||
批量获取多个IP的地理位置信息
|
||||
|
||||
Args:
|
||||
ips: IP地址列表
|
||||
|
||||
Returns:
|
||||
Dict[str, Dict[str, str]]: IP地址到地理位置信息的映射
|
||||
"""
|
||||
result = {}
|
||||
for ip in ips:
|
||||
result[ip] = get_ip_location(ip)
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================
|
||||
# 便捷函数
|
||||
# =============================================================
|
||||
|
||||
def is_internal_ip(ip: str) -> bool:
|
||||
"""
|
||||
判断是否为内网IP
|
||||
|
||||
Args:
|
||||
ip: IP地址
|
||||
|
||||
Returns:
|
||||
bool: 是否为内网IP
|
||||
"""
|
||||
if ip in ['127.0.0.1', 'localhost', '::1']:
|
||||
return True
|
||||
|
||||
if ip.startswith(('192.168.', '10.', '172.16.', '172.17.', '172.18.',
|
||||
'172.19.', '172.20.', '172.21.', '172.22.', '172.23.',
|
||||
'172.24.', '172.25.', '172.26.', '172.27.', '172.28.',
|
||||
'172.29.', '172.30.', '172.31.')):
|
||||
return True
|
||||
|
||||
return False
|
||||
Reference in New Issue
Block a user