├── .gitignore ├── go.mod ├── assets └── assets.go ├── go.sum ├── qqwry_test.go ├── client └── client.go ├── LICENSE ├── server └── server.go ├── README.md └── qqwry.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | .devcontainer/ 4 | assets/qqwry.dat 5 | assets/qqwry.ipdb -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xiaoqidun/qqwry 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/ipipdotnet/ipdb-go v1.3.3 7 | golang.org/x/text v0.31.0 8 | ) 9 | -------------------------------------------------------------------------------- /assets/assets.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import _ "embed" 4 | 5 | //go:embed qqwry.dat 6 | var QQWryDat []byte 7 | 8 | //go:embed qqwry.ipdb 9 | var QQWryIpdb []byte 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ipipdotnet/ipdb-go v1.3.3 h1:GLSAW9ypLUd6EF9QNK2Uhxew9Jzs4XMJ9gOZEFnJm7U= 2 | github.com/ipipdotnet/ipdb-go v1.3.3/go.mod h1:yZ+8puwe3R37a/3qRftXo40nZVQbxYDLqls9o5foexs= 3 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 4 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 5 | -------------------------------------------------------------------------------- /qqwry_test.go: -------------------------------------------------------------------------------- 1 | package qqwry 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func init() { 8 | if err := LoadFile("assets/qqwry.ipdb"); err != nil { 9 | panic(err) 10 | } 11 | } 12 | 13 | func TestQueryIP(t *testing.T) { 14 | queryIp := "119.29.29.29" 15 | location, err := QueryIP(queryIp) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | emptyVal := func(val string) string { 20 | if val != "" { 21 | return val 22 | } 23 | return "未知" 24 | } 25 | t.Logf("国家:%s,省份:%s,城市:%s,区县:%s,运营商:%s", 26 | emptyVal(location.Country), 27 | emptyVal(location.Province), 28 | emptyVal(location.City), 29 | emptyVal(location.District), 30 | emptyVal(location.ISP), 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/xiaoqidun/qqwry" 6 | "github.com/xiaoqidun/qqwry/assets" 7 | "os" 8 | ) 9 | 10 | func init() { 11 | qqwry.LoadData(assets.QQWryIpdb) 12 | } 13 | 14 | func main() { 15 | if len(os.Args) < 2 { 16 | return 17 | } 18 | queryIp := os.Args[1] 19 | location, err := qqwry.QueryIP(queryIp) 20 | if err != nil { 21 | fmt.Printf("错误:%v\n", err) 22 | return 23 | } 24 | emptyVal := func(val string) string { 25 | if val != "" { 26 | return val 27 | } 28 | return "未知" 29 | } 30 | fmt.Printf("国家:%s,省份:%s,城市:%s,区县:%s,运营商:%s\n", 31 | emptyVal(location.Country), 32 | emptyVal(location.Province), 33 | emptyVal(location.City), 34 | emptyVal(location.District), 35 | emptyVal(location.ISP), 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 xiaoqidun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "github.com/xiaoqidun/qqwry" 7 | "github.com/xiaoqidun/qqwry/assets" 8 | "net" 9 | "net/http" 10 | ) 11 | 12 | type resp struct { 13 | Data *qqwry.Location `json:"data"` 14 | Success bool `json:"success"` 15 | Message string `json:"message"` 16 | } 17 | 18 | func init() { 19 | qqwry.LoadData(assets.QQWryIpdb) 20 | } 21 | 22 | func main() { 23 | listen := flag.String("listen", "127.0.0.1:80", "http server listen addr") 24 | flag.Parse() 25 | http.HandleFunc("/ip/", IpAPI) 26 | if err := http.ListenAndServe(*listen, nil); err != nil { 27 | panic(err) 28 | } 29 | } 30 | 31 | func IpAPI(writer http.ResponseWriter, request *http.Request) { 32 | ip := request.URL.Path[4:] 33 | if ip == "" { 34 | ip, _, _ = net.SplitHostPort(request.RemoteAddr) 35 | } 36 | response := &resp{} 37 | location, err := qqwry.QueryIP(ip) 38 | if err != nil { 39 | response.Message = err.Error() 40 | } else { 41 | response.Data = location 42 | response.Success = true 43 | } 44 | b, _ := json.MarshalIndent(response, "", " ") 45 | _, _ = writer.Write(b) 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QQWry [![Go Reference](https://pkg.go.dev/badge/github.com/xiaoqidun/qqwry.svg)](https://pkg.go.dev/github.com/xiaoqidun/qqwry) 2 | 3 | Golang QQWry,高性能纯真IP查询库。 4 | 5 | # 使用须知 6 | 7 | 1. dat格式仅支持ipv4查询。 8 | 2. ipdb格式支持ipv4和ipv6查询。 9 | 10 | # 使用说明 11 | 12 | ```go 13 | package main 14 | 15 | import ( 16 | "fmt" 17 | "github.com/xiaoqidun/qqwry" 18 | ) 19 | 20 | func main() { 21 | // 从文件加载IP数据库 22 | if err := qqwry.LoadFile("qqwry.ipdb"); err != nil { 23 | panic(err) 24 | } 25 | // 从内存或缓存查询IP 26 | location, err := qqwry.QueryIP("119.29.29.29") 27 | if err != nil { 28 | fmt.Printf("错误:%v\n", err) 29 | return 30 | } 31 | fmt.Printf("国家:%s,省份:%s,城市:%s,区县:%s,运营商:%s\n", 32 | location.Country, 33 | location.Province, 34 | location.City, 35 | location.District, 36 | location.ISP, 37 | ) 38 | } 39 | ``` 40 | 41 | # IP数据库 42 | 43 | - DAT格式:[https://aite.xyz/share-file/qqwry/qqwry.dat](https://aite.xyz/share-file/qqwry/qqwry.dat) 44 | - IPDB格式:[https://aite.xyz/share-file/qqwry/qqwry.ipdb](https://aite.xyz/share-file/qqwry/qqwry.ipdb) 45 | 46 | # 编译说明 47 | 48 | 1. 下载IP数据库并放置于assets目录中。 49 | 2. client和server需要go1.16的内嵌资源特性。 50 | 3. 作为库使用,请直接引包,并不需要go1.16+才能编译。 51 | 52 | # 数据更新 53 | 54 | - 由于qqwry.dat缺乏更新,官方czdb格式又难以获得和分发,建议使用ipdb格式。 55 | - 这里的ipdb格式指metowolf提供的官方czdb格式转换而来的ipdb格式(纯真格式原版)。 56 | 57 | # 服务接口 58 | 59 | 1. 自行根据需要调整server下源码。 60 | 2. 可以通过-listen参数指定http服务地址。 61 | 3. json api:curl http://127.0.0.1/ip/119.29.29.29 62 | 63 | # 特别感谢 64 | 65 | - 感谢[纯真IP库](https://www.cz88.net/)一直坚持为大家提供免费IP数据库。 66 | - 感谢[yinheli](https://github.com/yinheli)的[qqwry](https://github.com/yinheli/qqwry)项目,为我提供纯真ip库解析算法参考。 67 | - 感谢[metowolf](https://github.com/metowolf)的[qqwry.ipdb](https://github.com/metowolf/qqwry.ipdb)项目,提供纯真czdb转ipdb数据库。 68 | 69 | # 授权说明 70 | 71 | 使用本类库你唯一需要做的就是把LICENSE文件往你用到的项目中拷贝一份。 -------------------------------------------------------------------------------- /qqwry.go: -------------------------------------------------------------------------------- 1 | package qqwry 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "github.com/ipipdotnet/ipdb-go" 8 | "golang.org/x/text/encoding/simplifiedchinese" 9 | "golang.org/x/text/transform" 10 | "io" 11 | "net" 12 | "os" 13 | "strings" 14 | "sync" 15 | ) 16 | 17 | var ( 18 | data []byte 19 | dataLen uint32 20 | ipdbCity *ipdb.City 21 | dataType = dataTypeDat 22 | locationCache = &sync.Map{} 23 | ) 24 | 25 | const ( 26 | dataTypeDat = 0 27 | dataTypeIpdb = 1 28 | ) 29 | 30 | const ( 31 | indexLen = 7 32 | redirectMode1 = 0x01 33 | redirectMode2 = 0x02 34 | ) 35 | 36 | type Location struct { 37 | Country string // 国家 38 | Province string // 省份 39 | City string // 城市 40 | District string // 区县 41 | ISP string // 运营商 42 | IP string // IP地址 43 | } 44 | 45 | func byte3ToUInt32(data []byte) uint32 { 46 | i := uint32(data[0]) & 0xff 47 | i |= (uint32(data[1]) << 8) & 0xff00 48 | i |= (uint32(data[2]) << 16) & 0xff0000 49 | return i 50 | } 51 | 52 | func gb18030Decode(src []byte) string { 53 | in := bytes.NewReader(src) 54 | out := transform.NewReader(in, simplifiedchinese.GB18030.NewDecoder()) 55 | d, _ := io.ReadAll(out) 56 | return string(d) 57 | } 58 | 59 | // QueryIP 从内存或缓存查询IP 60 | func QueryIP(ip string) (location *Location, err error) { 61 | if v, ok := locationCache.Load(ip); ok { 62 | return v.(*Location), nil 63 | } 64 | switch dataType { 65 | case dataTypeDat: 66 | return QueryIPDat(ip) 67 | case dataTypeIpdb: 68 | return QueryIPIpdb(ip) 69 | default: 70 | return nil, errors.New("data type not support") 71 | } 72 | } 73 | 74 | // QueryIPDat 从dat查询IP,仅加载dat格式数据库时使用 75 | func QueryIPDat(ipv4 string) (location *Location, err error) { 76 | ip := net.ParseIP(ipv4).To4() 77 | if ip == nil { 78 | return nil, errors.New("ip is not ipv4") 79 | } 80 | ip32 := binary.BigEndian.Uint32(ip) 81 | posA := binary.LittleEndian.Uint32(data[:4]) 82 | posZ := binary.LittleEndian.Uint32(data[4:8]) 83 | var offset uint32 = 0 84 | for { 85 | mid := posA + (((posZ-posA)/indexLen)>>1)*indexLen 86 | buf := data[mid : mid+indexLen] 87 | _ip := binary.LittleEndian.Uint32(buf[:4]) 88 | if posZ-posA == indexLen { 89 | offset = byte3ToUInt32(buf[4:]) 90 | buf = data[mid+indexLen : mid+indexLen+indexLen] 91 | if ip32 < binary.LittleEndian.Uint32(buf[:4]) { 92 | break 93 | } else { 94 | offset = 0 95 | break 96 | } 97 | } 98 | if _ip > ip32 { 99 | posZ = mid 100 | } else if _ip < ip32 { 101 | posA = mid 102 | } else if _ip == ip32 { 103 | offset = byte3ToUInt32(buf[4:]) 104 | break 105 | } 106 | } 107 | if offset <= 0 { 108 | return nil, errors.New("ip not found") 109 | } 110 | posM := offset + 4 111 | mode := data[posM] 112 | var ispPos uint32 113 | var addr, isp string 114 | switch mode { 115 | case redirectMode1: 116 | posC := byte3ToUInt32(data[posM+1 : posM+4]) 117 | mode = data[posC] 118 | posCA := posC 119 | if mode == redirectMode2 { 120 | posCA = byte3ToUInt32(data[posC+1 : posC+4]) 121 | posC += 4 122 | } 123 | for i := posCA; i < dataLen; i++ { 124 | if data[i] == 0 { 125 | addr = string(data[posCA:i]) 126 | break 127 | } 128 | } 129 | if mode != redirectMode2 { 130 | posC += uint32(len(addr) + 1) 131 | } 132 | ispPos = posC 133 | case redirectMode2: 134 | posCA := byte3ToUInt32(data[posM+1 : posM+4]) 135 | for i := posCA; i < dataLen; i++ { 136 | if data[i] == 0 { 137 | addr = string(data[posCA:i]) 138 | break 139 | } 140 | } 141 | ispPos = offset + 8 142 | default: 143 | posCA := offset + 4 144 | for i := posCA; i < dataLen; i++ { 145 | if data[i] == 0 { 146 | addr = string(data[posCA:i]) 147 | break 148 | } 149 | } 150 | ispPos = offset + uint32(5+len(addr)) 151 | } 152 | if addr != "" { 153 | addr = strings.TrimSpace(gb18030Decode([]byte(addr))) 154 | } 155 | ispMode := data[ispPos] 156 | if ispMode == redirectMode1 || ispMode == redirectMode2 { 157 | ispPos = byte3ToUInt32(data[ispPos+1 : ispPos+4]) 158 | } 159 | if ispPos > 0 { 160 | for i := ispPos; i < dataLen; i++ { 161 | if data[i] == 0 { 162 | isp = string(data[ispPos:i]) 163 | if isp != "" { 164 | if strings.Contains(isp, "CZ88.NET") { 165 | isp = "" 166 | } else { 167 | isp = strings.TrimSpace(gb18030Decode([]byte(isp))) 168 | } 169 | } 170 | break 171 | } 172 | } 173 | } 174 | location = SplitResult(addr, isp, ipv4) 175 | locationCache.Store(ipv4, location) 176 | return location, nil 177 | } 178 | 179 | // QueryIPIpdb 从ipdb查询IP,仅加载ipdb格式数据库时使用 180 | func QueryIPIpdb(ip string) (location *Location, err error) { 181 | ret, err := ipdbCity.Find(ip, "CN") 182 | if err != nil { 183 | return 184 | } 185 | location = SplitResult(ret[0], ret[1], ip) 186 | locationCache.Store(ip, location) 187 | return location, nil 188 | } 189 | 190 | // LoadData 从内存加载IP数据库 191 | func LoadData(database []byte) { 192 | if string(database[6:11]) == "build" { 193 | dataType = dataTypeIpdb 194 | loadCity, err := ipdb.NewCityFromBytes(database) 195 | if err != nil { 196 | panic(err) 197 | } 198 | ipdbCity = loadCity 199 | return 200 | } 201 | data = database 202 | dataLen = uint32(len(data)) 203 | } 204 | 205 | // LoadFile 从文件加载IP数据库 206 | func LoadFile(filepath string) (err error) { 207 | body, err := os.ReadFile(filepath) 208 | if err != nil { 209 | return 210 | } 211 | LoadData(body) 212 | return 213 | } 214 | 215 | // SplitResult 按照调整后的纯真社区版IP库地理位置格式返回结果 216 | func SplitResult(addr string, isp string, ipv4 string) (location *Location) { 217 | location = &Location{ISP: isp, IP: ipv4} 218 | splitList := strings.Split(addr, "–") 219 | for i := 0; i < len(splitList); i++ { 220 | switch i { 221 | case 0: 222 | location.Country = splitList[i] 223 | case 1: 224 | location.Province = splitList[i] 225 | case 2: 226 | location.City = splitList[i] 227 | case 3: 228 | location.District = splitList[i] 229 | } 230 | } 231 | if location.Country == "局域网" { 232 | location.ISP = location.Country 233 | } 234 | return 235 | } 236 | --------------------------------------------------------------------------------