獲取用戶 IP 歸屬地是個性化服務、風控分析、日誌審計的基礎需求。站長之家提供免費的 IP 查詢介面,本文介紹如何用 PHP 爬取或呼叫其 API,解析歸屬地資訊,並提供快取、異常處理、多源備援的生產級封裝。
站長之家 IP 查詢頁面:https://tool.chinaz.com/ip/?ip=8.8.8.8
ip (可選,預設為訪問者 IP)<?php
function getIpLocationChinaz(string $ip = ''): ?array {
$url = 'https://tool.chinaz.com/ip/?ip=' . urlencode($ip ?: getClientIp());
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; IPLocationBot/1.0)',
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => false, // HTTP 不需驗證
]);
$html = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || !$html) return null;
// 解析 HTML:尋找 "IP歸屬地: XX省XX市" 等資訊
// 頁面結構可能變動,以下為常見模式
$patterns = [
'location' => '/IP歸屬地[::]\s*([^<\n]+)/i',
'isp' => '/運營商[::]\s*([^<\n]+)/i',
'area' => '/地區[::]\s*([^<\n]+)/i',
];
$result = ['ip' => $ip];
foreach ($patterns as $key => $pattern) {
if (preg_match($pattern, $html, $m)) {
$result[$key] = trim($m[1]);
}
}
// 更穩健:針對具體 DOM 結構解析 (需引入 DOMDocument)
// $dom = new DOMDocument(); @$dom->loadHTML($html); $xpath = new DOMXPath($dom);
// $result['location'] = $xpath->evaluate('string(//div[@class="result"]/p[1])');
return $result ?: null;
}
function getClientIp(): string {
foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'] as $key) {
if (!empty($_SERVER[$key])) {
$ips = explode(',', $_SERVER[$key]);
return trim($ips[0]);
}
}
return 'unknown';
}
<?php
class IpLocationService {
private $redis;
private $ttl = 86400; // 24 小時快取
private $sources = [
'chinaz' => 'https://tool.chinaz.com/ip/?ip={ip}',
'ipapi' => 'https://ip-api.com/json/{ip}?lang=zh-CN', // 免費、JSON、支援 HTTPS
'ipinfo' => 'https://ipinfo.io/{ip}/json', // 需 Token、精度高
'taobao' => 'https://ip.taobao.com/outGetIpInfo?ip={ip}', // 淘寶 IP 庫
];
public function __construct(\Redis $redis = null) {
$this->redis = $redis;
}
public function getLocation(string $ip = ''): array {
$ip = $ip ?: $this->getClientIp();
$cacheKey = "ip:location:$ip";
// 1. 嘗試快取
if ($this->redis && ($cached = $this->redis->get($cacheKey))) {
return json_decode($cached, true) + ['cached' => true];
}
// 2. 依序嘗試資料源
foreach ($this->sources as $name => $urlTemplate) {
try {
$url = str_replace('{ip}', urlencode($ip), $urlTemplate);
$result = $this->fetchAndParse($name, $url);
if ($result) {
$result['source'] = $name;
$result['ip'] = $ip;
// 3. 寫入快取
if ($this->redis) {
$this->redis->setex($cacheKey, $this->ttl, json_encode($result, JSON_UNESCAPED_UNICODE));
}
return $result;
}
} catch (Throwable $e) {
error_log("IP location source '$name' failed for $ip: " . $e->getMessage());
continue; // 嘗試下一個源
}
}
return ['ip' => $ip, 'error' => 'All sources failed'];
}
private function fetchAndParse(string $source, string $url): ?array {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; IPLocationService/1.0)',
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || !$response) return null;
switch ($source) {
case 'chinaz':
return $this->parseChinazHtml($response);
case 'ipapi':
case 'ipinfo':
$data = json_decode($response, true);
return $data ? $this->normalizeIpapi($data) : null;
case 'taobao':
$data = json_decode($response, true);
return ($data['code'] ?? 1) === 0 ? $this->normalizeTaobao($data['data']) : null;
}
return null;
}
private function parseChinazHtml(string $html): ?array {
// 使用 DOMDocument 更穩健
$dom = new DOMDocument();
libxml_use_internal_errors(true);
$dom->loadHTML('' . $html);
libxml_clear_errors();
$xpath = new DOMXPath($dom);
// 根據實際頁面結構調整 XPath
$location = $xpath->evaluate('string(//div[contains(@class,"result")]//text()[contains(.,"IP歸屬地")])');
$isp = $xpath->evaluate('string(//div[contains(@class,"result")]//text()[contains(.,"運營商")])');
return [
'country' => '中國',
'region' => trim(str_replace('IP歸屬地:', '', $location)),
'isp' => trim(str_replace('運營商:', '', $isp)),
];
}
private function normalizeIpapi(array $data): array {
return [
'country' => $data['country'] ?? '',
'region' => $data['regionName'] ?? '',
'city' => $data['city'] ?? '',
'isp' => $data['isp'] ?? '',
'lat' => $data['lat'] ?? null,
'lon' => $data['lon'] ?? null,
];
}
private function normalizeTaobao(array $data): array {
return [
'country' => $data['country'] ?? '',
'region' => $data['region'] ?? '',
'city' => $data['city'] ?? '',
'isp' => $data['isp'] ?? '',
'area' => $data['area'] ?? '',
];
}
private function getClientIp(): string { /* 同前 */ }
}
<?php
require 'vendor/autoload.php';
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$service = new IpLocationService($redis);
// 同步查詢
$loc = $service->getLocation('8.8.8.8');
print_r($loc);
/*
Array
(
[ip] => 8.8.8.8
[country] => 美國
[region] => 加利福尼亞州
[city] => 山景城
[isp] => Google LLC
[source] => ipapi
[cached] =>
)
*/
// 批量查詢 (非同步佇列)
$ips = ['1.1.1.1', '8.8.8.8', '114.114.114.114'];
foreach ($ips as $ip) {
// 推送到 Redis 佇列,Worker 背景處理
$redis->lPush('queue:ip_location', $ip);
}
| 資料源 | 格式 | HTTPS | 免費額度 | 精度(國內) | 精度(國際) | 推薦場景 |
|---|---|---|---|---|---|---|
| 站長之家 | HTML | 否 | 未限制 | 高 | 中 | 國內項目、無預算 |
| ip-api.com | JSON | 是 | 45 req/min | 中 | 高 | 中小專案、快速接入 |
| ipinfo.io | JSON | 是 | 50k req/mo (免費) | 高 | 高 | 生產環境、需精度 |
| 淘寶 IP | JSON | 是 | 內部/限制 | 極高 | 低 | 阿里系、國內電商 |
| MaxMind GeoIP2 | MMDB | 本地 | 付費/免費版 | 高 | 高 | 高效能、離線、大流量 |
<?php
// composer require geoip2/geoip2:~2.0
use GeoIp2\Database\Reader;
$reader = new Reader('/usr/share/GeoIP/GeoLite2-City.mmdb');
$record = $reader->city('8.8.8.8');
echo $record->country->isoCode; // US
echo $record->country->names['zh-CN']; // 美國
echo $record->mostSpecificSubdivision->names['zh-CN']; // 加利福尼亞州
echo $record->city->names['zh-CN']; // 山景城
echo $record->traits->isp; // Google LLC
echo $record->traits->organization; // Google LLC
根據專案規模、預算、精度需求選擇適合方案:中小專案用 ip-api.com + Redis 快取;大型生產環境建議部署 MaxMind GeoIP2 本地庫,毫秒級響應、無外部依賴、資料可控。