Compare commits

...

37 Commits

Author SHA1 Message Date
cc141ac4f5 feat(更新依赖): 更新依赖 2025-11-27 16:55:33 +08:00
1dd385f77d feat(升级依赖): 升级依赖 2025-09-15 19:03:12 +08:00
20c61b7efd feat(升级依赖): 升级依赖 2025-07-11 09:37:19 +08:00
8b26e3b115 feat(升级依赖): 升级依赖 2025-05-14 00:06:06 +08:00
cbdd413fc4 feat(升级依赖): 升级依赖 2025-04-08 08:59:50 +08:00
9392bc022a feat(升级依赖): 升级依赖 2025-03-06 19:39:39 +08:00
cf49741a97 feat(升级依赖): 升级依赖 2025-02-06 13:02:15 +08:00
7a1610fa02 docs(更新文档): 更新文档 2025-01-07 16:35:38 +08:00
90b4bdcfc3 fix(优化查询): 统一局域网IP地址的ISP字段 2025-01-07 16:15:28 +08:00
149532dea2 fix(优化查询): 统一局域网IP地址的ISP字段 2025-01-07 16:13:13 +08:00
345094d135 docs(更新文档): 更新文档 2025-01-07 16:03:29 +08:00
13ade58f06 feat(更新功能): 兼容metowolf提供的qqwry.ipdb格式(原版) 2025-01-07 15:52:22 +08:00
672ffb4f51 feat(升级依赖): 更新依赖 2024-12-05 10:14:38 +08:00
c60cca6bd2 feat(更新依赖): 更新依赖 2024-11-20 20:15:17 +08:00
c3e92bf2ea feat(更新依赖): 更新依赖 2024-10-09 10:47:34 +08:00
c666cbda28 feat(更新依赖): 更新依赖 2024-09-19 23:26:40 +08:00
698a12cf03 style(代码风格): 调整代码风格 2024-06-20 09:45:00 +08:00
25daf60573 feat(适配特性): 适配纯真IP库社区版更新(2024-06-19)地理位置格式 2024-06-20 00:38:19 +08:00
26b587798e feat(升级依赖): 升级依赖 2024-06-05 20:57:12 +08:00
79da17e7d1 feat(更新依赖): 更新依赖 2024-05-07 15:22:36 +08:00
1b27c45e2a feat(更新依赖): 更新依赖 2023-11-14 14:36:55 +08:00
b4bbcc98b2 feat(更新依赖): 更新依赖 2023-09-29 18:14:16 +08:00
01491ac5e0 feat(升级依赖): 升级依赖并去掉废弃函数 2023-04-07 15:32:15 +08:00
afe166ae6e feat(更新依赖): 更新依赖 2023-03-04 14:54:37 +08:00
6dac40e195 feat(升级依赖): 升级依赖 2023-02-05 23:57:02 +08:00
37b8c3e8ed feat(升级依赖): 升级依赖 2022-03-01 14:26:45 +08:00
630b82cca6 fix(更新依赖): 更新依赖 2021-09-28 22:01:16 +08:00
c86de58d02 fix(更新依赖): 更新依赖 2021-07-08 11:51:33 +08:00
776b126944 docs(更新文档): 更新文档 2021-07-08 11:44:05 +08:00
f37264f5ab fix(更改区域): 区域更改为运营商 2021-07-08 11:39:43 +08:00
ccbae8531d feat(查询优化): 查询优化 2021-04-09 13:19:47 +08:00
0865c44d5e feat(查询优化): 剔除无效区域 2021-04-09 13:08:23 +08:00
35d03559c6 feat(查询优化): 剔除返回结果首尾空白字符 2021-04-09 12:46:25 +08:00
5303137428 docs(更新文档): 更新文档 2021-01-23 15:29:26 +08:00
841b08cf06 docs(更新文档): 更新文档 2021-01-23 15:28:40 +08:00
88e9c989f5 feat(添加功能): client作为本地查询工具,server作为服务给其他服务调用 2021-01-23 15:26:04 +08:00
47ef8058d0 docs(更新文档): 更新文档 2021-01-23 11:41:14 +08:00
9 changed files with 270 additions and 55 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.idea/ .idea/
.vscode/ .vscode/
.devcontainer/ .devcontainer/
qqwry.dat assets/qqwry.dat
assets/qqwry.ipdb

View File

