fp = 0; if (($this->fp = fopen(__DIR__ . '/bin/qqwry.dat', 'rb')) !== false) { $this->firstIp = $this->getLong(); $this->lastIp = $this->getLong(); $this->totalIp = ($this->lastIp - $this->firstIp) / 7; } parent::__construct($app); } /** * 设置未知的返回字段 * @param string $unknown * @return QqWryService */ public function setUnknown(string $unknown = '未知') { $this->unknown = $unknown; return $this; } /** * 获取省信息 * @param string $ip * @return mixed * @throws DtaException */ public function getProvince(string $ip = '') { return $this->getLocation($ip)['state']; } /** * 获取城市信息 * @param string $ip * @return mixed * @throws DtaException */ public function getCity(string $ip = '') { return $this->getLocation($ip)['city']; } /** * 获取地区信息 * @param string $ip * @return mixed * @throws DtaException */ public function getArea(string $ip = '') { return $this->getLocation($ip)['area']; } /** * 获取运营商信息 * @param string $ip * @return mixed * @throws DtaException */ public function getExtend(string $ip = '') { return $this->getLocation($ip)['extend']; } /** * 根据所给 IP 地址或域名返回所在地区信息 * @param string $ip * @return mixed|null * @throws DtaException */ public function getLocation(string $ip = '') { if (empty($ip)) { $ip = get_ip(); } if (strpos($ip, 'http://') === 0) { $ip = substr($ip, 7); $ip = gethostbyname($ip); } static $locationData = []; if (!isset($locationData[$ip])) { if (!$this->fp) { // 如果数据文件没有被正确打开,则直接返回错误 throw new DtaException('数据库文件不存在!'); } $location['ip'] = $ip; // 将输入的域名转化为IP地址 $ip = $this->packIp($location['ip']); // 将输入的IP地址转化为可比较的IP地址 // 不合法的IP地址会被转化为255.255.255.255 // 对分搜索 $l = 0; // 搜索的下边界 $u = $this->totalIp; // 搜索的上边界 $findip = $this->lastIp; // 如果没有找到就返回最后一条IP记录(QQWry.Dat的版本信息) while ($l <= $u) { // 当上边界小于下边界时,查找失败 $i = floor(($l + $u) / 2); // 计算近似中间记录 fseek($this->fp, $this->firstIp + $i * 7); $beginip = strrev(fread($this->fp, 4)); // 获取中间记录的开始IP地址 // strrev函数在这里的作用是将little-endian的压缩IP地址转化为big-endian的格式 // 以便用于比较,后面相同。 if ($ip < $beginip) { // 用户的IP小于中间记录的开始IP地址时 $u = $i - 1; // 将搜索的上边界修改为中间记录减一 } else { fseek($this->fp, $this->getLong3()); $endip = strrev(fread($this->fp, 4)); // 获取中间记录的结束IP地址 if ($ip > $endip) { // 用户的IP大于中间记录的结束IP地址时 $l = $i + 1; // 将搜索的下边界修改为中间记录加一 } else { // 用户的IP在中间记录的IP范围内时 $findip = $this->firstIp + $i * 7; break; // 则表示找到结果,退出循环 } } } //获取查找到的IP地理位置信息 fseek($this->fp, $findip); $location['beginip'] = long2ip($this->getLong()); // 用户IP所在范围的开始地址 $offset = $this->getLong3(); fseek($this->fp, $offset); $location['endip'] = long2ip($this->getLong()); // 用户IP所在范围的结束地址 $byte = fread($this->fp, 1); // 标志字节 switch (ord($byte)) { case 1: // 标志字节为1,表示国家和区域信息都被同时重定向 $countryOffset = $this->getLong3(); // 重定向地址 fseek($this->fp, $countryOffset); $byte = fread($this->fp, 1); // 标志字节 switch (ord($byte)) { case 2: // 标志字节为2,表示国家信息又被重定向 fseek($this->fp, $this->getLong3()); $location['all'] = $this->getString(); fseek($this->fp, $countryOffset + 4); $location['extend'] = $this->getExtendString(); break; default: // 否则,表示国家信息没有被重定向 $location['all'] = $this->getString($byte); $location['extend'] = $this->getExtendString(); break; } break; case 2: // 标志字节为2,表示国家信息被重定向 fseek($this->fp, $this->getLong3()); $location['all'] = $this->getString(); fseek($this->fp, $offset + 8); $location['extend'] = $this->getExtendString(); break; default: // 否则,表示国家信息没有被重定向 $location['all'] = $this->getString($byte); $location['extend'] = $this->getExtendString(); break; } // CZ88.NET表示没有有效信息 if (trim($location['all']) === 'CZ88.NET') { $location['all'] = $this->unknown; } if (trim($location['extend']) === 'CZ88.NET') { $location['extend'] = ''; } $location['all'] = iconv("gb2312", "UTF-8//IGNORE", $location['all']); $location['extend'] = iconv("gb2312", "UTF-8//IGNORE", $location['extend']); $location['extend'] = $location['extend'] ?? ''; $parseData = $this->parseLocation($location['all']); $location['state'] = $parseData[0]; $location['city'] = $parseData[1]; $location['area'] = $parseData[2]; // 全部地址 $res['location_all'] = $location['all']; // 运营商 $res['isp']['name'] = $location['extend']; // IP $res['ip']['ipv4'] = $location['ip']; $res['ip']['beginip'] = $location['beginip']; $res['ip']['endip'] = $location['endip']; $res['ip']['trueip'] = ip2long($location['ip']); $res['ip']['ipv6'] = $this->getNormalizedIP($location['ip']); $getAdCodeLatLng = $this->getNameAdCodeLatLng($location['state'], $location['city'], $location['area']); // 省份 $res['province'] = $getAdCodeLatLng['province']; // 城市 $res['city'] = $getAdCodeLatLng['city']; // 地区 $res['district'] = $getAdCodeLatLng['district']; $locationData[$ip] = $location; } return $res; } /** * 解析省市区县 * @param $location * @return array * @example '江苏省苏州市吴江市' , '江苏省苏州市吴中区' , '江苏省苏州市昆山市' , '黑龙江省鸡西市' , '广西桂林市' , '陕西省西安市户县' , '河南省开封市通许县' ,'内蒙古呼伦贝尔市海拉尔区','甘肃省白银市平川区','孟加拉','上海市' , '北京市朝阳区' ,'美国' ,'香港' , 俄罗斯' ,'IANA' */ private function parseLocation($location) { $state = $city = $area = $this->unknown; if (preg_match('/^(.+省)?(新疆|内蒙古|宁夏|西藏|广西|香港|澳门)?(.+市)?(.+市)?(.+(县|区))?/', $location, $preg)) { if (count($preg) == 4) { //匹配 "浙江省杭州市" $state = $preg[1] ? $preg[1] : ($preg[2] ? $preg[2] : $preg[3]); $city = $preg[3]; } else if (count($preg) == 7) { //匹配 "浙江省杭州市江干区" $state = $preg[1] ? $preg[1] : ($preg[2] ? $preg[2] : $preg[3]); $city = $preg[3]; $area = $preg[5]; } else if (count($preg) == 3) { //匹配 "香港" $state = $preg[1] ? $preg[1] : $preg[2]; $city = $state; } else if (count($preg) == 2) { //匹配 "浙江省" $state = $preg[1] ? $preg[1] : $this->unknown; } } return [$state, $city, $area]; } /** * 返回读取的长整型数 * @return mixed */ private function getLong() { //将读取的little-endian编码的4个字节转化为长整型数 $result = unpack('Vlong', fread($this->fp, 4)); return $result['long']; } /** * 返回读取的3个字节的长整型数 * @return mixed */ private function getLong3() { //将读取的little-endian编码的3个字节转化为长整型数 $result = unpack('Vlong', fread($this->fp, 3) . chr(0)); return $result['long']; } /** * 返回压缩后可进行比较的IP地址 * @param $ip * @return false|string */ private function packIp($ip) { // 将IP地址转化为长整型数,如果在PHP5中,IP地址错误,则返回False, // 这时intval将Flase转化为整数-1,之后压缩成big-endian编码的字符串 return pack('N', intval(ip2long($ip))); } /** * 返回读取的字符串 * * @access private * @param string $data * @return string */ private function getString($data = "") { $char = fread($this->fp, 1); while (ord($char) > 0) { // 字符串按照C格式保存,以\0结束 $data .= $char; // 将读取的字符连接到给定字符串之后 $char = fread($this->fp, 1); } return $data; } /** * 返回地区信息 * @return string */ private function getExtendString() { $byte = fread($this->fp, 1); // 标志字节 switch (ord($byte)) { case 0: // 没有区域信息 $area = ""; break; case 1: case 2: // 标志字节为1或2,表示区域信息被重定向 fseek($this->fp, $this->getLong3()); $area = $this->getString(); break; default: // 否则,表示区域信息没有被重定向 $area = $this->getString($byte); break; } return $area; } /** * 析构函数,用于在页面执行结束后自动关闭打开的文件。 */ public function __destruct() { if ($this->fp) { fclose($this->fp); } $this->fp = 0; } /** * ipv4转换ipv6 * @param $ip * @return bool|false|string|string[]|null */ protected function getNormalizedIP($ip) { if (!is_string($ip)) { return ''; } if (preg_match('%^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$%', $ip, $match)) { $IPParts = array(); for ($i = 1; $i <= 4; $i++) { $IPPart = (int)$match[$i]; if ($IPPart > 255) { return ''; } $IPParts[$i] = str_pad(decHex($IPPart), 2, '0', STR_PAD_LEFT); } return '0000:0000:0000:0000:0000:ffff:' . $IPParts[1] . $IPParts[2] . ':' . $IPParts[3] . $IPParts[4]; } return ''; } /** * 解析CODE * @param $province_name * @param $city_name * @param $district_name * @return array */ private function getNameAdCodeLatLng($province_name, $city_name, $district_name) { // 名称 $province['name'] = $province_name; $city['name'] = $city_name; $district['name'] = $district_name; // adcode $province['adcode'] = ''; $city['adcode'] = ''; $district['adcode'] = ''; // lat $province['lat'] = ''; $city['lat'] = ''; $district['lat'] = ''; // lng $province['lng'] = ''; $city['lng'] = ''; $district['lng'] = ''; if (!empty($province_name)) { $json_province = json_decode(file_get_contents(__DIR__ . '/bin/province.json'), true); foreach ($json_province['rows'] as $key => $value) { if ($value['name'] == $province_name) { $province['name'] = $value['name']; $province['adcode'] = $value['adcode']; $province['lat'] = $value['lat']; $province['lng'] = $value['lng']; } } } if (!empty($city_name)) { $json_city = json_decode(file_get_contents(__DIR__ . '/bin/city.json'), true); foreach ($json_city['rows'] as $key => $value) { if ($value['name'] == $city_name) { $city['name'] = $value['name']; $city['adcode'] = $value['adcode']; $city['lat'] = $value['lat']; $city['lng'] = $value['lng']; } } } if (!empty($district_name)) { $json_district = json_decode(file_get_contents(__DIR__ . '/bin/district.json'), true); foreach ($json_district['rows'] as $key => $value) { if ($value['name'] == $district_name) { $district['name'] = $value['name']; $district['adcode'] = $value['adcode']; $district['lat'] = $value['lat']; $district['lng'] = $value['lng']; } } } return [ 'province' => $province, 'city' => $city, 'district' => $district ]; } }