前几天和@上善若水聊起IP归属地这事儿,发现市面上免费的IP查询服务,精度其实都挺一般的。
为什么我选择本地IP数据库?
对我来说,博客上显示IP归属地,真没必要搞什么付费精确定位。能知道访客大概在哪个省份、哪个城市,已经完全够用了。本地数据库有几个实实在在的好处:
- 够稳定:不不依赖外部接口,不用担心API挂掉
- 响应快:本地查询,速度比在线查询快不少
- 隐私性好:不需要将用户IP发送给第三方
从纯真IP切换到ip2region
之前Weisay Grace一直用的是纯真IP数据库dat格式,但它20224年9月就正式停更了。虽然出了新的CZDB格式,但需要申请密钥,用起来还是有点麻烦。所以后续主题更新都会改用ip2region。
纯真IP数据库确实年头久了,但也积累了一些问题:
- 数据格式比较乱,错别字时不时能看到
- IPv6支持很基础,国内IPv6一律只显示“中国”
- 我之前还专门用IPlook配合Excel手动修正过dat格式数据库
为什么选择ip2region?
ip2region在今年9月发布了v3.0版本,开始真正支持IPv6:
- IPv6能定位到地级市(虽然准确度还有提升空间)
- 数据数据格式规范,看着舒服规范统一
- 免费开源
- 项目还在持续更新维护
虽然ip2region只能到地级市,不如纯真IP有些能到区县镇那么细,但对博客评论显示来说,知道到城市这个级别,真的已经足够了。
动手在WordPress里集成ip2region
简单四步就能搞定:
1、下载必要文件
从GitHub或Gitee下载ip2region,主要需要这三个文件:
data/ip2region_v4.xdb(IPv4数据库)
data/ip2region_v6.xdb(IPv6数据库)
binding/php/xdb/Searcher.class.php(查询核心文件)
2、创建转换文件
在主题文件夹中创建ip2region.php,添加下面的IP转换代码,注意代码中引用的文件路径。
<?php
/**
* Ip2region 是一个离线 IP 数据管理框架和定位库,支持 IPv4 和 IPv6。
*
* 官方社区:https://ip2region.net/
*/
require_once __DIR__ . '/xdb/Searcher.class.php';
use \ip2region\xdb\Util;
use \ip2region\xdb\Searcher;
//初始化,使用向量索引
function init_ip2region_vector($dbFile) {
if (!file_exists($dbFile)) {
error_log("IP数据库文件不存在: " . $dbFile);
return null;
}
try {
// 读取文件头,获取版本信息
$header = Util::loadHeaderFromFile($dbFile);
$version = Util::versionFromHeader($header);
// 加载向量索引
$vIndex = Util::loadVectorIndexFromFile($dbFile);
// 创建 Searcher
return Searcher::newWithVectorIndex($version, $dbFile, $vIndex);
} catch (Exception $e) {
error_log("IP数据库初始化失败: " . $e->getMessage());
return null;
}
}
// 全局 Searcher,显式初始化为 null
global $ip2region_searcher_v4, $ip2region_searcher_v6;
$ip2region_searcher_v4 = $ip2region_searcher_v4 ?? null;
$ip2region_searcher_v6 = $ip2region_searcher_v6 ?? null;
//获取 IPv4 或 IPv6 Searcher
function get_ip_searcher($ip) {
global $ip2region_searcher_v4, $ip2region_searcher_v6;
// 判断 ip 类型
$isIpv6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
$isIpv4 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
// 尝试加载 IPv6 searcher
if ($isIpv6) {
if ($ip2region_searcher_v6 === null) {
$dbFile_v6 = __DIR__ . '/data/ip2region_v6.xdb';
$ip2region_searcher_v6 = init_ip2region_vector($dbFile_v6);
}
if ($ip2region_searcher_v6 !== null) {
return $ip2region_searcher_v6;
}
// 如果 IPv6 DB 不可用,继续尝试加载 IPv4(fallback降级)
}
// IPv4 路径(或者作为fallback降级)
if ($ip2region_searcher_v4 === null) {
$dbFile_v4 = __DIR__ . '/data/ip2region_v4.xdb';
$ip2region_searcher_v4 = init_ip2region_vector($dbFile_v4);
}
return $ip2region_searcher_v4;
}
//判断 IP 类型
function get_ip_type($ip) {
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return 'ipv4';
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return 'ipv6';
}
return false;
}
//判断内网IP并返回显示文本
function is_private_ip($ip) {
$ip_type = get_ip_type($ip);
if ($ip_type === 'ipv4') {
if (filter_var(
$ip,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
) === false) {
return '内网IP';
}
} elseif ($ip_type === 'ipv6') {
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE) === false) {
return '内网IP';
}
if ($ip === '::1') {
return '内网IP';
}
if (preg_match('/^fe80:/i', $ip)) {
return '内网IP';
}
}
return false;
}
//IP转换函数
function convertip($ip, $withIsp = false, $simpleMode = false) {
if (!$ip) return '火星';
// 检查内网IP
$private_result = is_private_ip($ip);
if ($private_result !== false) {
return $private_result;
}
// 获取 searcher(延迟加载、并可降级)
$searcher = get_ip_searcher($ip);
if ($searcher === null) {
return '火星'; // 数据库不存在或加载失败
}
try {
$region = $searcher->search($ip);
} catch (Exception $e) {
return '火星';
}
$parts = explode('|', $region);
$country = $parts[0] ?? '';
$province = $parts[1] ?? '';
$city = $parts[2] ?? '';
$isp = $parts[3] ?? '';
// 过滤空值或 '0'
$country = ($country && $country !== '0') ? $country : '';
$province = ($province && $province !== '0') ? $province : '';
$city = ($city && $city !== '0') ? $city : '';
$isp = ($isp && $isp !== '0') ? $isp : '';
$resultParts = [];
if ($country === '中国') {
// 处理直辖市:北京、上海、天津、重庆
$municipalities = ['北京', '上海', '天津', '重庆'];
if (in_array($province, $municipalities)) {
// 直辖市:直接使用市字段,避免重复
$resultParts[] = $city;
} else {
// 处理可能的重叠情况(比如"上海市上海市")
if ($province && $city && $province === $city) {
// 省份和城市相同,只显示一个
$resultParts[] = $city;
} else {
if ($province) $resultParts[] = $province;
if (!$simpleMode && $city) $resultParts[] = $city;
}
}
} else {
if ($country) $resultParts[] = $country;
if ($province) $resultParts[] = $province;
if (!$simpleMode && $city) $resultParts[] = $city;
}
// 可选显示网络ISP
if ($withIsp && $isp) {
$resultParts[] = ' ' . $isp;
}
return implode('', array_filter($resultParts));
}
//简版 - 国内只显示省,国外显示国家 + 省
function convertipsimple($ip, $withIsp = false) {
return convertip($ip, $withIsp, true);
}
?>
3、在主题中加载
在function.php中添加下面代码,注意代码中引用的文件路径:
require get_template_directory() . '/ip2region.php';
4、在评论中调用
想在评论里显示地理位置?用这几个函数就行:
// 显示省市(无运营商)
echo convertip(get_comment_author_IP());
// 显示省市 + 运营商
echo convertip(get_comment_author_IP(), true);
// 只显示省份
echo convertipsimple(get_comment_author_IP());
// 只显示省份 + 运营商,
echo convertipsimple(get_comment_author_IP(), true);
//如果是国外,以上都会加上国家,只有中国时会过滤一下
这样设置完,就能在WordPress评论里就能看到评论者的地理位置了,简单又直观。其实稍微改改获取IP的方式,这套代码也能用在其他PHP博客系统上。
显示下其实蛮好的,有些时候有地域显示交流起来会顺畅很多
纯真的 github 上也有每天更新的库,不过我还没试
其实也没必要精确到区县镇那么细,精确到省市就好了,不过有些博友可能会想精确到区县镇的
我自己修改试试
学习了!如果要加上这个功能,给精确至省份就好的感觉。
我压根就不想开这个功能
测试一下。