├── .github └── workflows │ └── php.yml ├── .gitignore ├── LICENSE ├── README.MD ├── UPDATE.MD ├── bin ├── update-ip.php └── update-region-code.php ├── composer.json ├── doc ├── README.md ├── introduction-ipdb.txt ├── introduction-qqwry.txt └── qqwry.pdf ├── src ├── IpLocation.php ├── IpParser │ ├── IpParserInterface.php │ ├── IpV6wry.php │ └── QQwry.php ├── StringParser.php └── libs │ ├── ipv6wry.db │ └── qqwry.dat ├── tests └── ip.php └── tmp └── mca_region_code.html /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Composer 2 | 3 | on: 4 | push: 5 | branches: [ 2.x, 3.x ] 6 | pull_request: 7 | branches: [ 2.x, 3.x] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Validate composer.json and composer.lock 18 | run: composer validate --strict 19 | 20 | - name: Cache Composer packages 21 | id: composer-cache 22 | uses: actions/cache@v2 23 | with: 24 | path: vendor 25 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 26 | restore-keys: | 27 | ${{ runner.os }}-php- 28 | 29 | - name: Install dependencies 30 | run: composer install --prefer-dist --no-progress 31 | 32 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" 33 | # Docs: https://getcomposer.org/doc/articles/scripts.md 34 | 35 | # - name: Run test suite 36 | # run: composer run-script test 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .DS_Store 3 | vendor 4 | composer.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | ============================================================ 22 | 23 | 本协议是用户(您)和ZX公司(zxinc.org)之间关于使用ZX IP地址数据库(本数据库)达成的协议。您安装或者使用本数据库的行为将视为对本协的接受及同意。除非您接受本协议,否则请勿下载、安装或使用本数据库,并请将本数据库从计算机中移除。 24 | 25 | 1. 本数据库是免费许可软件,不进行出售。你可以免费的复制,分发和传播本数据库,但您必须保证每一份复制、分发和传播都必须是未更改过的,完整和真实的。 26 | 2. 您作为个人使用本数据库。您只能对本数据库进行非商业性的应用。 27 | 3. 任何免费软件以及非商业性网站均可无偿使用本数据库,但在其说明上均应注明本数据库的名称和来源为“ZX IP地址数据库”。 28 | 4. 本数据库为免费共享软件。我们对本数据库产品不提供任何保证,不对任何用户因本数据库所遭遇到的任何理论上的或实际上的损失承担责任,不对用户使用本数据库造成的任何后果承担责任。 29 | 5. 本数据库所收集的信息,均是从网上收集而来。数据库只包含IP与其对应的地址,但是这些数据不会涉及您的个人信息,因此也不会侵害您的隐私。 30 | 6. 欢迎任何人为我们提供正确详尽的IP地址。可登录网站(http://ip.zxinc.org)或论坛(http://bbs.zxinc.org)提交正确的IP与地址,以便我们修正并提高本数据库IP地址数据的准确性。 31 | 32 | ZX公司(zxinc.org)版权所有,保留一切解释权利 ! 33 | 34 | 35 | ============================================================ 36 | 37 | QQ IP数据库 纯真版 38 | 39 | IP数据记录:527250条 40 | 数据库大小:9M 41 | 42 | 收集了包括中国电信、中国移动、中国联通、长城宽带、聚友宽带等 ISP 的 IP 地址数据,包括网吧数据。 43 | 希望能够通过大家的共同努力打造一个没有未知数据,没有错误数据的QQ IP。IP数据库每5天更新一次,请大家 44 | 定期更新IP数据库! 45 | 因为IP地址数据是民间收集的,各运营商也会不时的更改IP段,所以有点遗漏、错误是难免的。随数据库附 46 | 送IP解压,查询软件。假如发现IP地址有不对的,或想提供新的IP地址,请到纯真IP小秘书提供IP地址数据,或者登陆 47 | 纯真时空论坛“电脑板块”中的“代理及IP板”块告诉我们。,以便及时更新IP数据库。谢谢! 48 | 49 | ★★★★★★★★★★★★★★★★★★★★★★★★ 50 | 在线升级数据库: 51 | 随数据库附送的查询程序(ip.exe)具有在线检测并升级IP数据库的功能,只要运行该程序,点击右上角的 52 | “在线升级”,就可以升级IP数据库到新的版本,无需再到下载网站下载新版的IP数据库。 53 | ★★★★★★★★★★★★★★★★★★★★★★★★ 54 | 55 | 56 | IP小秘书:http://www.cz88.net/ip 57 | 纯真网络:http://www.cz88.net 58 | 纯真时空论坛:http://bbs.cz88.net 59 | 60 | 金狐 QQ:486126 EMail:admin@cz88.net 61 | 62 | 63 | ============================================================ 64 | 65 | ip-database 协议简述 66 | 67 | 1,可以自由使用解析程序,但务必在程序内标明来源。https://github.com/itbdw/it-database 68 | 2,不保证任何准确性,应当知晓方可使用 69 | 3,仅供学习和娱乐用,如被用作违法事宜,本人均不承担任何责任 70 | 4,其它合理性解释权利归本人所有 71 | 72 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | ## 说明 2 | 3 | 这套识别程序的数据库是免费IP数据库、IP离线地址库。支持将IP转化为结构化的国家、省、市、县、运营商、地区信息 4 | 5 | 0,该系统是离线的,代码内已经包含IPV4+IPV6离线包。 6 | 7 | 1,IPV4数据库基于纯真IP库,IP地址纠错相关和最新地址获取请直接去纯真官网,具体见最下方链接。IPV6数据库见最下方链接。 8 | 9 | 2,纯真IP识别算法来源网络。 10 | 11 | 3,结构化程序来自我2012年的脑洞。 12 | 13 | 纯真IP数据存储时,并不是结构化的,因此 结构化程序 解析出来有误差在所难免,国内 ip 可以识别出省份,基本可以识别出市。运营商、县数据看运气。 14 | 15 | ## 使用说明 16 | 17 | 当前版本为3.x(ipv4+ipv6),如需要2.x请访问 https://github.com/itbdw/ip-database/tree/2.x 18 | 19 | 目前3.x无缝兼容2.x版本,理论上可直接升级。 20 | 21 | ``` 22 | composer require 'itbdw/ip-database' ^3.0 23 | ``` 24 | 25 | 26 | ```php 27 | 28 | //根据实际情况,基本上用框架(如 Laravel)的话不需要手动引入 29 | //require 'vendor/autoload.php'; 30 | 31 | use itbdw\Ip\IpLocation; 32 | 33 | //0配置使用 34 | echo json_encode(IpLocation::getLocation($ip), JSON_UNESCAPED_UNICODE) . "\n"; 35 | 36 | //支持自定义文件路径 37 | $qqwry_filepath = '/abspath/qqwry.dat'; 38 | $ipv6wry_path = '/abspath/ipv6wry.db'; 39 | echo json_encode(IpLocation::getLocation($ip, $qqwry_filepath), JSON_UNESCAPED_UNICODE) . "\n"; 40 | 41 | 42 | ``` 43 | 44 | ## 响应 45 | 46 | 获取成功 47 | ```json 48 | { 49 | "ip": "163.177.65.160", 50 | "country": "中国", 51 | "province": "广东", 52 | "city": "深圳市", 53 | "county": "", 54 | "isp": "联通", 55 | "area": "中国广东省深圳市腾讯计算机系统联通节点" 56 | } 57 | ``` 58 | 59 | 异常 60 | ```json 61 | { 62 | "error": "ip invalid" 63 | } 64 | ``` 65 | 66 | 67 | ## 本地测试测试 68 | 69 | ``` 70 | cd 进入 ip-database 目录 ,composer install 71 | 72 | php tests/ip.php 73 | 74 | {"ip":"172.217.25.14","country":"美国","province":"","city":"","county":"","area":"美国 Google全球边缘网络","isp":""} 75 | {"ip":"140.205.172.5","country":"中国","province":"上海","city":"","county":"","area":"中国上海 阿里云","isp":""} 76 | {"ip":"123.125.115.110","country":"中国","province":"北京","city":"","county":"","area":"中国北京 北京百度网讯科技有限公司联通节点(BGP)","isp":"联通"} 77 | {"ip":"221.196.0.0","country":"中国","province":"天津","city":"河北区","county":"","area":"中国天津河北区 联通","isp":"联通"} 78 | {"ip":"60.195.153.98","country":"中国","province":"北京","city":"顺义区","county":"","area":"中国北京顺义区 后沙峪金龙网吧","isp":""} 79 | {"ip":"218.193.183.35","country":"中国","province":"上海","city":"","county":"","area":"中国上海 D27-707","isp":""} 80 | {"ip":"210.74.2.227","country":"中国","province":"北京","city":"","county":"","area":"中国北京 实验学院机房","isp":""} 81 | {"ip":"162.105.217.0","country":"中国","province":"北京","city":"","county":"","area":"中国北京 4区-4f","isp":""} 82 | {"ip":"fe80:0000:0001:0000:0440:44ff:1233:5678","country":"局域网","province":"","city":"","county":"","area":"局域网 本地链路单播地址","isp":""} 83 | {"ip":"2409:8900:103f:14f:d7e:cd36:11af:be83","country":"中国","province":"北京","city":"","county":"","area":"中国北京 中国移动CMNET网络","isp":"移动"} 84 | 85 | php tests/ip.php -i 58.196.128.0 86 | {"ip":"58.196.128.0","country":"中国","province":"上海","city":"","county":"","area":"中国上海 上海交通大学","isp":""} 87 | 88 | php tests/ip.php -i 2409:8a00:6c1d:81c0:51b4:d603:57d1:b5ec 89 | {"ip":"2409:8a00:6c1d:81c0:51b4:d603:57d1:b5ec","country":"中国","province":"北京","city":"","county":"","area":"中国北京 中国移动公众宽带","isp":"移动"} 90 | 91 | ``` 92 | 93 | ## 提高下载速度 94 | 建议腾讯云加速 https://mirrors.cloud.tencent.com/help/composer.html 95 | 96 | 原因 https://github.com/itbdw/ip-database/issues/42 97 | 98 | ## 赞助喝口水 99 | 这个项目也是多个日夜思考的结果,如果觉得对你有帮助,小手一抖也是感谢的。 100 |
101 |
102 | 103 |
104 |
105 | 106 | ## 手动更新离线包 107 | 1,纯真IP库(需要安装 EXE ,解压获得离线包) 108 | https://www.cz88.net/help?id=free 109 | 110 | 2,IPV6(目前已经不再提供离线下载包) 111 | https://ip.zxinc.org/ipquery/ 112 | -------------------------------------------------------------------------------- /UPDATE.MD: -------------------------------------------------------------------------------- 1 | ## update log 2 | 3 | 4 | 5 | ``` 6 | IP 地理位置查询类 7 | 8 | 2023-12-01 9 | 升级 qqwry.day 2023011022 版本 10 | 11 | 12 | 2021-04-10 13 | 14 | 1,3.x 支持ipv6,无缝衔接2.x 15 | 3.x已完成的功能有 16 | 17 | - 支持ipv6 18 | - 2.x 平滑升级 19 | 20 | 计划完成的功能有 21 | 22 | - 解析民政部地区码,以便返回更加精确的城市信息;同时可携带其他信息,如地区码、经纬度等 23 | 24 | 25 | 26 | 2020-10-30 27 | 28 | 1,大更新,重构代码。兼容历史代码。希望能支持ipv6 29 | 30 | 2019-07-25 31 | 32 | 1,增加自动更新功能,参考 https://blog.shuax.com/archives/QQWryUpdate.html 感谢 https://github.com/itbdw/ip-database/issues/10 33 | 34 | 2017-09-12 35 | 36 | 1,缩减返回数据,去掉字段 remark smallarea beginip endip 37 | 2,将调用改为单例模式,保证只读取一次文件 38 | 3,修复 bug,直接将返回 gbk 编码内容转为 utf-8,移除编码隐患 39 | 4,去掉了"省"标志,变成了如 中国 浙江 杭州市 这样的数据 40 | 41 | 2017-09-04 42 | 43 | 1,更新 composer 相对路径,bug fix 44 | 45 | 2015-06-11 46 | 47 | 1,支持composer 方式引用 48 | 2,更新 is_valid_ip 实现 49 | 50 | 2013-11-10 51 | 52 | 1,优化,新增支持到市区,县城 53 | 2,返回结构增加 smallarea,具体请看注释 54 | 55 | 2012-10-21 56 | 57 | 1,增加市,县显示 58 | 2,去掉不靠谱的自动转码 59 | 先统一改为 GBK,最后再做转换解决编码问题 60 | 61 | 2012-08-15 62 | 63 | 1,更新为 PHP5 的规范 64 | 2,增加 wphp_ip2long 方法 65 | 3,增加 get_province 方法 66 | 4,增加 get_isp 方法 67 | 5,增加 is_valid_ip 方法 68 | 69 | ``` -------------------------------------------------------------------------------- /bin/update-ip.php: -------------------------------------------------------------------------------- 1 | false, 35 | CURLOPT_RETURNTRANSFER => true, 36 | CURLOPT_TIMEOUT => 600, 37 | CURLOPT_CUSTOMREQUEST => "GET", 38 | CURLOPT_HTTPHEADER => $header, 39 | CURLOPT_URL => $url, 40 | )); 41 | $res = curl_exec($curl); 42 | 43 | if (curl_errno($curl)) { 44 | error_log("curl error " . curl_errno($curl) . ' ' . curl_error($curl)); 45 | } 46 | 47 | curl_close($curl); 48 | return $res; 49 | } 50 | 51 | //可设置为服务器特定目录,单独,避免组件升级互相影响 52 | $dir = dirname(__DIR__) . "/src/libs"; 53 | $option = getopt("d::"); 54 | if (isset($option['d'])) { 55 | if (!is_readable($option['d'])) { 56 | die("bad param, dir not readable " . $option['d']); 57 | } 58 | $dir = $option['d']; 59 | } 60 | 61 | $stime = microtime(true); 62 | 63 | echo "开始准备更新数据库" . date("Y-m-d H:i:s"); 64 | echo "\n"; 65 | 66 | $copywrite = curls_get("http://update.cz88.net/ip/copywrite.rar"); 67 | 68 | if (!$copywrite) { 69 | $download_spend = $qqwry_time - $stime; 70 | die("copywrite.rar 下载失败 " . sprintf("下载耗时%s", $download_spend)); 71 | } 72 | 73 | 74 | $qqwry = curls_get("https://update.cz88.net/ip/qqwry.rar"); 75 | $qqwry_time = microtime(true); 76 | 77 | if (!$qqwry) { 78 | $download_spend = $qqwry_time - $stime; 79 | die("qqwry.rar 下载失败 " . sprintf("下载耗时%s", $download_spend)); 80 | } 81 | 82 | $key = unpack("V6", $copywrite)[6]; 83 | for ($i = 0; $i < 0x200; $i++) { 84 | $key *= 0x805; 85 | $key++; 86 | $key = $key & 0xFF; 87 | $qqwry[$i] = chr(ord($qqwry[$i]) ^ $key); 88 | } 89 | $qqwry = gzuncompress($qqwry); 90 | $unzip_time = microtime(true); 91 | 92 | $download_spend = $qqwry_time - $stime; 93 | $unzip_spend = $unzip_time - $qqwry_time; 94 | 95 | if (!$qqwry) { 96 | die("gzip 解压缩失败 " . sprintf("下载耗时%s,解压耗时%s", $download_spend, $unzip_spend)); 97 | } 98 | 99 | $tmp_file = $dir . '/' . 'qqwry.dat.bak'; 100 | $online_file = $dir . '/' . 'qqwry.dat'; 101 | 102 | if (file_put_contents($tmp_file, $qqwry)) { 103 | $put_time = microtime(true); 104 | $put_spend = $put_time - $unzip_time; 105 | copy($online_file, $online_file.'.online.bak'); 106 | copy($tmp_file, $online_file); 107 | 108 | $copy_spend = microtime(true) - $put_time; 109 | die("更新成功 " . sprintf("下载耗时%s,解压耗时%s,写入耗时%s,复制耗时%s", $download_spend, $unzip_spend, $put_spend, $copy_spend)); 110 | } else { 111 | die("更新失败 " . sprintf("下载耗时%s,解压耗时%s", $download_spend, $unzip_spend)); 112 | } 113 | -------------------------------------------------------------------------------- /bin/update-region-code.php: -------------------------------------------------------------------------------- 1 | setDBPath(self::getIpV4Path()); 49 | $location = $ins->getIp($ip); 50 | } else if (self::isIpV6($ip)) { 51 | $ins = new IpV6wry(); 52 | $ins->setDBPath(self::getIpV6Path()); 53 | $location = $ins->getIp($ip); 54 | 55 | } else { 56 | $location = [ 57 | 'error' => 'IP Invalid' 58 | ]; 59 | } 60 | 61 | return $location; 62 | } 63 | 64 | /** 65 | * @param $ip 66 | * @param string $ipV4Path 67 | * @param string $ipV6Path 68 | * @return array|mixed 69 | */ 70 | public static function getLocation($ip, $ipV4Path='', $ipV6Path='') { 71 | $location = self::getLocationWithoutParse($ip, $ipV4Path, $ipV6Path); 72 | if (isset($location['error'])) { 73 | return $location; 74 | } 75 | return StringParser::parse($location); 76 | } 77 | 78 | /** 79 | * @param $path 80 | */ 81 | public static function setIpV4Path($path) 82 | { 83 | self::$ipV4Path = $path; 84 | } 85 | 86 | /** 87 | * @param $path 88 | */ 89 | public static function setIpV6Path($path) 90 | { 91 | self::$ipV6Path = $path; 92 | } 93 | 94 | /** 95 | * @return string 96 | */ 97 | private static function getIpV4Path() { 98 | return self::$ipV4Path ? : self::src('/libs/qqwry.dat'); 99 | } 100 | 101 | /** 102 | * @return string 103 | */ 104 | private static function getIpV6Path() { 105 | return self::$ipV6Path ? : self::src('/libs/ipv6wry.db'); 106 | } 107 | 108 | /** 109 | * @param $ip 110 | * @return bool 111 | */ 112 | private static function isIpV4($ip) { 113 | return false !== filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); 114 | } 115 | 116 | /** 117 | * @param $ip 118 | * @return bool 119 | */ 120 | private static function isIpV6($ip) { 121 | return false !== filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); 122 | } 123 | 124 | /** 125 | * @param $filename 126 | * @return string 127 | */ 128 | public static function src($filename) { 129 | return self::root('/src'.$filename); 130 | } 131 | 132 | /** 133 | * @param $filename 134 | * @return string 135 | */ 136 | public static function root($filename) { 137 | return IP_DATABASE_ROOT_DIR . $filename; 138 | } 139 | } -------------------------------------------------------------------------------- /src/IpParser/IpParserInterface.php: -------------------------------------------------------------------------------- 1 | $exception->getMessage(), 30 | ]; 31 | } 32 | 33 | $return = [ 34 | 'ip' => $ip, 35 | 'country' => $tmp['addr'][0], 36 | 'area' => $tmp['addr'][1], 37 | ]; 38 | return $return; 39 | } 40 | 41 | 42 | private static $filePath; 43 | 44 | const FORMAT = 'J2'; 45 | private static $total = null; 46 | // 索引区 47 | private static $index_start_offset; 48 | private static $index_end_offset; 49 | private static $offlen; 50 | private static $iplen; 51 | private static $has_initialized = false; 52 | 53 | /** 54 | * return database record count 55 | * @return int|string 56 | */ 57 | public static function total() 58 | { 59 | if (null === static::$total) { 60 | $fd = fopen(static::$filePath, 'rb'); 61 | static::initialize($fd); 62 | fclose($fd); 63 | } 64 | return static::$total; 65 | } 66 | 67 | public static function initialize($fd) 68 | { 69 | if (!static::$has_initialized) { 70 | if (PHP_INT_SIZE < 8) { 71 | throw new \RuntimeException('64bit OS supported only'); 72 | } 73 | if (version_compare(PHP_VERSION, "7.0", "<")) { 74 | throw new \RuntimeException('php version 7.0 or greater'); 75 | } 76 | static::$index_start_offset = static::read8($fd, 16); 77 | static::$offlen = static::read1($fd, 6); 78 | static::$iplen = static::read1($fd, 7); 79 | static::$total = static::read8($fd, 8); 80 | static::$index_end_offset = static::$index_start_offset 81 | + (static::$iplen + static::$offlen) * static::$total; 82 | static::$has_initialized = true; 83 | } 84 | } 85 | 86 | /** 87 | * query ipv6 88 | * @param $ip 89 | * @return array 90 | */ 91 | public static function query($ip) 92 | { 93 | $ip_bin = inet_pton($ip); 94 | if (false === $ip_bin) { 95 | throw new \RuntimeException("error IPv6 address: $ip"); 96 | } 97 | if (16 !== strlen($ip_bin)) { 98 | throw new \RuntimeException("error IPv6 address: $ip"); 99 | } 100 | $fd = fopen(static::$filePath, 'rb'); 101 | static::initialize($fd); 102 | $ip_num_arr = unpack(static::FORMAT, $ip_bin); 103 | // IP地址前半部分转换成有int 104 | $ip_num1 = $ip_num_arr[1]; 105 | // IP地址后半部分转换成有int 106 | $ip_num2 = $ip_num_arr[2]; 107 | $ip_find = static::find($fd, $ip_num1, $ip_num2, 0, static::$total); 108 | $ip_offset = static::$index_start_offset + $ip_find * (static::$iplen + static::$offlen); 109 | $ip_offset2 = $ip_offset + static::$iplen + static::$offlen; 110 | $ip_start = inet_ntop(pack(static::FORMAT, static::read8($fd, $ip_offset), 0)); 111 | try { 112 | $ip_end = inet_ntop(pack(static::FORMAT, static::read8($fd, $ip_offset2) - 1, 0)); 113 | } catch (\RuntimeException $e) { 114 | $ip_end = "FFFF:FFFF:FFFF:FFFF::"; 115 | } 116 | $ip_record_offset = static::read8($fd, $ip_offset + static::$iplen, static::$offlen); 117 | $ip_addr = static::read_record($fd, $ip_record_offset); 118 | $ip_addr_disp = $ip_addr[0] . " " . $ip_addr[1]; 119 | if (is_resource($fd)) { 120 | fclose($fd); 121 | } 122 | return ["start" => $ip_start, "end" => $ip_end, "addr" => $ip_addr, "disp" => $ip_addr_disp]; 123 | } 124 | 125 | /** 126 | * 读取记录 127 | * @param $fd 128 | * @param $offset 129 | * @return string[] 130 | */ 131 | public static function read_record($fd, $offset) 132 | { 133 | $record = [0 => "", 1 => ""]; 134 | $flag = static::read1($fd, $offset); 135 | if ($flag == 1) { 136 | $location_offset = static::read8($fd, $offset + 1, static::$offlen); 137 | return static::read_record($fd, $location_offset); 138 | } 139 | $record[0] = static::read_location($fd, $offset); 140 | if ($flag == 2) { 141 | $record[1] = static::read_location($fd, $offset + static::$offlen + 1); 142 | } else { 143 | $record[1] = static::read_location($fd, $offset + strlen($record[0]) + 1); 144 | } 145 | return $record; 146 | } 147 | 148 | /** 149 | * 读取地区 150 | * @param $fd 151 | * @param $offset 152 | * @return string 153 | */ 154 | public static function read_location($fd, $offset) 155 | { 156 | if ($offset == 0) { 157 | return ""; 158 | } 159 | $flag = static::read1($fd, $offset); 160 | // 出错 161 | if ($flag == 0) { 162 | return ""; 163 | } 164 | // 仍然为重定向 165 | if ($flag == 2) { 166 | $offset = static::read8($fd, $offset + 1, static::$offlen); 167 | return static::read_location($fd, $offset); 168 | } 169 | return static::readstr($fd, $offset); 170 | } 171 | 172 | /** 173 | * 查找 ip 所在的索引 174 | * @param $fd 175 | * @param $ip_num1 176 | * @param $ip_num2 177 | * @param $l 178 | * @param $r 179 | * @return mixed 180 | */ 181 | public static function find($fd, $ip_num1, $ip_num2, $l, $r) 182 | { 183 | if ($l + 1 >= $r) { 184 | return $l; 185 | } 186 | $m = intval(($l + $r) / 2); 187 | $m_ip1 = static::read8($fd, static::$index_start_offset + $m * (static::$iplen + static::$offlen), 188 | static::$iplen); 189 | $m_ip2 = 0; 190 | if (static::$iplen <= 8) { 191 | $m_ip1 <<= 8 * (8 - static::$iplen); 192 | } else { 193 | $m_ip2 = static::read8($fd, static::$index_start_offset + $m * (static::$iplen + static::$offlen) + 8, 194 | static::$iplen - 8); 195 | $m_ip2 <<= 8 * (16 - static::$iplen); 196 | } 197 | if (static::uint64cmp($ip_num1, $m_ip1) < 0) { 198 | return static::find($fd, $ip_num1, $ip_num2, $l, $m); 199 | } 200 | if (static::uint64cmp($ip_num1, $m_ip1) > 0) { 201 | return static::find($fd, $ip_num1, $ip_num2, $m, $r); 202 | } 203 | if (static::uint64cmp($ip_num2, $m_ip2) < 0) { 204 | return static::find($fd, $ip_num1, $ip_num2, $l, $m); 205 | } 206 | return static::find($fd, $ip_num1, $ip_num2, $m, $r); 207 | } 208 | 209 | public static function readraw($fd, $offset = null, $size = 0) 210 | { 211 | if (!is_null($offset)) { 212 | fseek($fd, $offset); 213 | } 214 | return fread($fd, $size); 215 | } 216 | 217 | public static function read1($fd, $offset = null) 218 | { 219 | if (!is_null($offset)) { 220 | fseek($fd, $offset); 221 | } 222 | $a = fread($fd, 1); 223 | return @unpack("C", $a)[1]; 224 | } 225 | 226 | public static function read8($fd, $offset = null, $size = 8) 227 | { 228 | if (!is_null($offset)) { 229 | fseek($fd, $offset); 230 | } 231 | $a = fread($fd, $size) . "\0\0\0\0\0\0\0\0"; 232 | return @unpack("P", $a)[1]; 233 | } 234 | 235 | public static function readstr($fd, $offset = null) 236 | { 237 | if (!is_null($offset)) { 238 | fseek($fd, $offset); 239 | } 240 | $str = ""; 241 | $chr = static::read1($fd, $offset); 242 | while ($chr != 0) { 243 | $str .= chr($chr); 244 | $offset++; 245 | $chr = static::read1($fd, $offset); 246 | } 247 | return $str; 248 | } 249 | 250 | public static function ip2num($ip) 251 | { 252 | return unpack("N", inet_pton($ip))[1]; 253 | } 254 | 255 | public static function inet_ntoa($nip) 256 | { 257 | $ip = []; 258 | for ($i = 3; $i > 0; $i--) { 259 | $ip_seg = intval($nip / pow(256, $i)); 260 | $ip[] = $ip_seg; 261 | $nip -= $ip_seg * pow(256, $i); 262 | } 263 | $ip[] = $nip; 264 | return join(".", $ip); 265 | } 266 | 267 | public static function uint64cmp($a, $b) 268 | { 269 | if ($a >= 0 && $b >= 0 || $a < 0 && $b < 0) { 270 | return $a <=> $b; 271 | } 272 | if ($a >= 0 && $b < 0) { 273 | return -1; 274 | } 275 | return 1; 276 | } 277 | 278 | } 279 | -------------------------------------------------------------------------------- /src/IpParser/QQwry.php: -------------------------------------------------------------------------------- 1 | filePath = $filePath; 17 | } 18 | 19 | /** 20 | * @param $ip 21 | * @return array 22 | */ 23 | public function getIp($ip) 24 | { 25 | try { 26 | $tmp = $this->getAddr($ip); 27 | } catch (\Exception $exception) { 28 | return [ 29 | 'error' => $exception->getMessage(), 30 | ]; 31 | } 32 | 33 | $return = [ 34 | 'ip' => $ip, 35 | 'country' => $tmp['country'], 36 | 'area' => $tmp['area'], 37 | ]; 38 | return $return; 39 | } 40 | 41 | /** 42 | * 文件路径 43 | * @var string 44 | */ 45 | private $filePath; 46 | /** 47 | * qqwry.dat文件指针 48 | * 49 | * @var resource 50 | */ 51 | private $fp; 52 | /** 53 | * 第一条IP记录的偏移地址 54 | * 55 | * @var int 56 | */ 57 | private $firstIp; 58 | /** 59 | * 最后一条IP记录的偏移地址 60 | * 61 | * @var int 62 | */ 63 | private $lastIp; 64 | /** 65 | * IP记录的总条数(不包含版本信息记录) 66 | * 67 | * @var int 68 | */ 69 | private $totalIp; 70 | 71 | 72 | /** 73 | * 如果ip错误 74 | * 75 | * $result 是返回的数组 76 | * $result['ip'] 输入的ip 77 | * $result['country'] 国家 如 中国 78 | * $result['area'] 最完整的信息 如 中国河北省邢台市威县新科网吧(北外街) 79 | * 80 | * 81 | * @param $ip 82 | * @return array 83 | */ 84 | public function getAddr($ip) 85 | { 86 | $filename = $this->filePath; 87 | if (!file_exists($filename)) { 88 | trigger_error("Failed open ip database file!"); 89 | throw new \Exception('Failed open ip database'); 90 | } 91 | if (is_null($this->fp)) { 92 | $this->fp = 0; 93 | if (($this->fp = fopen($filename, 'rb')) !== false) { 94 | $this->firstIp = $this->getLong(); 95 | $this->lastIp = $this->getLong(); 96 | $this->totalIp = ($this->lastIp - $this->firstIp) / 7; 97 | } 98 | } 99 | $location = $this->getLocation($ip); 100 | return $location; 101 | } 102 | 103 | /** 104 | * 返回读取的长整型数 105 | * 106 | * @access private 107 | * @return int 108 | */ 109 | private function getLong() 110 | { 111 | //将读取的little-endian编码的4个字节转化为长整型数 112 | $result = unpack('Vlong', fread($this->fp, 4)); 113 | return $result['long']; 114 | } 115 | 116 | /** 117 | * 根据所给 IP 地址或域名返回所在地区信息 118 | * 119 | * @access public 120 | * @param string $ip 121 | * @return array ip country area beginip endip 122 | */ 123 | private function getLocation($ip) 124 | { 125 | if (!$this->fp) { 126 | return null; 127 | } 128 | // 如果数据文件没有被正确打开,则直接返回空 129 | $location['ip'] = $ip; 130 | $ip = $this->packIp($location['ip']); 131 | // 将输入的IP地址转化为可比较的IP地址 132 | // 不合法的IP地址会被转化为255.255.255.255 133 | // 对分搜索 134 | $l = 0; 135 | // 搜索的下边界 136 | $u = $this->totalIp; 137 | // 搜索的上边界 138 | $findip = $this->lastIp; 139 | // 如果没有找到就返回最后一条IP记录(qqwry.dat的版本信息) 140 | while ($l <= $u) { 141 | // 当上边界小于下边界时,查找失败 142 | $i = floor(($l + $u) / 2); 143 | // 计算近似中间记录 144 | fseek($this->fp, $this->firstIp + $i * 7); 145 | $beginip = strrev(fread($this->fp, 4)); 146 | // 获取中间记录的开始IP地址 147 | // strrev函数在这里的作用是将little-endian的压缩IP地址转化为big-endian的格式 148 | // 以便用于比较,后面相同。 149 | if ($ip < $beginip) { 150 | // 用户的IP小于中间记录的开始IP地址时 151 | $u = $i - 1; 152 | // 将搜索的上边界修改为中间记录减一 153 | } else { 154 | fseek($this->fp, $this->getLong3()); 155 | $endip = strrev(fread($this->fp, 4)); 156 | // 获取中间记录的结束IP地址 157 | if ($ip > $endip) { 158 | // 用户的IP大于中间记录的结束IP地址时 159 | $l = $i + 1; 160 | // 将搜索的下边界修改为中间记录加一 161 | } else { 162 | // 用户的IP在中间记录的IP范围内时 163 | $findip = $this->firstIp + $i * 7; 164 | break; 165 | // 则表示找到结果,退出循环 166 | } 167 | } 168 | } 169 | //获取查找到的IP地理位置信息 170 | fseek($this->fp, $findip); 171 | $location['beginip'] = long2ip($this->getLong()); 172 | // 用户IP所在范围的开始地址 173 | $offset = $this->getLong3(); 174 | fseek($this->fp, $offset); 175 | $location['endip'] = long2ip($this->getLong()); 176 | // 用户IP所在范围的结束地址 177 | $byte = fread($this->fp, 1); 178 | // 标志字节 179 | switch (ord($byte)) { 180 | case 1: // 标志字节为1,表示国家和区域信息都被同时重定向 181 | $countryOffset = $this->getLong3(); 182 | // 重定向地址 183 | fseek($this->fp, $countryOffset); 184 | $byte = fread($this->fp, 1); 185 | // 标志字节 186 | switch (ord($byte)) { 187 | case 2: // 标志字节为2,表示国家信息被重定向 188 | fseek($this->fp, $this->getLong3()); 189 | $location['country'] = $this->getString(); 190 | fseek($this->fp, $countryOffset + 4); 191 | $location['area'] = $this->getArea(); 192 | break; 193 | default: // 否则,表示国家信息没有被重定向 194 | $location['country'] = $this->getString($byte); 195 | $location['area'] = $this->getArea(); 196 | break; 197 | } 198 | break; 199 | case 2: // 标志字节为2,表示国家信息被重定向 200 | fseek($this->fp, $this->getLong3()); 201 | $location['country'] = $this->getString(); 202 | fseek($this->fp, $offset + 8); 203 | $location['area'] = $this->getArea(); 204 | break; 205 | default: // 否则,表示国家信息没有被重定向 206 | $location['country'] = $this->getString($byte); 207 | $location['area'] = $this->getArea(); 208 | break; 209 | } 210 | $location['country'] = iconv("GBK", "UTF-8", $location['country']); 211 | $location['area'] = iconv("GBK", "UTF-8", $location['area']); 212 | if ($location['country'] == " CZ88.NET" || $location['country'] == "纯真网络") { 213 | // CZ88.NET表示没有有效信息 214 | $location['country'] = "无数据"; 215 | } 216 | if ($location['area'] == " CZ88.NET") { 217 | $location['area'] = ""; 218 | } 219 | return $location; 220 | } 221 | 222 | /** 223 | * 返回压缩后可进行比较的IP地址 224 | * 225 | * @access private 226 | * @param string $ip 227 | * @return string 228 | */ 229 | private function packIp($ip) 230 | { 231 | // 将IP地址转化为长整型数,如果在PHP5中,IP地址错误,则返回False, 232 | // 这时intval将Flase转化为整数-1,之后压缩成big-endian编码的字符串 233 | return pack('N', intval($this->ip2long($ip))); 234 | } 235 | 236 | /** 237 | * Ip 地址转为数字地址 238 | * php 的 ip2long 这个函数有问题 239 | * 133.205.0.0 ==>> 2244804608 240 | * 241 | * @param string $ip 要转换的 ip 地址 242 | * @return int 转换完成的数字 243 | */ 244 | private function ip2long($ip) 245 | { 246 | $ip_arr = explode('.', $ip); 247 | $iplong = (16777216 * intval($ip_arr[0])) + (65536 * intval($ip_arr[1])) + (256 * intval($ip_arr[2])) + intval($ip_arr[3]); 248 | return $iplong; 249 | } 250 | 251 | /** 252 | * 返回读取的3个字节的长整型数 253 | * 254 | * @access private 255 | * @return int 256 | */ 257 | private function getLong3() 258 | { 259 | //将读取的little-endian编码的3个字节转化为长整型数 260 | $result = unpack('Vlong', fread($this->fp, 3) . chr(0)); 261 | return $result['long']; 262 | } 263 | 264 | /** 265 | * 返回读取的字符串 266 | * 267 | * @access private 268 | * @param string $data 269 | * @return string 270 | */ 271 | private function getString($data = "") 272 | { 273 | $char = fread($this->fp, 1); 274 | while (ord($char) > 0) { 275 | // 字符串按照C格式保存,以\0结束 276 | $data .= $char; 277 | // 将读取的字符连接到给定字符串之后 278 | $char = fread($this->fp, 1); 279 | } 280 | return $data; 281 | } 282 | 283 | /** 284 | * 返回地区信息 285 | * 286 | * @access private 287 | * @return string 288 | */ 289 | private function getArea() 290 | { 291 | $byte = fread($this->fp, 1); 292 | // 标志字节 293 | switch (ord($byte)) { 294 | case 0: // 没有区域信息 295 | $area = ""; 296 | break; 297 | case 1: 298 | case 2: // 标志字节为1或2,表示区域信息被重定向 299 | fseek($this->fp, $this->getLong3()); 300 | $area = $this->getString(); 301 | break; 302 | default: // 否则,表示区域信息没有被重定向 303 | $area = $this->getString($byte); 304 | break; 305 | } 306 | return $area; 307 | } 308 | 309 | /** 310 | * 析构函数,用于在页面执行结束后自动关闭打开的文件。 311 | */ 312 | public function __destruct() 313 | { 314 | if ($this->fp) { 315 | fclose($this->fp); 316 | } 317 | $this->fp = 0; 318 | } 319 | } -------------------------------------------------------------------------------- /src/StringParser.php: -------------------------------------------------------------------------------- 1 | $value) { 148 | 149 | if (false !== strpos($location['country'], $value)) { 150 | $isChina = true; 151 | $location['province'] = $value; 152 | 153 | //直辖市 154 | if (in_array($value, self::$dictCityDirectly)) { 155 | 156 | //直辖市 157 | $_tmp_province = explode($value, $location['country']); 158 | 159 | //市辖区 160 | if (isset($_tmp_province[1])) { 161 | 162 | $_tmp_province[1] = self::lTrim($_tmp_province[1], $separatorCity); 163 | 164 | 165 | if (strpos($_tmp_province[1], $separatorDistrict) !== false) { 166 | $_tmp_qu = explode($separatorDistrict, $_tmp_province[1]); 167 | 168 | //解决 休息休息校区 变成城市区域 169 | $isHitBlackTail = false; 170 | foreach (self::$dictDistrictBlackTails as $blackTail) { 171 | //尾 172 | if (mb_substr($_tmp_qu[0], -mb_strlen($blackTail)) == $blackTail) { 173 | $isHitBlackTail = true; 174 | break; 175 | } 176 | } 177 | 178 | //校区,学区 179 | if ((!$isHitBlackTail) && mb_strlen($_tmp_qu[0]) < 5) { 180 | //有点尴尬 181 | $location['city'] = $_tmp_qu[0] . $separatorDistrict; 182 | } 183 | } 184 | } 185 | } else { 186 | 187 | //没有省份标志 只能替换 188 | $_tmp_city = str_replace($location['province'], '', $location['country']); 189 | 190 | //防止直辖市捣乱 上海市xxx区 =》 市xx区 191 | $_tmp_city = self::lTrim($_tmp_city, $separatorCity); 192 | 193 | //内蒙古 类型的 获取市县信息 194 | if (strpos($_tmp_city, $separatorCity) !== false) { 195 | //市 196 | $_tmp_city = explode($separatorCity, $_tmp_city); 197 | 198 | $location['city'] = $_tmp_city[0] . $separatorCity; 199 | 200 | //县 201 | if (isset($_tmp_city[1])) { 202 | if (strpos($_tmp_city[1], $separatorCounty) !== false) { 203 | $_tmp_county = explode($separatorCounty, $_tmp_city[1]); 204 | $location['county'] = $_tmp_county[0] . $separatorCounty; 205 | } 206 | 207 | //区 208 | if (!$location['county'] && strpos($_tmp_city[1], $separatorDistrict) !== false) { 209 | $_tmp_qu = explode($separatorDistrict, $_tmp_city[1]); 210 | $location['county'] = $_tmp_qu[0] . $separatorDistrict; 211 | } 212 | } 213 | } 214 | } 215 | 216 | break; 217 | } 218 | } 219 | } 220 | 221 | if ($isChina) { 222 | $location['country'] = '中国'; 223 | } 224 | 225 | 226 | $result['ip'] = $location['ip']; 227 | 228 | $result['country'] = $location['country']; 229 | $result['province'] = $location['province']; 230 | $result['city'] = $location['city']; 231 | $result['county'] = $location['county']; 232 | 233 | $result['area'] = $location['country'] . $location['province'] . $location['city'] . $location['county'] . ' ' . $location['org_area']; 234 | 235 | $result['isp'] = self::getIsp($result['area']); 236 | 237 | if ($withOriginal) { 238 | $result['org'] = $org; 239 | } 240 | 241 | return $result; 242 | } 243 | 244 | 245 | /** 246 | * @param $str 247 | * @return string 248 | */ 249 | private static function getIsp($str) 250 | { 251 | $ret = ''; 252 | 253 | foreach (self::$dictIsp as $k => $v) { 254 | if (false !== strpos($str, $v)) { 255 | $ret = $v; 256 | break; 257 | } 258 | } 259 | 260 | return $ret; 261 | } 262 | 263 | private static function lTrim($word, $w) { 264 | $pos = mb_stripos($word, $w); 265 | if ($pos === 0) { 266 | $word = mb_substr($word, 1); 267 | } 268 | return $word; 269 | } 270 | 271 | 272 | } -------------------------------------------------------------------------------- /src/libs/ipv6wry.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itbdw/ip-database/977b44904396390272a18cbf7ec29893d602652f/src/libs/ipv6wry.db -------------------------------------------------------------------------------- /src/libs/qqwry.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itbdw/ip-database/977b44904396390272a18cbf7ec29893d602652f/src/libs/qqwry.dat -------------------------------------------------------------------------------- /tests/ip.php: -------------------------------------------------------------------------------- 1 | 4 | * @since 2015-06-11 5 | */ 6 | 7 | //you do not need to do this if use composer! 8 | //require dirname(__DIR__) . '/src/IpParser/IpParserInterface.php'; 9 | // 10 | //require dirname(__DIR__) . '/src/IpLocation.php'; 11 | //require dirname(__DIR__) . '/src/IpParser/QQwry.php'; 12 | //require dirname(__DIR__) . '/src/IpParser/IpV6wry.php'; 13 | //require dirname(__DIR__) . '/src/StringParser.php'; 14 | 15 | //需要 composer install 或者去掉上面注释,并注释这一行 16 | include dirname(__DIR__) .'/vendor/autoload.php'; 17 | 18 | $input = getopt("i:", [], $id); 19 | 20 | use itbdw\Ip\IpLocation; 21 | 22 | $ips = [ 23 | "172.217.25.14",//美国 24 | "140.205.172.5",//杭州 25 | "123.125.115.110",//北京 26 | "221.196.0.0",// 27 | "60.195.153.98", 28 | 29 | //bug ip 都是涉及到直辖市的 30 | "218.193.183.35", //"province":"上海交通大学闵行校区", 31 | "210.74.2.227", //,"province":"北京工业大学","city":"", 32 | "162.105.217.0", //,"province":"北京大学万柳学区","ci 33 | 34 | "fe80:0000:0001:0000:0440:44ff:1233:5678", 35 | "2409:8900:103f:14f:d7e:cd36:11af:be83", 36 | "2400:3200:baba::1",//阿里云 37 | 38 | 39 | 40 | ]; 41 | 42 | if (isset($input['i']) || isset($input['ip'])) { 43 | $ips = []; 44 | 45 | if (isset($input['i'])) { 46 | $ips[] = $input['i']; 47 | } 48 | 49 | if (isset($input['ip'])) { 50 | $ips[] = $input['ip']; 51 | } 52 | } 53 | 54 | foreach ($ips as $ip) { 55 | echo json_encode(IpLocation::getLocation($ip), JSON_UNESCAPED_UNICODE) . "\n"; 56 | } 57 | 58 | 59 | --------------------------------------------------------------------------------