- 后端
【免费下载链接】YOURLS
🔗 The 𝘥𝘦 𝘧𝘢𝘤𝘵𝘰 standard, self hosted, powerful and customizable, URL shortener in PHP
本篇技术指南以仓库内 MaxMind GeoIP2 PHP API 的官方说明(includes/vendor/geoip2/geoip2/README.md)为主体,完整讲解其两种使用形态——本地.mmdb数据库读取与远程 Web 服务查询,并结合 YOURLS 的源码级集成(includes/functions-geo.php)与单元测试(tests/tests/geoip/GeoIPTest.php)深化印证。读完本文,你将掌握 GeoIP2 包的安装方式、Reader/Client的完整调用链、六类数据库查询示例、异常处理边界,以及这套 API 在自托管短链系统中落地为「IP → 国家代码 → 国旗」功能的实际做法。
一、包简介与定位
GeoIP2 PHP API 是一个面向 MaxMind 地理定位服务的 PHP 客户端库,统一封装了两类数据来源:
- GeoIP2 / GeoLite2 Web 服务:通过 HTTP 请求远程接口,按调用次数计费或使用 GeoLite2 免费额度;
- GeoIP2 / GeoLite2 数据库:本地读取
.mmdb格式的 MaxMind DB 文件,一次下载、无限次离线查询。
在 YOURLS 仓库中,该包被完整 vendor 化于 includes/vendor/geoip2/geoip2,根目录 composer.json 声明依赖"geoip2/geoip2" : "^2.10";从 WebService/Client.php 中的常量VERSION = 'v2.13.0'可确认当前捆绑版本为 v2.13.0,其 PHP 端要求PHP 7.2 及以上,并依赖 MaxMind DB Reader(maxmind-db/reader)完成底层数据库解析。
二、安装方式
官方文档提供两种安装途径:Composer(推荐)与 Phar 归档。
2.1 通过 Composer 安装
在项目根目录先下载 Composer:
curl -sS https://getcomposer.org/installer | php执行后项目目录中会出现composer.phar。随后声明并安装依赖:
php composer.phar require geoip2/geoip2:~2.0完成后项目目录下应出现composer.json、composer.lock以及vendor目录;如果使用版本控制系统,应将composer.json纳入版本管理。最后在代码中引入自动加载器:
require 'vendor/autoload.php';补充说明:YOURLS 采用「预打包 vendor」策略,包已直接置于 includes/vendor/geoip2/geoip2 下,其自动加载由 YOURLS 自身的加载器接管,无需在业务代码中手动require vendor/autoload.php;但对于独立使用该库的 PHP 项目,上述三步仍是标准流程。
2.2 通过 Phar 安装
官方也提供包含大部分依赖的 Phar 归档(发布于其 releases 页面)。使用前提:
- 必须安装并启用 PHP 的Phar 扩展;
- 若需要发起 Web 服务请求,还必须安装 PHP 的cURL 扩展(Debian 系发行版通常对应
php-curl软件包),安装后可能需要重启 Web 服务器。
缺少 cURL 扩展时会出现如下致命错误,可作为排查依据:
PHP Fatal error: Uncaught Error: Call to undefined function MaxMind\WebService\curl_version()加载方式只需一行:
require 'geoip2.phar';2.3 可选的 C 扩展加速
MaxMind DB API 附带一个可选的 C 扩展,安装后可显著提升 GeoIP2/GeoLite2数据库查询性能;官方特别说明:该扩展对Web 服务查询没有任何影响(Web 服务的耗时瓶颈在网络请求而非本地解析)。若追求高 QPS 的离线查询场景,值得按该 API 的安装说明配置此扩展。
三、IP 地理定位的精度前提
文档明确警示:IP 地理定位天然不精确。位置点往往靠近人口中心,GeoIP2 数据库或 Web 服务给出的任何位置信息,都不能用来定位到具体地址或家庭住户。在 YOURLS 的实际用法中,这层数据仅用于展示「国家」级别的统计与国旗(详见第七节),正是对精度边界合理取舍的示例。
四、数据库读取(Database Reader)
4.1 基本用法与对象模型
使用数据库前,须以数据库文件路径作为构造函数的第一个参数创建\GeoIp2\Database\Reader对象:
$reader = new \GeoIp2\Database\Reader('/usr/local/share/GeoIP/GeoIP2-City.mmdb');查看 Reader.php 的构造函数,其完整签名为__construct(string $filename, array $locales = ['en']):
$filename:.mmdb数据库文件路径;文件损坏或无效时抛出\MaxMind\Db\Reader\InvalidDatabaseException;$locales:name属性使用的语言优先级列表,默认['en'],可传['zh-CN']等获取多语言名称。
构造函数内部会解析数据库元数据(databaseType),据此校验后续调用的方法是否与该数据库类型匹配。Reader对象应当被复用——同一实例可跨多次查询,不必每次查询都重建。
之后调用与数据库类型对应的方法(city、country、anonymousIp、asn、connectionType、domain、enterprise、isp)。查询成功返回对应的模型类(Model),模型内部再包含若干记录类(Record),例如City模型聚合了city、location、postal、subdivisions等记录;数据库缺失的字段,其属性值为null。
异常体系(Reader.php 中getRecord()可印证):
| 异常 | 触发条件 |
|---|---|
\GeoIp2\Exception\AddressNotFoundException | 地址不在数据库中(查询结果记录为null) |
\InvalidArgumentException | 传入的 IP 字符串非法 |
\MaxMind\Db\Reader\InvalidDatabaseException | 数据库损坏/无效,或查询结果非数组 |
\BadMethodCallException | 方法与数据库类型不匹配,如对 Country 库调用->city() |
4.2 City 示例(完整可运行)
<?php require_once 'vendor/autoload.php'; use GeoIp2\Database\Reader; // 创建 Reader 对象,应在多次查询间复用 $reader = new Reader('/usr/local/share/GeoIP/GeoIP2-City.mmdb'); // 将 "city" 替换为与你数据库匹配的方法,例如 "country" $record = $reader->city('128.101.101.101'); print($record->country->isoCode . "\n"); // 'US' print($record->country->name . "\n"); // 'United States' print($record->country->names['zh-CN'] . "\n"); // '美国' print($record->mostSpecificSubdivision->name . "\n"); // 'Minnesota' print($record->mostSpecificSubdivision->isoCode . "\n"); // 'MN' print($record->city->name . "\n"); // 'Minneapolis' print($record->postal->code . "\n"); // '55455' print($record->location->latitude . "\n"); // 44.9733 print($record->location->longitude . "\n"); // -93.2323 print($record->traits->network . "\n"); // '128.101.101.101/32'值得注意的实现细节:
$record->mostSpecificSubdivision在 City 模型 中总是返回一个对象——即便响应里没有任何 subdivision,也会返回空的Subdivision对象,避免调用方做空指针判断;$record->traits->network并非数据库原始字段,而是由 Util::cidr() 依据ip_address与prefix_len计算出的 CIDR 网络表示(见 Traits.php)。
4.3 Anonymous IP 示例
<?php require_once 'vendor/autoload.php'; use GeoIp2\Database\Reader; $reader = new Reader('/usr/local/share/GeoIP/GeoIP2-Anonymous-IP.mmdb'); $record = $reader->anonymousIp('128.101.101.101'); if ($record->isAnonymous) { print "anon\n"; } print($record->ipAddress . "\n"); // '128.101.101.101' print($record->network . "\n"); // '128.101.101.101/32'4.4 Connection-Type 示例
<?php require_once 'vendor/autoload.php'; use GeoIp2\Database\Reader; $reader = new Reader('/usr/local/share/GeoIP/GeoIP2-Connection-Type.mmdb'); $record = $reader->connectionType('128.101.101.101'); print($record->connectionType . "\n"); // 'Corporate' print($record->ipAddress . "\n"); // '128.101.101.101' print($record->network . "\n"); // '128.101.101.101/32'4.5 Domain 示例
<?php require_once 'vendor/autoload.php'; use GeoIp2\Database\Reader; $reader = new Reader('/usr/local/share/GeoIP/GeoIP2-Domain.mmdb'); $record = $reader->domain('128.101.101.101'); print($record->domain . "\n"); // 'umn.edu' print($record->ipAddress . "\n"); // '128.101.101.101' print($record->network . "\n"); // '128.101.101.101/32'4.6 Enterprise 示例
Enterprise 数据库是 GeoIP2 商业数据集的集大成者,额外提供各字段的置信度(confidence,取值 0–100)与定位精度半径:
<?php require_once 'vendor/autoload.php'; use GeoIp2\Database\Reader; $reader = new Reader('/usr/local/share/GeoIP/GeoIP2-Enterprise.mmdb'); // 使用 ->enterprise 方法查询 Enterprise 数据库 $record = $reader->enterprise('128.101.101.101'); print($record->country->confidence . "\n"); // 99 print($record->country->isoCode . "\n"); // 'US' print($record->country->name . "\n"); // 'United States' print($record->country->names['zh-CN'] . "\n"); // '美国' print($record->mostSpecificSubdivision->confidence . "\n"); // 77 print($record->mostSpecificSubdivision->name . "\n"); // 'Minnesota' print($record->mostSpecificSubdivision->isoCode . "\n"); // 'MN' print($record->city->confidence . "\n"); // 60 print($record->city->name . "\n"); // 'Minneapolis' print($record->postal->code . "\n"); // '55455' print($record->location->accuracyRadius . "\n"); // 50 print($record->location->latitude . "\n"); // 44.9733 print($record->location->longitude . "\n"); // -93.2323 print($record->traits->network . "\n"); // '128.101.101.101/32'4.7 ISP 示例
<?php require_once 'vendor/autoload.php'; use GeoIp2\Database\Reader; $reader = new Reader('/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'); $record = $reader->isp('128.101.101.101'); print($record->autonomousSystemNumber . "\n"); // 217 print($record->autonomousSystemOrganization . "\n"); // 'University of Minnesota' print($record->isp . "\n"); // 'University of Minnesota' print($record->organization . "\n"); // 'University of Minnesota' print($record->ipAddress . "\n"); // '128.101.101.101' print($record->network . "\n"); // '128.101.101.101/32'4.8 模型与记录类的内部组织
从 src 目录结构 可梳理出「模型—记录」两层结构:
- Model(Model/):
City、Country、Enterprise、Insights、AnonymousIp、Asn、ConnectionType、Domain、Isp,对应各数据库/端点返回的整体数据; - Record(Record/):
City、Continent、Country、Location、Postal、RepresentedCountry、Subdivision、Traits、MaxMind,是模型内各数据分片。
两个可复用的实现机制值得注意:
- AbstractModel 实现了
\JsonSerializable,jsonSerialize()直接返回原始数组,便于把查询结果序列化输出;其__get()在访问未知属性时会抛出RuntimeException("Unknown attribute: ..."),有助于及早发现字段拼写错误。 Traits记录中所有is_*布尔属性(如isAnonymous、isTorExitNode)在数据缺失时默认返回false而非null(AbstractModel.php 的get()方法实现),避免布尔语义出现三态歧义。
五、数据库更新
.mmdb是离线快照,需要定期更新以保持准确性。官方提供:
- GeoIP Update 程序:官方推荐的数据更新工具,可从其发布页获取;
- 第三方 geoip2-update 工具:基于 PHP 与 Composer 的社区方案,MaxMind 官方不提供支持也不维护。
在 YOURLS 仓库中,数据库更新方式记录于 includes/geo/README.md:在 maxmind.com 注册账号后,将新版GeoLite2-Country.mmdb替换 includes/geo/GeoLite2-Country.mmdb 即可;该文件附带 MaxMind 的 GeoLite2 EULA 与 GeoNames(CC BY 4.0)地理数据许可说明。
六、Web Service Client(Web 服务客户端)
6.1 用法与构造参数
创建\GeoIp2\WebService\Client对象需提供账号 ID 与许可证密钥:
$client = new Client(42, 'abcdef123456');查看 Client.php,完整签名为__construct(int $accountId, string $licenseKey, array $locales = ['en'], array $options = []):
- 第 3 个参数
$locales:模型类->name方法的语言偏好列表; - 第 4 个参数
$options支持的选项(与 README 一致并可溯源到源码):
| 选项 | 说明 | 默认值 |
|---|---|---|
host | 查询的主机;设为geolite.info可改用 GeoLite2 Web 服务 | geoip.maxmind.com |
timeout | 请求超时(秒) | — |
connectTimeout | 建立连接的超时(秒) | — |
proxy | HTTP 代理,可含 schema、端口、用户名与密码,如http://username:password@127.0.0.1:10 | — |
例如切换到 GeoLite2 Web 服务:
$client = new Client(42, 'abcdef123456', ['en'], ['host' => 'geolite.info']);源码细节:为兼容旧版,若第 4 个参数误传字符串,会被自动改写为['host' => $options];客户端固定使用 User-AgentGeoIP2-API/v2.13.0,请求路径前缀为/geoip/v2.1。
创建后调用与端点对应的方法并传入 IP(缺省为'me',即用调用方自身的外网 IP):
$record = $client->city('128.101.101.101');端点与模型对应:country(Country 服务,数据最少)、city(City Plus 服务)、insights(Insights 服务,数据最全,仅 GeoIP2 支持、GeoLite2 不支持)。
6.2 异常映射
responseFor() 把底层 MaxMind WebService 异常统一翻译为 GeoIP2 语义的结构化异常:
| 异常 | 语义 |
|---|---|
AddressNotFoundException | 地址不在数据库中(如私网地址) |
AuthenticationException | 账号 ID 或许可证密钥有问题 |
OutOfQueriesException | 账号查询额度耗尽 |
InvalidRequestException | 请求被服务端接收但无效 |
HttpException | 意外 HTTP 错误码或响应(如 500、连接故障) |
GeoIp2Exception | 以上异常的父类;200 但响应体非法时直接抛出 |
6.3 完整示例
<?php require_once 'vendor/autoload.php'; use GeoIp2\WebService\Client; // 创建可跨请求复用的 Client 对象。 // 将 "42" 替换为你的账号 ID,"abcdef123456" 替换为许可证密钥; // 若使用 GeoLite2 Web 服务,在第 4 个参数 options 数组中设置 "host" => "geolite.info"。 $client = new Client(42, 'abcdef123456'); // 将 "city" 替换为你所用 Web 服务对应的方法,例如 "country"、"insights" $record = $client->city('128.101.101.101'); print($record->country->isoCode . "\n"); // 'US' print($record->country->name . "\n"); // 'United States' print($record->country->names['zh-CN'] . "\n"); // '美国' print($record->mostSpecificSubdivision->name . "\n"); // 'Minnesota' print($record->mostSpecificSubdivision->isoCode . "\n"); // 'MN' print($record->city->name . "\n"); // 'Minneapolis' print($record->postal->code . "\n"); // '55455' print($record->location->latitude . "\n"); // 44.9733 print($record->location->longitude . "\n"); // -93.2323 print($record->traits->network . "\n"); // '128.101.101.101/32'七、数据库或数组键的选择建议
文档给出一个重要警告:强烈不建议把任何names属性(多语言名称)的值用作数据库或数组的键,因为这些名称会随版本发布变化。推荐使用以下稳定标识:
| 记录类 | 推荐键 |
|---|---|
GeoIp2\Record\City | $city->geonameId |
GeoIp2\Record\Continent | $continent->code或$continent->geonameId |
GeoIp2\Record\Country/RepresentedCountry | $country->isoCode或$country->geonameId |
GeoIp2\Record\Subdivision | $subdivision->isoCode或$subdivision->geonameId |
该建议在 YOURLS 中有直接对应:其国家代码映射表(functions-geo.php)正是以稳定的两位isoCode(如AU、FR)为键存储国家长名,而非依赖易变的本地化名称。
八、返回数据的完整性说明
各端点返回的基础记录大体相同,但可填充的属性随端点而异;同时,即便某个端点声称提供某类数据,MaxMind 也并非对每个 IP 都有该数据。因此任何端点都可能返回部分或全部属性为空(值为null)的记录。唯一始终返回的数据是GeoIp2\Record\Traits记录中的ipAddress属性。调用方设计字段展示逻辑时,应默认这些字段可能缺失。
九、GeoNames 集成
GeoNames 提供覆盖全球地理要素(含居民点)的 Web 服务与可下载数据库(免费与付费高级数据并存),每个要素以整数geonameId唯一标识。GeoIP2 的 Web 服务与数据库返回的众多记录都携带geonameId属性,指向 GeoNames 数据库中的对应地理要素(城市、地区、国家等)。MaxMind 的部分数据(地名、ISO 代码等)本身也来源于 GeoNames 的高级数据集。
十、数据问题上报
- IP 映射错误(某 IP 被标到错误位置):向 MaxMind 提交更正;
- 拼写等一般错误:先到 GeoNames 核查,其地图视图提供 "move"、"edit"、"alternate names" 等更正入口;GeoNames 数据修正后会自动进入后续 MaxMind 版本;
- 付费客户不确定提交渠道:联系 MaxMind 支持。
十一、在 YOURLS 中的实际集成剖析
GeoIP2 包在 YOURLS 中的核心消费点是 includes/functions-geo.php,整个链路「IP → 国家代码 → 国旗」可直接对应上文各章节:
11.1 IP 转国家代码
yourls_geo_ip_to_countrycode()(functions-geo.php)是数据库读取的实战范例:
- 先检查 Cloudflare 请求头
HTTP_CF_IPCOUNTRY(可经geo_use_cloudflare过滤器关闭),若命中两位大写字母则直接返回,零数据库开销; - IP 为空时经
yourls_get_IP()取当前用户 IP; - 数据库路径经
geo_ip_path_to_db过滤器可替换,默认指向YOURLS_INC.'/geo/GeoLite2-Country.mmdb'(即 includes/geo/GeoLite2-Country.mmdb);文件不可读时返回默认值; - 核心调用即本节所述模式:
new \GeoIp2\Database\Reader($db)+$reader->country($ip)+$record->country->isoCode; - 用
try/catch (\Exception)兜底全部异常,注释中明确记录了三种典型异常与消息原文,与本文 4.1 节异常表完全对应:AddressNotFoundException:"The address 10.0.0.30 is not in the database"InvalidArgumentException:"The value "10.0.0.300" is not a valid IP address" / "The file ... does not exist or is not readable"InvalidDatabaseException:"The MaxMind DB file's search tree is corrupt"
此外函数前后还挂载了shunt_geo_ip_to_countrycode、geo_ip_to_countrycode等过滤器,便于插件短路或改写定位结果——这正是 README「Web 服务/数据库」两种形态之外,YOURLS 自定义的扩展点设计。
11.2 国家代码转国家名与国旗
yourls_geo_countrycode_to_countryname()(functions-geo.php)内置了 MaxMind 风格的两字母代码→长名映射表(含A1=Anonymous Proxy、A2=Satellite Provider 等特殊项);yourls_geo_get_flag()(functions-geo.php)按代码拼出 includes/geo/flags/ 下的flag_xx.gif资源地址,未命中时回退到空代码的flag_.gif。
11.3 测试印证
GeoIPTest.php 用真实 GeoLite2 数据库验证了整条链路:
- IPv4 样本:
8.8.8.8→US、13.37.13.37→FR、79.79.79.79→GB,而私网地址10.0.0.1与非法串helloworld均回落默认值none; - IPv6 样本:
2001:4860:4860::8888→US(即 8.8.8.8 的 IPv6 形态)、::1→none; - 国家名映射:
AU→Australia、BZ→Belize、CA→Canada,未知代码WW→空串; - 国旗 URL:
AU返回以geo/flags/flag_au.gif结尾的地址,空/非法代码回落flag_.gif。
十二、贡献、版本与许可
- 贡献规范:鼓励提交补丁与 Pull Request,代码遵循 PSR-2 风格;尽量附带单元测试;测试数据可通过
git submodule update --init --recursive(或克隆时加--recursive)获取; - 版本策略:遵循语义化版本(SemVer),升级行为以主版本号变更明示破坏性改动;
- 许可:本软件版权归 MaxMind, Inc.(2013–2020),以 Apache License 2.0 发布;仓库内配套的 GeoLite2 数据库与国旗资源另有各自许可条款,详见 includes/geo/README.md。
小结
GeoIP2 PHP API 以「一套接口、两种数据源」的设计,把本地离线查询与远程 Web 服务统一在Reader/Client两个门面之后,配合分层模型、结构化异常与稳定的geonameId/isoCode标识,极大降低了地理定位功能的接入成本。在 YOURLS 中,它被压缩为「一行Reader+ 一个country()调用」即完成从 IP 到国家代码的转换,再经内置映射与国旗资源快速渲染——这份官方 README 所描述的能力,也正是自托管短链系统统计「访客来自哪里」的基础设施。
- 后端
【免费下载链接】YOURLS
🔗 The 𝘥𝘦 𝘧𝘢𝘤𝘵𝘰 standard, self hosted, powerful and customizable, URL shortener in PHP
相关推荐
如何在Laf云开发中实现强大的地理空间查询:位置服务开发终极指南
如何在Laf云开发中实现强大的地理空间查询:位置服务开发终极指南 Laf云开发平台提供了完整的数据库地理空间查询功能,让开发者能够轻松构建基于位置的服务应用。无
后端Serverless前端云原生GeoIP2 Java API完整指南:快速实现IP地理位置查询
GeoIP2 Java API是一款强大的IP地理位置查询工具,能够帮助开发者快速获取IP地址的地理位置信息。无论是构建网站分析系统、实现内容本地化,还是增强网
后端YOURLS 内置 GeoIP 地理定位:GeoLite2 数据库集成、手动更新与源码原理
YOURLS 内置 GeoIP 地理定位:GeoLite2 数据库集成、手动更新与源码原理 本指南围绕 YOURLS 仓库中的 includes/geo/REA
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考