@@ -4,9 +4,8 @@ Golang QQWry高性能纯真IP查询库。
# 使用须知 # 使用须知
1. 仅支持ipv4查询。 1. dat格式仅支持ipv4查询。
2. city可能是城市也可能是国家 2. ipdb格式支持ipv4和ipv6查询
3. area可能是区域也可能是运营商。
# 使用说明 # 使用说明
@@ -14,25 +13,58 @@ Golang QQWry高性能纯真IP查询库。
package main package main
import ( import (
"fmt"
"github.com/xiaoqidun/qqwry" "github.com/xiaoqidun/qqwry"
"log"
) )
func main() { func main() {
// 从文件加载IP数据库 // 从文件加载IP数据库
if err := qqwry.LoadFile("qqwry.dat"); err != nil { if err := qqwry.LoadFile("qqwry.ipdb"); err != nil {
panic(err) panic(err)
} }
// 从内存或缓存查询IP // 从内存或缓存查询IP
city, area, err := qqwry.QueryIP("1.1.1.1") location, err := qqwry.QueryIP("119.29.29.29")
log.Printf("城市:%s区域%s错误%v", city, area, err) if err != nil {
fmt.Printf("错误:%v\n", err)
return
}
fmt.Printf("国家:%s省份%s城市%s区县%s运营商%s\n",
location.Country,
location.Province,
location.City,
location.District,
location.ISP,
)
} }
``` ```
# IP数据库
- DAT格式[https://aite.xyz/share-file/qqwry/qqwry.dat](https://aite.xyz/share-file/qqwry/qqwry.dat)
- IPDB格式[https://aite.xyz/share-file/qqwry/qqwry.ipdb](https://aite.xyz/share-file/qqwry/qqwry.ipdb)
# 编译说明
1. 下载IP数据库并放置于assets目录中。
2. client和server需要go1.16的内嵌资源特性。
3. 作为库使用请直接引包并不需要go1.16+才能编译。
# 数据更新
- 由于qqwry.dat缺乏更新官方czdb格式又难以获得和分发建议使用ipdb格式。
- 这里的ipdb格式指metowolf提供的官方czdb格式转换而来的ipdb格式纯真格式原版
# 服务接口
1. 自行根据需要调整server下源码。
2. 可以通过-listen参数指定http服务地址。
3. json apicurl http://127.0.0.1/ip/119.29.29.29
# 特别感谢 # 特别感谢
- 感谢[纯真IP库](https://www.cz88.net/)一直坚持为大家提供免费IP数据库。 - 感谢[纯真IP库](https://www.cz88.net/)一直坚持为大家提供免费IP数据库。
- 感谢[yinheli](https://github.com/yinheli)的[qqwry](https://github.com/yinheli/qqwry)项目为我提供纯真ip库解析算法参考。 - 感谢[yinheli](https://github.com/yinheli)的[qqwry](https://github.com/yinheli/qqwry)项目为我提供纯真ip库解析算法参考。
- 感谢[metowolf](https://github.com/metowolf)的[qqwry.ipdb](https://github.com/metowolf/qqwry.ipdb)项目提供纯真czdb转ipdb数据库。
# 授权说明 # 授权说明

9
assets/assets.go Normal file
View File

@@ -0,0 +1,9 @@
package assets
import _ "embed"
//go:embed qqwry.dat
var QQWryDat []byte
//go:embed qqwry.ipdb
var QQWryIpdb []byte

37
client/client.go Normal file
View File

@@ -0,0 +1,37 @@
package main
import (
"fmt"
"github.com/xiaoqidun/qqwry"
"github.com/xiaoqidun/qqwry/assets"
"os"
)
func init() {
qqwry.LoadData(assets.QQWryIpdb)
}
func main() {
if len(os.Args) < 2 {
return
}
queryIp := os.Args[1]
location, err := qqwry.QueryIP(queryIp)
if err != nil {
fmt.Printf("错误:%v\n", err)
return
}
emptyVal := func(val string) string {
if val != "" {
return val
}
return "未知"
}
fmt.Printf("国家:%s省份%s城市%s区县%s运营商%s\n",
emptyVal(location.Country),
emptyVal(location.Province),
emptyVal(location.City),
emptyVal(location.District),
emptyVal(location.ISP),
)
}

7
go.mod
View File

@@ -1,5 +1,8 @@
module github.com/xiaoqidun/qqwry module github.com/xiaoqidun/qqwry
go 1.16 go 1.20
require golang.org/x/text v0.3.5 require (
github.com/ipipdotnet/ipdb-go v1.3.3
golang.org/x/text v0.31.0
)

7
go.sum
View File

@@ -1,3 +1,4 @@
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= github.com/ipipdotnet/ipdb-go v1.3.3 h1:GLSAW9ypLUd6EF9QNK2Uhxew9Jzs4XMJ9gOZEFnJm7U=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= github.com/ipipdotnet/ipdb-go v1.3.3/go.mod h1:yZ+8puwe3R37a/3qRftXo40nZVQbxYDLqls9o5foexs=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=

150
qqwry.go
View File

@@ -4,17 +4,27 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"errors" "errors"
"github.com/ipipdotnet/ipdb-go"
"golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform" "golang.org/x/text/transform"
"io/ioutil" "io"
"net" "net"
"os"
"strings"
"sync" "sync"
) )
var ( var (
data []byte data []byte
dataLen uint32 dataLen uint32
ipCache = &sync.Map{} ipdbCity *ipdb.City
dataType = dataTypeDat
locationCache = &sync.Map{}
)
const (
dataTypeDat = 0
dataTypeIpdb = 1
) )
const ( const (
@@ -23,9 +33,13 @@ const (
redirectMode2 = 0x02 redirectMode2 = 0x02
) )
type cache struct { type Location struct {
City string Country string // 国家
Area string Province string // 省份
City string // 城市
District string // 区县
ISP string // 运营商
IP string // IP地址
} }
func byte3ToUInt32(data []byte) uint32 { func byte3ToUInt32(data []byte) uint32 {
@@ -38,21 +52,30 @@ func byte3ToUInt32(data []byte) uint32 {
func gb18030Decode(src []byte) string { func gb18030Decode(src []byte) string {
in := bytes.NewReader(src) in := bytes.NewReader(src)
out := transform.NewReader(in, simplifiedchinese.GB18030.NewDecoder()) out := transform.NewReader(in, simplifiedchinese.GB18030.NewDecoder())
d, _ := ioutil.ReadAll(out) d, _ := io.ReadAll(out)
return string(d) return string(d)
} }
// QueryIP 从内存或缓存查询IP // QueryIP 从内存或缓存查询IP
func QueryIP(queryIp string) (city string, area string, err error) { func QueryIP(ip string) (location *Location, err error) {
if v, ok := ipCache.Load(queryIp); ok { if v, ok := locationCache.Load(ip); ok {
city = v.(cache).City return v.(*Location), nil
area = v.(cache).Area
return
} }
ip := net.ParseIP(queryIp).To4() switch dataType {
case dataTypeDat:
return QueryIPDat(ip)
case dataTypeIpdb:
return QueryIPIpdb(ip)
default:
return nil, errors.New("data type not support")
}
}
// QueryIPDat 从dat查询IP仅加载dat格式数据库时使用
func QueryIPDat(ipv4 string) (location *Location, err error) {
ip := net.ParseIP(ipv4).To4()
if ip == nil { if ip == nil {
err = errors.New("ip is not ipv4") return nil, errors.New("ip is not ipv4")
return
} }
ip32 := binary.BigEndian.Uint32(ip) ip32 := binary.BigEndian.Uint32(ip)
posA := binary.LittleEndian.Uint32(data[:4]) posA := binary.LittleEndian.Uint32(data[:4])
@@ -82,12 +105,12 @@ func QueryIP(queryIp string) (city string, area string, err error) {
} }
} }
if offset <= 0 { if offset <= 0 {
err = errors.New("ip not found") return nil, errors.New("ip not found")
return
} }
posM := offset + 4 posM := offset + 4
mode := data[posM] mode := data[posM]
var areaPos uint32 var ispPos uint32
var addr, isp string
switch mode { switch mode {
case redirectMode1: case redirectMode1:
posC := byte3ToUInt32(data[posM+1 : posM+4]) posC := byte3ToUInt32(data[posM+1 : posM+4])
@@ -99,63 +122,114 @@ func QueryIP(queryIp string) (city string, area string, err error) {
} }
for i := posCA; i < dataLen; i++ { for i := posCA; i < dataLen; i++ {
if data[i] == 0 { if data[i] == 0 {
city = string(data[posCA:i]) addr = string(data[posCA:i])
break break
} }
} }
if mode != redirectMode2 { if mode != redirectMode2 {
posC += uint32(len(city) + 1) posC += uint32(len(addr) + 1)
} }
areaPos = posC ispPos = posC
case redirectMode2: case redirectMode2:
posCA := byte3ToUInt32(data[posM+1 : posM+4]) posCA := byte3ToUInt32(data[posM+1 : posM+4])
for i := posCA; i < dataLen; i++ { for i := posCA; i < dataLen; i++ {
if data[i] == 0 { if data[i] == 0 {
city = string(data[posCA:i]) addr = string(data[posCA:i])
break break
} }
} }
areaPos = offset + 8 ispPos = offset + 8
default: default:
posCA := offset + 4 posCA := offset + 4
for i := posCA; i < dataLen; i++ { for i := posCA; i < dataLen; i++ {
if data[i] == 0 { if data[i] == 0 {
city = string(data[posCA:i]) addr = string(data[posCA:i])
break break
} }
} }
areaPos = offset + uint32(5+len(city)) ispPos = offset + uint32(5+len(addr))
} }
areaMode := data[areaPos] if addr != "" {
if areaMode == redirectMode1 || areaMode == redirectMode2 { addr = strings.TrimSpace(gb18030Decode([]byte(addr)))
areaPos = byte3ToUInt32(data[areaPos+1 : areaPos+4])
} }
if areaPos > 0 { ispMode := data[ispPos]
for i := areaPos; i < dataLen; i++ { if ispMode == redirectMode1 || ispMode == redirectMode2 {
ispPos = byte3ToUInt32(data[ispPos+1 : ispPos+4])
}
if ispPos > 0 {
for i := ispPos; i < dataLen; i++ {
if data[i] == 0 { if data[i] == 0 {
area = string(data[areaPos:i]) isp = string(data[ispPos:i])
if isp != "" {
if strings.Contains(isp, "CZ88.NET") {
isp = ""
} else {
isp = strings.TrimSpace(gb18030Decode([]byte(isp)))
}
}
break break
} }
} }
} }
city = gb18030Decode([]byte(city)) location = SplitResult(addr, isp, ipv4)
area = gb18030Decode([]byte(area)) locationCache.Store(ipv4, location)
ipCache.Store(queryIp, cache{City: city, Area: area}) return location, nil
return }
// QueryIPIpdb 从ipdb查询IP仅加载ipdb格式数据库时使用
func QueryIPIpdb(ip string) (location *Location, err error) {
ret, err := ipdbCity.Find(ip, "CN")
if err != nil {
return
}
location = SplitResult(ret[0], ret[1], ip)
locationCache.Store(ip, location)
return location, nil
} }
// LoadData 从内存加载IP数据库 // LoadData 从内存加载IP数据库
func LoadData(database []byte) { func LoadData(database []byte) {
if string(database[6:11]) == "build" {
dataType = dataTypeIpdb
loadCity, err := ipdb.NewCityFromBytes(database)
if err != nil {
panic(err)
}
ipdbCity = loadCity
return
}
data = database data = database
dataLen = uint32(len(data)) dataLen = uint32(len(data))
} }
// LoadFile 从文件加载IP数据库 // LoadFile 从文件加载IP数据库
func LoadFile(filepath string) (err error) { func LoadFile(filepath string) (err error) {
data, err = ioutil.ReadFile(filepath) body, err := os.ReadFile(filepath)
if err != nil { if err != nil {
return return
} }
dataLen = uint32(len(data)) LoadData(body)
return
}
// SplitResult 按照调整后的纯真社区版IP库地理位置格式返回结果
func SplitResult(addr string, isp string, ipv4 string) (location *Location) {
location = &Location{ISP: isp, IP: ipv4}
splitList := strings.Split(addr, "")
for i := 0; i < len(splitList); i++ {
switch i {
case 0:
location.Country = splitList[i]
case 1:
location.Province = splitList[i]
case 2:
location.City = splitList[i]
case 3:
location.District = splitList[i]
}
}
if location.Country == "局域网" {
location.ISP = location.Country
}
return return
} }

View File

@@ -5,16 +5,28 @@ import (
) )
func init() { func init() {
if err := LoadFile("qqwry.dat"); err != nil { if err := LoadFile("assets/qqwry.ipdb"); err != nil {
panic(err) panic(err)
} }
} }
func TestQueryIP(t *testing.T) { func TestQueryIP(t *testing.T) {
queryIp := "1.1.1.1" queryIp := "119.29.29.29"
city, area, err := QueryIP(queryIp) location, err := QueryIP(queryIp)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
t.Logf("城市:%s区域%s", city, area) emptyVal := func(val string) string {
if val != "" {
return val
}
return "未知"
}
t.Logf("国家:%s省份%s城市%s区县%s运营商%s",
emptyVal(location.Country),
emptyVal(location.Province),
emptyVal(location.City),
emptyVal(location.District),
emptyVal(location.ISP),
)
} }

46
server/server.go Normal file
View File

@@ -0,0 +1,46 @@
package main
import (
"encoding/json"
"flag"
"github.com/xiaoqidun/qqwry"
"github.com/xiaoqidun/qqwry/assets"
"net"
"net/http"
)
type resp struct {
Data *qqwry.Location `json:"data"`
Success bool `json:"success"`
Message string `json:"message"`
}
func init() {
qqwry.LoadData(assets.QQWryIpdb)
}
func main() {
listen := flag.String("listen", "127.0.0.1:80", "http server listen addr")
flag.Parse()
http.HandleFunc("/ip/", IpAPI)
if err := http.ListenAndServe(*listen, nil); err != nil {
panic(err)
}
}
func IpAPI(writer http.ResponseWriter, request *http.Request) {
ip := request.URL.Path[4:]
if ip == "" {
ip, _, _ = net.SplitHostPort(request.RemoteAddr)
}
response := &resp{}
location, err := qqwry.QueryIP(ip)
if err != nil {
response.Message = err.Error()
} else {
response.Data = location
response.Success = true
}
b, _ := json.MarshalIndent(response, "", " ")
_, _ = writer.Write(b)
}