$special, 'province' => '', 'city' => '', 'label' => $special, 'found' => true]; } // 2) 中国大陆号码:+86/0086 开头 或 11位以 1 开头 if (isChinaMobile($digits, $hasPlus)) { $geo = lookupChinaPhoneDat($digits); if ($geo !== null) { return [ 'country' => '中国', 'province' => $geo['province'], 'city' => $geo['city'], 'label' => '中国-' . $geo['province'] . ($geo['city'] && $geo['city'] !== $geo['province'] ? '-' . $geo['city'] : ''), 'found' => true, ]; } return ['country' => '中国', 'province' => '', 'city' => '', 'label' => '中国', 'found' => false]; } // 3) 海外号码:识别国籍(国家区号表) $country = lookupCountryByCode($digits); if ($country !== null) { return ['country' => $country, 'province' => '', 'city' => '', 'label' => $country, 'found' => true]; } return ['country' => '', 'province' => '', 'city' => '', 'label' => '未知', 'found' => false]; } /** 港澳台特殊识别 */ function specialRegion($digits) { // 852 香港 / 853 澳门 / 886 台湾(号码 8-9 位) if (strpos($digits, '852') === 0 && strlen($digits) >= 11) return '中国香港'; if (strpos($digits, '853') === 0 && strlen($digits) >= 11) return '中国澳门'; if (strpos($digits, '886') === 0 && strlen($digits) >= 12) return '中国台湾'; return null; } /** 是否中国大陆手机号 */ function isChinaMobile($digits, $hasPlus) { // +86 或 0086 开头(去除前缀后为 11 位 1 开头) if (strpos($digits, '86') === 0 && strlen($digits) === 13) { return preg_match('/^861[3-9]\d{9}$/', $digits) === 1; } // 纯 11 位 1 开头(无国际前缀) if (!$hasPlus && strlen($digits) === 11 && $digits[0] === '1') { return preg_match('/^1[3-9]\d{9}$/', $digits) === 1; } return false; } /** 使用 phone.dat 查询国内省市 */ function lookupChinaPhoneDat($digits) { static $loader = null; if ($loader === null) { $loader = new PhoneDatLoader(__DIR__ . '/../../data/phone.dat'); } // 提取 11 位号码中的前 7 位号段(如 1381234);带 86 前缀先去前缀 $local = preg_replace('/^86/', '', $digits); $prefix7 = substr($local, 0, 7); if (strlen($prefix7) !== 7 || !preg_match('/^\d{7}$/', $prefix7)) { return null; } return $loader->find($prefix7); } /** 海外号码:国家区号表识别国籍 */ function lookupCountryByCode($digits) { static $map = null; if ($map === null) { $map = require __DIR__ . '/country_codes.php'; } // 依次尝试 3 位 / 2 位 / 1 位区号 for ($len = 3; $len >= 1; $len--) { $code = substr($digits, 0, $len); if (isset($map[$code])) { return $map[$code]; } } return null; } /* ==================== phone.dat 解析器(PHP 移植) ==================== */ /** * phone.dat 文件解析器 * 文件格式(与 Python phone 库一致): * - 头部:4字节版本号 + 4字节首条记录偏移 * - 记录:每条 9 字节 = 4字节号段(int) + 4字节数据偏移(int) + 1字节号码类型 * - 数据区:省份|城市|邮编|区号 (竖线分隔,\0 结尾) */ class PhoneDatLoader { private $buf; private $firstOffset; private $count; public function __construct($file) { $this->buf = file_get_contents($file); if ($this->buf === false || strlen($this->buf) < 8) { $this->buf = ''; $this->firstOffset = 0; $this->count = 0; return; } $head = unpack('a4version/ioffset', substr($this->buf, 0, 8)); $this->firstOffset = $head['offset']; $this->count = (int)((strlen($this->buf) - $this->firstOffset) / 9); } /** * 按 7 位号段查询 * @param string $prefix7 如 "1381234" * @return array|null ['province'=>, 'city'=>, 'zip_code'=>, 'area_code'=>, 'phone_type'=>] */ public function find($prefix7) { if ($this->count <= 0 || !preg_match('/^\d{7}$/', (string)$prefix7)) { return null; } $intPhone = (int)$prefix7; $left = 0; $right = $this->count; $buflen = strlen($this->buf); while ($left <= $right) { $middle = (int)(($left + $right) / 2); $offset = $this->firstOffset + $middle * 9; if ($offset >= $buflen) { return null; } $rec = unpack('iprefix/irecord_offset/Ctype', substr($this->buf, $offset, 9)); if ($rec['prefix'] > $intPhone) { $right = $middle - 1; } elseif ($rec['prefix'] < $intPhone) { $left = $middle + 1; } else { $content = $this->readRecord($rec['record_offset']); if ($content === null) { return null; } $parts = explode('|', $content); return [ 'province' => $parts[0] ?? '', 'city' => $parts[1] ?? '', 'zip_code' => $parts[2] ?? '', 'area_code' => $parts[3] ?? '', 'phone_type' => $this->typeName($rec['type']), ]; } } return null; } private function readRecord($offset) { $end = strpos($this->buf, "\x00", $offset); if ($end === false) { return null; } return substr($this->buf, $offset, $end - $offset); } private function typeName($no) { $map = [ 1 => '移动', 2 => '联通', 3 => '电信', 4 => '电信虚拟运营商', 5 => '联通虚拟运营商', 6 => '移动虚拟运营商', 7 => '广电', 8 => '广电虚拟运营商', ]; return $map[$no] ?? '未知'; } }