├── .gitignore ├── .travis.yml ├── Makefile ├── resource.go ├── iploc_test.go ├── LICENSE ├── ip.go ├── cmd ├── iploc-gen │ └── iploc-gen.go ├── iploc-fetch │ └── iploc-fetch.go └── iploc-conv │ └── iploc-conv.go ├── indexes.go ├── detail.go ├── README.md ├── iploc.go └── parser.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.9 4 | install: 5 | - go get -u github.com/spf13/pflag 6 | - go get -u golang.org/x/text/transform 7 | - go get -u golang.org/x/text/encoding/simplifiedchinese 8 | - go get -u github.com/google/btree 9 | - make install 10 | script: 11 | - make clean -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | IPLOC_CONV = $(GOPATH)/bin/iploc-conv 2 | IPLOC_FETCH := $(GOPATH)/bin/iploc-fetch 3 | IPLOC_GEN = $(GOPATH)/bin/iploc-gen 4 | 5 | 6 | .PHONY: all test clean install prepare 7 | 8 | all: install prepare test 9 | 10 | clean: 11 | rm -f $(IPLOC_CONV) $(IPLOC_FETCH) $(IPLOC_GEN) 12 | rm -f qqwry.dat 13 | 14 | install: 15 | cd cmd/iploc-conv; go install 16 | cd cmd/iploc-fetch; go install 17 | cd cmd/iploc-gen; go install 18 | 19 | prepare: 20 | iploc-fetch ./qqwry.gbk.dat -q 21 | iploc-conv -s qqwry.gbk.dat -d qqwry.dat -n -q 22 | rm qqwry.gbk.dat 23 | 24 | test: 25 | go test -v 26 | 27 | bench: 28 | go test -bench=. -------------------------------------------------------------------------------- /resource.go: -------------------------------------------------------------------------------- 1 | package iploc 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | type resReadCloser interface { 9 | Read(b []byte) (n int, err error) 10 | ReadAt(b []byte, off int64) (n int, err error) 11 | Close() error 12 | } 13 | 14 | type resource struct { 15 | data []byte 16 | seek int64 17 | } 18 | 19 | func (res *resource) Read(b []byte) (n int, err error) { 20 | if len(b) == 0 { 21 | return 22 | } 23 | if max := len(res.data); len(b) > max { 24 | b = b[:max] 25 | } 26 | n, err = res.ReadAt(b, res.seek) 27 | res.seek += int64(n) 28 | return 29 | } 30 | 31 | func (res *resource) ReadAt(b []byte, off int64) (n int, err error) { 32 | if off < 0 { 33 | return 0, fmt.Errorf("negative offset: %d", off) 34 | } else if len(b) == 0 || off >= int64(len(res.data)) { 35 | return 0, nil 36 | } 37 | copy(b, res.data[off:]) 38 | if int64(len(b))+off > int64(len(res.data)) { 39 | return int(int64(len(res.data)) - off), io.EOF 40 | } 41 | return len(b), nil 42 | } 43 | 44 | func (res *resource) Close() error { 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /iploc_test.go: -------------------------------------------------------------------------------- 1 | package iploc 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | const DATPath = "qqwry.dat" 10 | 11 | func TestFind(t *testing.T) { 12 | loc, err := Open(DATPath) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | 17 | detail1 := loc.Find("4.4") 18 | detail2, _ := Find(DATPath, "4.0.0.4") 19 | 20 | if detail1.Start.Compare(detail2.Start) != 0 || detail1.End.Compare(detail2.End) != 0 || detail1.String() != detail2.String() { 21 | t.Fatal("error") 22 | } 23 | } 24 | 25 | func BenchmarkFind(b *testing.B) { 26 | b.StopTimer() 27 | loc, err := Open(DATPath) 28 | if err != nil { 29 | b.Fatal(err) 30 | } 31 | rand.Seed(time.Now().UnixNano()) 32 | b.StartTimer() 33 | for i := 0; i < b.N; i++ { 34 | loc.FindUint(rand.Uint32()) 35 | } 36 | } 37 | 38 | func BenchmarkFindUnIndexed(b *testing.B) { 39 | b.StopTimer() 40 | loc, err := OpenWithoutIndexes(DATPath) 41 | if err != nil { 42 | b.Fatal(err) 43 | } 44 | rand.Seed(time.Now().UnixNano()) 45 | b.StartTimer() 46 | for i := 0; i < b.N; i++ { 47 | loc.FindUint(rand.Uint32()) 48 | } 49 | } 50 | 51 | func BenchmarkFindParallel(b *testing.B) { 52 | b.StopTimer() 53 | loc, err := Open(DATPath) 54 | if err != nil { 55 | b.Fatal(err) 56 | } 57 | rand.Seed(time.Now().UnixNano()) 58 | b.StartTimer() 59 | b.RunParallel(func(pb *testing.PB) { 60 | for pb.Next() { 61 | if loc.FindUint(rand.Uint32()) == nil { 62 | b.Fatal("error") 63 | } 64 | } 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Kayon 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /ip.go: -------------------------------------------------------------------------------- 1 | package iploc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | IPLen = 4 13 | ) 14 | 15 | type IP [IPLen]byte 16 | 17 | func (ip IP) Bytes() []byte { 18 | return ip[:] 19 | } 20 | 21 | func (ip IP) ReverseBytes() []byte { 22 | var b [4]byte 23 | copy(b[:], ip[:]) 24 | for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 { 25 | b[i], b[j] = b[j], b[i] 26 | } 27 | return b[:] 28 | } 29 | 30 | func (ip IP) String() string { 31 | return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]) 32 | } 33 | 34 | func (ip IP) Uint() uint32 { 35 | return binary.BigEndian.Uint32(ip[:]) 36 | } 37 | 38 | // Compare like bytes.Compare 39 | // The result will be 0 if == a, -1 if < a, and +1 if > a. 40 | func (ip IP) Compare(a IP) int { 41 | return bytes.Compare(ip[:], a[:]) 42 | } 43 | 44 | func ParseIP(s string) (ip IP, err error) { 45 | var b = make([]byte, 0, IPLen) 46 | var d uint64 47 | for i, v := range strings.Split(s, ".") { 48 | if d, err = strconv.ParseUint(v, 10, 8); err != nil { 49 | err = fmt.Errorf("invalid IP address %s", s) 50 | return 51 | } 52 | b = append(b, byte(d)) 53 | if i == 3 { 54 | break 55 | } 56 | } 57 | if len(b) == 0 { 58 | err = fmt.Errorf("invalid IP address %s", s) 59 | return 60 | } 61 | 62 | // filling,e.g. 127.1 -> 127.0.0.1 63 | // copy to array, right padding 64 | copy(ip[:], b) 65 | if padding := IPLen - len(b); padding > 0 { 66 | // left padding 67 | if lastIndex := len(b) - 1; b[lastIndex] > 0 { 68 | ip[lastIndex], ip[3] = 0, ip[lastIndex] 69 | } 70 | } 71 | return 72 | } 73 | 74 | func ParseBytesIP(b []byte) (ip IP) { 75 | copy(ip[:], b) 76 | return 77 | } 78 | 79 | func ParseUintIP(u uint32) (ip IP) { 80 | binary.BigEndian.PutUint32(ip[:], u) 81 | return 82 | } 83 | -------------------------------------------------------------------------------- /cmd/iploc-gen/iploc-gen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "fmt" 7 | "io" 8 | "os" 9 | "strconv" 10 | "text/template" 11 | 12 | "github.com/kayon/iploc" 13 | flag "github.com/spf13/pflag" 14 | ) 15 | 16 | const ( 17 | goFilename = "iploc-binary.go" 18 | goFileTPL = `// generated by iploc-gen 19 | package {{ .Package }} 20 | 21 | import ( 22 | "github.com/kayon/iploc" 23 | ) 24 | 25 | var IPLoc *iploc.Locator 26 | 27 | func init() { 28 | var err error 29 | if IPLoc, err = iploc.{{ .Method }}([]byte({{ .Dat }})); err != nil { 30 | panic(err) 31 | } 32 | }` 33 | ) 34 | 35 | var ( 36 | qqwrySrc string 37 | pkgName string 38 | noIndex bool 39 | help bool 40 | file = template.Must(template.New("file").Parse(goFileTPL)) 41 | ) 42 | 43 | func init() { 44 | flag.StringVarP(&pkgName, "pkg", "p", "main", "package name") 45 | flag.BoolVarP(&noIndex, "no-index", "n", false, "initialization with OpenWithoutIndexes") 46 | flag.BoolVarP(&help, "help", "h", false, "this help") 47 | flag.Usage = func() { 48 | fmt.Fprintf(os.Stderr, "iploc-gen: convert iploc and DAT into Go source code.\nUsage: iploc-gen [DAT path] [arguments]\nOptions:\n") 49 | flag.PrintDefaults() 50 | } 51 | flag.Parse() 52 | qqwrySrc = flag.Arg(0) 53 | 54 | if qqwrySrc == "" || help { 55 | flag.Usage() 56 | if help { 57 | os.Exit(0) 58 | } 59 | os.Exit(1) 60 | } 61 | } 62 | 63 | type Content struct { 64 | Dat string 65 | } 66 | 67 | func main() { 68 | p, err := iploc.NewParser(qqwrySrc, true) 69 | if err != nil { 70 | fmt.Fprintf(os.Stderr, err.Error()) 71 | os.Exit(1) 72 | } 73 | 74 | f, err := os.Create(goFilename) 75 | if err != nil { 76 | fmt.Fprintf(os.Stderr, err.Error()) 77 | os.Exit(1) 78 | } 79 | defer f.Close() 80 | 81 | buffer := bytes.NewBuffer(nil) 82 | cw, _ := zlib.NewWriterLevel(buffer, zlib.BestCompression) 83 | _, err = io.Copy(cw, p.Reader()) 84 | cw.Close() 85 | 86 | if err != nil { 87 | fmt.Fprintf(os.Stderr, err.Error()) 88 | os.Exit(1) 89 | } 90 | 91 | var method = "Load" 92 | if noIndex { 93 | method = "LoadWithoutIndexes" 94 | } 95 | 96 | file.Execute(f, map[string]string{"Package": pkgName, "Dat": strconv.QuoteToASCII(buffer.String()), "Method": method}) 97 | } 98 | -------------------------------------------------------------------------------- /indexes.go: -------------------------------------------------------------------------------- 1 | package iploc 2 | 3 | import ( 4 | "encoding/gob" 5 | 6 | "github.com/google/btree" 7 | ) 8 | 9 | type dataItem = btree.Item 10 | 11 | // 索引时 12 | // 0: ip start 13 | // 1: ip end 14 | // 2: country position 15 | // 3: region position 16 | // 无索引时 17 | // 2: ip end position 18 | // 3: n/a 19 | type indexItem [4]uint32 20 | 21 | func (i indexItem) Less(than btree.Item) bool { 22 | // 默认降序 1:End < 0:Start 23 | if v, ok := than.(indexItem); ok { 24 | return i[1] < v[0] 25 | 26 | } 27 | return i[1] < than.(indexItemAscend)[0] 28 | } 29 | 30 | // indexItem 升序 31 | type indexItemAscend [4]uint32 32 | 33 | func (i indexItemAscend) Less(than btree.Item) bool { 34 | // 升序 0:End < 0:Start 35 | if v, ok := than.(indexItem); ok { 36 | return i[0] < v[0] 37 | 38 | } 39 | return i[0] < than.(indexItemAscend)[0] 40 | } 41 | 42 | type indexes struct { 43 | index *btree.BTree 44 | indexMid indexItem 45 | locations map[uint32][]byte 46 | } 47 | 48 | func (idx *indexes) indexOf(u uint32) (hit indexItem) { 49 | // 对比中间值决定高低顺序,提升查询速度 50 | if u > idx.indexMid[1] { 51 | idx.index.DescendLessOrEqual(indexItem{0, u}, func(i btree.Item) bool { 52 | hit = i.(indexItem) 53 | return false 54 | }) 55 | } else if u < idx.indexMid[0] { 56 | idx.index.AscendGreaterOrEqual(indexItemAscend{u}, func(i btree.Item) bool { 57 | hit = i.(indexItem) 58 | return false 59 | }) 60 | } else { 61 | hit = idx.indexMid 62 | } 63 | return 64 | } 65 | 66 | func (idx *indexes) getLocation(i, j uint32) Location { 67 | return parseLocation(idx.locations[i], idx.locations[j]) 68 | } 69 | 70 | func newIndexes(p *Parser) *indexes { 71 | idx := &indexes{ 72 | index: btree.New(10), 73 | } 74 | idx.locations = make(map[uint32][]byte) 75 | 76 | var ( 77 | item indexItem 78 | raw LocationRaw 79 | mid = int(p.Count()) >> 1 80 | has bool 81 | ) 82 | 83 | p.IndexRange(func(i int, start, end, pos uint32) bool { 84 | item = indexItem{start, end, pos} 85 | raw = p.ReadLocationRaw(int64(pos)) 86 | if raw.Text[0] != nil { 87 | if _, has = idx.locations[raw.Pos[0]]; !has { 88 | idx.locations[raw.Pos[0]] = raw.Text[0] 89 | } 90 | } 91 | if raw.Text[1] != nil { 92 | if _, has = idx.locations[raw.Pos[1]]; !has { 93 | idx.locations[raw.Pos[1]] = raw.Text[1] 94 | } 95 | } 96 | item[2] = raw.Pos[0] 97 | item[3] = raw.Pos[1] 98 | if i == mid { 99 | idx.indexMid = item 100 | } 101 | idx.index.ReplaceOrInsert(item) 102 | return true 103 | }) 104 | return idx 105 | } 106 | 107 | func init() { 108 | gob.Register([][4]uint32{}) 109 | gob.Register(map[uint32][]byte{}) 110 | } 111 | -------------------------------------------------------------------------------- /detail.go: -------------------------------------------------------------------------------- 1 | package iploc 2 | 3 | var provincePrefix = map[int32]map[int32]int{'北': {'京': 0}, '天': {'津': 0}, '河': {'北': 0, '南': 0}, '山': {'东': 0, '西': 0}, '内': {'蒙': 1}, '辽': {'宁': 0}, '吉': {'宁': 0}, '黑': {'龙': 1}, '上': {'海': 0}, '江': {'苏': 0, '西': 0}, '浙': {'江': 0}, '安': {'徽': 0}, '福': {'福': 0}, '湖': {'南': 0, '北': 0}, '广': {'东': 0, '西': 0}, '海': {'南': 0}, '重': {'庆': 0}, '四': {'川': 0}, '贵': {'州': 0}, '云': {'南': 0}, '西': {'藏': 0}, '陕': {'西': 0}, '甘': {'肃': 0}, '青': {'海': 0}, '宁': {'夏': 0}, '新': {'疆': 0}, '香': {'港': 0}, '澳': {'门': 0}, '台': {'湾': 0}} 4 | var adEndSuffix = [2]map[int32]int{{'市': 1, '州': 1, '区': 1, '盟': 1}, {'县': 2, '市': 2, '旗': 2}} 5 | 6 | type Detail struct { 7 | IP IP 8 | Start IP 9 | End IP 10 | Location 11 | } 12 | 13 | func (detail Detail) String() string { 14 | return detail.Location.String() 15 | } 16 | 17 | func (detail Detail) Bytes() []byte { 18 | return detail.Location.Bytes() 19 | } 20 | 21 | func (detail Detail) InIP(ip IP) bool { 22 | return detail.Start.Compare(ip) < 1 && detail.End.Compare(ip) > -1 23 | } 24 | 25 | func (detail Detail) In(rawIP string) bool { 26 | ip, err := ParseIP(rawIP) 27 | if err != nil { 28 | return false 29 | } 30 | return detail.InIP(ip) 31 | } 32 | 33 | func (detail Detail) InUint(uintIP uint32) bool { 34 | return detail.InIP(ParseUintIP(uintIP)) 35 | } 36 | 37 | func (detail *Detail) fill() *Detail { 38 | if detail.Region == "N/A" { 39 | return detail 40 | } 41 | 42 | var ( 43 | rs = []rune(detail.Country) 44 | s [2][]rune 45 | p map[int32]int 46 | ok bool 47 | i, n int 48 | ) 49 | 50 | if p, ok = provincePrefix[rs[0]]; ok { 51 | i, ok = p[rs[1]] 52 | i += 2 53 | } 54 | if !ok { 55 | return detail 56 | } 57 | 58 | detail.Country = "中国" 59 | detail.Province = string(rs[:i]) 60 | 61 | if i >= len(rs) { 62 | if rs[0] == '北' || rs[0] == '天' || rs[0] == '上' || rs[0] == '重' { 63 | detail.City = string(rs[:i-1]) 64 | } 65 | return detail 66 | } 67 | 68 | if rs[i] == '市' { 69 | i++ 70 | detail.City = string(rs[:i]) 71 | } else if rs[i] == '省' { 72 | i++ 73 | } 74 | 75 | for ; i < len(rs); i++ { 76 | s[n] = append(s[n], rs[i]) 77 | if _, ok = adEndSuffix[n][rs[i]]; ok { 78 | if rs[i] != '市' && i + 1 < len(rs) && rs[i+1] == '市' { 79 | continue 80 | } 81 | n++ 82 | } 83 | if n > 1 { 84 | break 85 | } 86 | } 87 | 88 | if detail.City != "" { 89 | detail.County = string(s[0]) 90 | } else { 91 | detail.City = string(s[0]) 92 | detail.County = string(s[1]) 93 | } 94 | 95 | return detail 96 | } 97 | 98 | type Location struct { 99 | Country string 100 | Region string 101 | Province string 102 | City string 103 | County string 104 | raw string 105 | } 106 | 107 | func (location Location) String() string { 108 | return location.raw 109 | } 110 | 111 | func (location Location) Bytes() []byte { 112 | return []byte(location.raw) 113 | } 114 | 115 | func parseLocation(country, region []byte) Location { 116 | location := Location{ 117 | Country: string(country), 118 | Region: string(region), 119 | } 120 | location.raw = location.Country 121 | if region != nil { 122 | location.raw += " " + location.Region 123 | } 124 | return location 125 | } 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iploc 2 | 3 | [![Build Status](https://travis-ci.org/kayon/iploc.svg?branch=master)](https://travis-ci.org/kayon/iploc) 4 | 5 | 使用纯真IP库 `qqwry.dat`,高性能,线程安全,并对国内数据格式化到省、市、县 6 | 7 | > 需要 go 1.9 或更高 8 | 9 | > 附带的 `qqwry.dat` 为 `UTF-8` 编码 `2018-05-15版本` 10 | 11 | > 不再提供 `qqwry.dat`, 新增命令行工具 `iploc-fetch`, 可在线获取官方最新版本的 `qqwry.dat` 12 | 13 | 14 | ## 安装 15 | 16 | ``` 17 | go get -u github.com/kayon/iploc/... 18 | ``` 19 | 20 | #### 无法安装 `golang.org/x/text` 包,没有梯子使用下面方法 21 | 22 | ``` 23 | $ mkdir -P $GOPATH/src/golang.org/x 24 | cd $GOPATH/src/golang.org/x 25 | git clone https://github.com/golang/text.git 26 | ``` 27 | 28 | ## 获取&更新 qqwry.dat 29 | 30 | #### 1. 下载 `qqwry.dat` 31 | 32 | ##### 方法一:使用命令行工具 [iploc-fetch](#iploc-fetch) 33 | 34 | 由于服务器限制国外IP,只能使用国内网络。 35 | 36 | 下载到当前目录,保存为 `qqwry.gbk.dat` 37 | 38 | ``` 39 | $ iploc-fetch qqwry.gbk.dat 40 | ``` 41 | 42 | ##### 方法二:手动下载 43 | 44 | 在[纯真官网下载](http://www.cz88.net/fox/ipdat.shtml)并安装,复制安装目录中的 `qqwry.dat` 45 | 46 | #### 2. 转换为 `UTF-8` 47 | 48 | 使用命令行工具 [iploc-conv](#iploc-conv) 将刚刚下载的 `qqwry.gbk.dat` 转换为 `UTF-8` 保存为 `qqwry.dat` 49 | 50 | ``` 51 | iploc-conv -s qqwry.gbk.dat -d qqwry.dat 52 | ``` 53 | 54 | 55 | ## Benchmarks 56 | 57 | ``` 58 | // 缓存索引 59 | BenchmarkFind-8 2000000 735 ns/op 136万/秒 60 | // 无索引 61 | BenchmarkFindUnIndexed-8 20000 78221 ns/op 1.2万/秒 62 | // 缓存索引,并发 63 | BenchmarkFindParallel-8 10000000 205 ns/op 487万/秒 64 | ``` 65 | 66 | ## 使用 67 | 68 | ``` 69 | func main() { 70 | loc, err := iploc.Open("qqwry.dat") 71 | if err != nil { 72 | panic(err) 73 | } 74 | detail := loc.Find("8.8.8") // 补全为8.8.0.8, 参考 ping 工具 75 | fmt.Printf("IP:%s; 网段:%s - %s; %s\n", detail.IP, detail.Start, detail.End, detail) 76 | 77 | detail2 := loc.Find("8.8.3.1") 78 | fmt.Printf("%t %t\n", detail.In(detail2.IP.String()), detail.String() == detail2.String()) 79 | 80 | // output 81 | // IP:8.8.0.8; 网段: 8.7.245.0 - 8.8.3.255; 美国 科罗拉多州布隆菲尔德市Level 3通信股份有限公司 82 | // true true 83 | 84 | detail = loc.Find("1.24.41.0") 85 | fmt.Println(detail.String()) 86 | fmt.Println(detail.Country, detail.Province, detail.City, detail.County) 87 | 88 | // output 89 | // 内蒙古锡林郭勒盟苏尼特右旗 联通 90 | // 中国 内蒙古 锡林郭勒盟 苏尼特右旗 91 | 92 | } 93 | ``` 94 | 95 | #### 快捷方法 96 | ##### Find(qqwrySrc, ip string) (*Detail, error) 97 | `iploc.Find` 使用 `OpenWithoutIndexes` 98 | 99 | #### 初始化 100 | ##### Open(qqwrySrc string) (*Locator, error) 101 | 102 | `iploc.Open` 缓存并索引,生成索引需要耗时500毫秒左右,但会带来更高的查询性能 103 | 104 | ##### OpenWithoutIndexes(qqwrySrc string) (*Locator, error) 105 | 106 | `iploc.OpenWithoutIndexes` 只读取文件头做简单检查,无索引 107 | 108 | #### 查询 109 | 110 | ``` 111 | (*Locator).Find(ip string) *Detail 112 | 113 | ``` 114 | > 如果IP不合法,返回 `nil` 115 | 116 | 117 | ## 命令行工具 118 | 119 | #### iploc-conv 120 | 121 | 将原版 `qqwry.dat` 由 `GBK` 转换为 `UTF-8` 122 | 123 | ``` 124 | $ iploc-conv -s src.gbk.dat -d dst.utf8.dat 125 | ``` 126 | 127 | > 新生成的DAT文件保留原数据结构,由于编码原因,文件会增大一点 128 | 129 | > 修正原 qqwry.dat 中几处错误的重定向 (qqwry.dat 2018-05-10),并将 "CZ88.NET" 替换为 "N/A" 130 | 131 | #### iploc-fetch 132 | 133 | 从纯真官网下载最新 `qqwry.dat` 134 | 135 | 由于服务器限制国外IP,只能使用国内网络。 136 | 137 | ``` 138 | $ iploc-fetch qqwry.gbk.dat 139 | ``` 140 | 141 | > 下载后别忘了使用 `iploc-conv` 转换为 `UTF-8` 142 | 143 | #### iploc-gen 144 | 145 | 创建静态版本的 **iploc** 集成到你的项目中 146 | 147 | `iploc-gen` 会在当前目录创建 iploc-binary.go 文件,拷贝到你的项目中,通过变量名 *IPLoc* 直接可以使用 148 | 149 | ``` 150 | $ iploc-gen path/qqwry.dat 151 | ``` 152 | 153 | > `--pkg` 指定 package name, 默认 main 154 | 155 | > `-n` 使用 `OpenWithoutIndexes` 初始化,无索引 156 | 157 | 158 | ## 静态编译 iploc 和 qqwry.dat 并集成到你的项目中 159 | 160 | 编译后的二进制没有 `qqwry.dat` 依赖,不需要再带着 `qqwry.dat` 一起打包了 161 | 162 | ##### 示例 163 | 164 | 到项目目录 `$GOPATH/src/myproject/` 中 165 | 166 | ``` 167 | $ mkdir myloc && cd myloc 168 | $ iploc-gen path/qqwry.dat --pkg myloc 169 | ``` 170 | 171 | > $GOPATH/src/myproject/main.go 172 | 173 | ``` 174 | package main 175 | 176 | import ( 177 | "fmt" 178 | 179 | "myproject/myloc" 180 | ) 181 | 182 | func main() { 183 | fmt.Println(myloc.IPLoc.Find("8.8.8.8")) 184 | } 185 | ``` -------------------------------------------------------------------------------- /cmd/iploc-fetch/iploc-fetch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "encoding/binary" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "net/http" 11 | "os" 12 | 13 | flag "github.com/spf13/pflag" 14 | "golang.org/x/text/encoding/simplifiedchinese" 15 | "golang.org/x/text/transform" 16 | ) 17 | 18 | const ( 19 | userAgent = "Mozilla/3.0 (compatible; Indy Library)" 20 | urlCopywrite = "http://update.cz88.net/ip/copywrite.rar" 21 | urlQqwryDat = "http://update.cz88.net/ip/qqwry.rar" 22 | ) 23 | 24 | var ( 25 | outputFile string 26 | quiet, help bool 27 | 28 | fileSize uint64 29 | fileSizeW int 30 | ) 31 | 32 | type FetchWriter struct { 33 | n uint64 34 | } 35 | 36 | func (w *FetchWriter) Write(p []byte) (int, error) { 37 | n := len(p) 38 | w.n += uint64(n) 39 | w.progress() 40 | return n, nil 41 | } 42 | 43 | func (w *FetchWriter) progress() { 44 | printf("\rfetch: %6.2f%% %*d/%d bytes", float64(w.n)/float64(fileSize)*100, fileSizeW, w.n, fileSize) 45 | } 46 | 47 | func fetch(key uint32) error { 48 | resp, err := newRequest(urlQqwryDat) 49 | if err != nil { 50 | return err 51 | } 52 | defer resp.Body.Close() 53 | 54 | if uint64(resp.ContentLength) != fileSize { 55 | return fmt.Errorf("the file size of the agreement is different") 56 | } 57 | 58 | var b []byte 59 | buffer := bytes.NewBuffer(b[:]) 60 | 61 | _, err = io.Copy(buffer, io.TeeReader(resp.Body, &FetchWriter{})) 62 | printf("\n") 63 | if err != nil { 64 | return err 65 | } 66 | b = buffer.Bytes() 67 | 68 | // 解码前512字节 69 | for i := 0; i < 0x200; i++ { 70 | key *= 0x805 71 | key++ 72 | key &= 0xFF 73 | b[i] = b[i] ^ byte(key) 74 | } 75 | 76 | r, err := zlib.NewReader(bytes.NewBuffer(b)) 77 | if err != nil { 78 | return err 79 | } 80 | defer r.Close() 81 | 82 | f, err := os.Create(outputFile) 83 | if err != nil { 84 | return err 85 | } 86 | _, err = io.Copy(f, r) 87 | return err 88 | } 89 | 90 | func init() { 91 | flag.BoolVarP(&quiet, "quiet", "q", false, "only output error") 92 | flag.BoolVarP(&help, "help", "h", false, "this help") 93 | flag.CommandLine.SortFlags = false 94 | flag.Parse() 95 | 96 | flag.Usage = func() { 97 | fmt.Fprintf(os.Stderr, "iploc-fetch: fetch qqwry.dat.\nUsage: iploc-fetch [output filename] [arguments]\nOptions:\n") 98 | flag.PrintDefaults() 99 | } 100 | 101 | outputFile = flag.Arg(0) 102 | 103 | if outputFile == "" || help { 104 | flag.Usage() 105 | if help { 106 | os.Exit(0) 107 | } 108 | os.Exit(1) 109 | } 110 | } 111 | 112 | func main() { 113 | resp, err := newRequest(urlCopywrite) 114 | if err != nil { 115 | fmt.Fprintf(os.Stderr, err.Error()) 116 | os.Exit(1) 117 | } 118 | b, err := ioutil.ReadAll(resp.Body) 119 | resp.Body.Close() 120 | 121 | fatal(err) 122 | 123 | s := readVersion(b) 124 | if s == nil { 125 | fatal(fmt.Errorf("invalid file description")) 126 | } 127 | fileSize = uint64(binary.LittleEndian.Uint32(b[12:])) 128 | fileSizeW = len(fmt.Sprint(fileSize)) 129 | key := binary.LittleEndian.Uint32(b[20:]) 130 | 131 | printf("version: %s\n", toUTF8(s)) 132 | printf("fetch: ...") 133 | 134 | fatal(fetch(key)) 135 | } 136 | 137 | func newRequest(urls string) (*http.Response, error) { 138 | req, err := http.NewRequest("GET", urls, nil) 139 | if err != nil { 140 | return nil, err 141 | } 142 | req.Header.Set("User-Agent", userAgent) 143 | client := &http.Client{} 144 | return client.Do(req) 145 | } 146 | 147 | func readVersion(p []byte) []byte { 148 | var start = 24 149 | var end int 150 | 151 | if start >= len(p) { 152 | return nil 153 | } 154 | 155 | // 0x20 ASCII space 156 | for p[start] != 0x20 { 157 | start++ 158 | } 159 | start += 1 160 | end = start 161 | 162 | if end >= len(p) { 163 | return nil 164 | } 165 | 166 | for p[end] != 0x00 { 167 | end++ 168 | } 169 | return p[start:end] 170 | } 171 | 172 | func toUTF8(s []byte) (b []byte) { 173 | r := transform.NewReader(bytes.NewReader(s), simplifiedchinese.GBK.NewDecoder()) 174 | b, _ = ioutil.ReadAll(r) 175 | return 176 | } 177 | 178 | func fatal(err error) { 179 | if err != nil { 180 | fmt.Fprintf(os.Stderr, err.Error()+"\n") 181 | os.Exit(1) 182 | } 183 | } 184 | 185 | func printf(format string, args ...interface{}) { 186 | if quiet { 187 | return 188 | } 189 | fmt.Printf(format, args...) 190 | } 191 | -------------------------------------------------------------------------------- /iploc.go: -------------------------------------------------------------------------------- 1 | package iploc 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "fmt" 7 | "io/ioutil" 8 | "runtime" 9 | ) 10 | 11 | const Version = "1.0" 12 | 13 | type rangeIterator func(i int, start, end IP) bool 14 | 15 | // Find shorthand for iploc.Open.Find 16 | // not be preload file and without indexed 17 | func Find(qqwrySrc, rawIP string) (*Detail, error) { 18 | loc, err := OpenWithoutIndexes(qqwrySrc) 19 | if err != nil { 20 | return nil, err 21 | } 22 | defer loc.parser.Close() 23 | return loc.Find(rawIP), nil 24 | } 25 | 26 | // Open 生成索引,查询速度快 27 | func Open(qqwrySrc string) (loc *Locator, err error) { 28 | loc = &Locator{} 29 | var parser *Parser 30 | if parser, err = NewParser(qqwrySrc, true); err != nil { 31 | return nil, err 32 | } 33 | loc.indexes = newIndexes(parser) 34 | loc.count = int(parser.count) 35 | return 36 | } 37 | 38 | // OpenWithoutIndexes 无索引,不预载文件,打开速度快,但查询速度慢 39 | func OpenWithoutIndexes(qqwrySrc string) (loc *Locator, err error) { 40 | loc = &Locator{} 41 | if loc.parser, err = NewParser(qqwrySrc, false); err != nil { 42 | return nil, err 43 | } 44 | loc.count = int(loc.parser.count) 45 | return 46 | } 47 | 48 | func Load(b []byte) (loc *Locator, err error) { 49 | buf := bytes.NewReader(b) 50 | r, err := zlib.NewReader(buf) 51 | if err != nil { 52 | return nil, err 53 | } 54 | b, err = ioutil.ReadAll(r) 55 | if err != nil { 56 | return nil, err 57 | } 58 | loc = &Locator{} 59 | res := &resource{data: b} 60 | var parser *Parser 61 | if parser, err = NewParserRes(res, uint32(len(res.data))); err != nil { 62 | return nil, err 63 | } 64 | loc.indexes = newIndexes(parser) 65 | loc.count = int(parser.count) 66 | return 67 | } 68 | 69 | func LoadWithoutIndexes(b []byte) (loc *Locator, err error) { 70 | buf := bytes.NewReader(b) 71 | r, err := zlib.NewReader(buf) 72 | if err != nil { 73 | return nil, err 74 | } 75 | b, err = ioutil.ReadAll(r) 76 | if err != nil { 77 | return nil, err 78 | } 79 | loc = &Locator{} 80 | res := &resource{data:b} 81 | if loc.parser, err = NewParserRes(res, uint32(len(res.data))); err != nil { 82 | return nil, err 83 | } 84 | loc.count = int(loc.parser.count) 85 | return 86 | } 87 | 88 | type Locator struct { 89 | parser *Parser 90 | indexes *indexes 91 | count int 92 | } 93 | 94 | // Close close the file descriptor, if there is no preload 95 | func (loc *Locator) Close() error { 96 | if loc.parser != nil { 97 | return loc.parser.Close() 98 | } 99 | return nil 100 | } 101 | 102 | func (loc *Locator) Count() int { 103 | return loc.count 104 | } 105 | 106 | func (loc *Locator) FindIP(ip IP) *Detail { 107 | return loc.find(ip) 108 | } 109 | 110 | func (loc *Locator) FindUint(uintIP uint32) *Detail { 111 | return loc.find(ParseUintIP(uintIP)) 112 | } 113 | 114 | func (loc *Locator) Find(rawIP string) *Detail { 115 | ip, err := ParseIP(rawIP) 116 | if err != nil { 117 | return nil 118 | } 119 | return loc.find(ip) 120 | } 121 | 122 | func (loc *Locator) Range(iterator rangeIterator) { 123 | if loc.indexes != nil { 124 | var n int 125 | loc.indexes.index.AscendRange(nil, nil, func(i dataItem) bool { 126 | n++ 127 | return iterator(n, ParseUintIP(i.(indexItem)[0]), ParseUintIP(i.(indexItem)[1])) 128 | }) 129 | } else { 130 | loc.parser.IndexRange(func(i int, start, end, pos uint32) bool { 131 | return iterator(i, ParseUintIP(start), ParseUintIP(end)) 132 | }) 133 | } 134 | } 135 | 136 | func (loc *Locator) find(ip IP) *Detail { 137 | defer func() { 138 | if r := recover(); r != nil { 139 | buf := make([]byte, 1<<16) 140 | buf = buf[:runtime.Stack(buf, false)] 141 | fmt.Printf("iploc > panic: %v\n%s", r, buf) 142 | } 143 | }() 144 | 145 | hit := loc.seek(ip) 146 | detail := &Detail{ 147 | IP: ip, 148 | Start: ParseUintIP(hit[0]), 149 | End: ParseUintIP(hit[1]), 150 | Location: loc.getLocation(hit), 151 | } 152 | return detail.fill() 153 | } 154 | 155 | func (loc *Locator) seek(ip IP) (hit indexItem) { 156 | if loc.indexes != nil { 157 | hit = loc.indexes.indexOf(ip.Uint()) 158 | } else { 159 | var low, mid, high uint32 160 | var index int64 161 | var start, end IP 162 | high = loc.parser.Count() - 1 163 | for low <= high { 164 | mid = (low + high) >> 1 165 | index = int64(loc.parser.min + mid*indexBlockSize) 166 | start = ParseBytesIP(loc.parser.ReadBytes(index, ipByteSize)) 167 | if ip.Compare(start) < 0 { 168 | high = mid - 1 169 | } else { 170 | index = loc.parser.ReadPosition(index + ipByteSize) 171 | end = ParseBytesIP(loc.parser.ReadBytes(index, ipByteSize)) 172 | if ip.Compare(end) > 0 { 173 | low = mid + 1 174 | } else { 175 | hit = indexItem{start.Uint(), end.Uint(), uint32(index), 0} 176 | break 177 | } 178 | } 179 | } 180 | } 181 | return 182 | } 183 | 184 | func (loc *Locator) getLocation(item indexItem) Location { 185 | if loc.indexes != nil { 186 | return loc.indexes.getLocation(item[2], item[3]) 187 | } 188 | return loc.parser.digLocation(int64(item[2])) 189 | } 190 | -------------------------------------------------------------------------------- /cmd/iploc-conv/iploc-conv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | "time" 12 | 13 | flag "github.com/spf13/pflag" 14 | "golang.org/x/text/encoding/simplifiedchinese" 15 | "golang.org/x/text/transform" 16 | 17 | "github.com/kayon/iploc" 18 | ) 19 | 20 | // 将原版 qqwry.dat 由 GBK 转换为 UTF-8 21 | // 修正原 qqwry.dat 中几处错误的重定向,并将 "CZ88.NET" 替换为 "N/A" 22 | 23 | var ( 24 | qqwrySrc, qqwryDst string 25 | quiet, noCheck, help bool 26 | gbkDecoder transform.Transformer 27 | ) 28 | 29 | // 修正 qqwry.dat 2018-05-10 30 | var fix = map[uint32][2]string{ 31 | 3413565439: {"广东省广州市", "有线宽带"}, // 203.118.223.255 32 | 3524192255: {"广东省广州市", "联通"}, // 210.14.231.255 33 | 3526640127: {"福建省厦门市", "联通"}, // 210.52.65.255 34 | 3526641407: {"广东省广州市", "联通"}, // 210.52.70.255 35 | 3526646783: {"广东省佛山市", "联通"}, // 210.52.91.255 36 | 3549601791: {"北京市", "广电网"}, // 211.146.159.255 37 | 3549605631: {"北京市", "广电网"}, // 211.146.174.255 38 | } 39 | 40 | func init() { 41 | flag.StringVarP(&qqwrySrc, "src", "s", "", "source DAT file (GBK)") 42 | flag.StringVarP(&qqwryDst, "dst", "d", "", "destination DAT file (UTF-8)") 43 | flag.BoolVarP(&quiet, "quiet", "q", false, "only output error") 44 | flag.BoolVarP(&noCheck, "nc", "n", false, "do not check the converted DAT") 45 | flag.BoolVarP(&help, "help", "h", false, "this help") 46 | flag.CommandLine.SortFlags = false 47 | flag.Parse() 48 | 49 | flag.Usage = func() { 50 | fmt.Fprintf(os.Stderr, "iploc-conv: converts DAT file from GBK into UTF-8.\nUsage: iploc-conv -s src.gbk.dat -d dst.utf8.dat [arguments]\nOptions:\n") 51 | flag.PrintDefaults() 52 | } 53 | 54 | if qqwrySrc == "" || qqwryDst == "" || help { 55 | flag.Usage() 56 | if help { 57 | os.Exit(0) 58 | } 59 | os.Exit(1) 60 | } else if qqwrySrc == qqwryDst { 61 | fmt.Fprintln(os.Stderr, "source and destination files is same") 62 | os.Exit(1) 63 | } 64 | 65 | gbkDecoder = simplifiedchinese.GBK.NewDecoder() 66 | } 67 | 68 | func main() { 69 | st := time.Now() 70 | parser, err := iploc.NewParser(qqwrySrc, true) 71 | if err != nil { 72 | fmt.Println(err) 73 | return 74 | } 75 | 76 | f, err := os.Create(qqwryDst) 77 | if err != nil { 78 | fmt.Println(err) 79 | return 80 | } 81 | 82 | var ( 83 | raw iploc.LocationRaw 84 | locOffset = make(map[uint32]uint32) 85 | locPos = make(map[string]uint32) 86 | indexes, s []byte 87 | head [8]byte 88 | offset, o uint32 89 | has bool 90 | count = parser.Count() 91 | l = len(fmt.Sprint(count)) 92 | w, n int 93 | wt = bufio.NewWriter(f) 94 | ) 95 | 96 | // write head 8 bytes 97 | wt.Write(head[:]) 98 | 99 | // head 8 bytes 100 | offset = 8 101 | parser.IndexRange(func(i int, start, end, pos uint32) bool { 102 | if !quiet { 103 | fmt.Printf("\rBuild %6.2f%% %*d/%d", float64(i)/float64(count)*100, l, i, count) 104 | } 105 | raw = parser.ReadLocationRaw(int64(pos)) 106 | // write end ip 4 bytes 107 | w, _ = wt.Write(iploc.ParseUintIP(end).ReverseBytes()) 108 | n += w 109 | // write start ip 4 bytes 110 | indexes = append(indexes, iploc.ParseUintIP(start).ReverseBytes()...) 111 | // write offset 3 bytes 112 | indexes = append(indexes, byte(offset), byte(offset>>8), byte(offset>>16)) 113 | // end ip 4 bytes 114 | offset += 4 115 | 116 | if _, has = fix[end]; has { 117 | if fix[end][0] != "" { 118 | if locPos[fix[end][0]] != 0 { 119 | raw.Text[0] = nil 120 | raw.Mode[0] = 0x02 121 | raw.Pos[0] = locPos[fix[end][0]] 122 | } else { 123 | raw.Text[0] = []byte(fix[end][0]) 124 | raw.Mode[0] = 0x00 125 | raw.Pos[0] = offset 126 | } 127 | } 128 | if fix[end][1] != "" { 129 | if locPos[fix[end][1]] != 0 { 130 | raw.Text[1] = nil 131 | raw.Mode[1] = 0x02 132 | raw.Pos[1] = locPos[fix[end][1]] 133 | } else { 134 | raw.Text[1] = []byte(fix[end][0]) 135 | raw.Mode[1] = 0x00 136 | raw.Pos[1] = uint32(len(raw.Text[0])) + offset 137 | } 138 | } 139 | } 140 | 141 | if raw.Text[0] != nil { 142 | if raw.Mode[0] != 0x00 { 143 | w, _ = wt.Write([]byte{raw.Mode[0]}) 144 | n += w 145 | offset += 1 146 | } 147 | locOffset[raw.Pos[0]] = offset 148 | s = toUTF8(raw.Text[0]) 149 | locPos[string(s)] = raw.Pos[0] 150 | w, _ = wt.Write(append(s, 0x00)) 151 | n += w 152 | offset += uint32(len(s)) + 1 153 | } else { 154 | o = locOffset[raw.Pos[0]] 155 | w, _ = wt.Write([]byte{raw.Mode[0], byte(o), byte(o>>8), byte(o>>16)}) 156 | n += w 157 | offset += 4 // 1+3 158 | } 159 | 160 | // redirected redundant data (mode 0x02) 161 | if raw.Text[1] != nil && locOffset[raw.Pos[1]] == 0 { 162 | locOffset[raw.Pos[1]] = offset 163 | s = toUTF8(raw.Text[1]) 164 | // CZ88.NET 165 | if bytes.Compare(s, []byte{32, 67, 90, 56, 56, 46, 78, 69, 84}) == 0 { 166 | s = []byte{78, 47, 65} // N/A 167 | } 168 | locPos[string(s)] = raw.Pos[1] 169 | w, _ = wt.Write(append(s, 0x00)) 170 | n += w 171 | offset += uint32(len(s)) + 1 172 | } else { 173 | o = locOffset[raw.Pos[1]] 174 | w, _ = wt.Write([]byte{0x02, byte(o), byte(o>>8), byte(o>>16)}) 175 | n += w 176 | offset += 4 177 | } 178 | return true 179 | }) 180 | 181 | if !quiet { 182 | fmt.Printf("\rBuild %6.2f%% %*d/%d %s\n", 100.0, l, count, count, time.Since(st)) 183 | } 184 | 185 | min := n + 8 186 | max := min + len(indexes) - 7 187 | binary.LittleEndian.PutUint32(head[:4], uint32(min)) 188 | binary.LittleEndian.PutUint32(head[4:], uint32(max)) 189 | wt.Write(indexes) 190 | wt.Flush() 191 | f.WriteAt(head[:], io.SeekStart) 192 | f.Close() 193 | 194 | if !noCheck { 195 | check() 196 | } 197 | } 198 | 199 | func check() { 200 | if !quiet { 201 | fmt.Print("Check ...") 202 | } 203 | var ( 204 | locGBK, locUTF8 *iploc.Locator 205 | err error 206 | logger io.WriteCloser 207 | ) 208 | 209 | if locGBK, err = iploc.Open(qqwrySrc); err != nil { 210 | fmt.Fprintf(os.Stderr, err.Error()) 211 | os.Exit(1) 212 | } 213 | 214 | if locUTF8, err = iploc.Open(qqwryDst); err != nil { 215 | fmt.Fprintf(os.Stderr, err.Error()) 216 | os.Exit(1) 217 | } 218 | 219 | if locGBK.Count() != locUTF8.Count() { 220 | fmt.Fprintf(os.Stderr, "it's not the same version") 221 | os.Exit(1) 222 | } 223 | if logger, err = os.Create("iploc-conv.log"); err != nil { 224 | fmt.Fprintf(os.Stderr, err.Error()) 225 | os.Exit(1) 226 | } 227 | var ( 228 | detail1, detail2 *iploc.Detail 229 | count = locGBK.Count() 230 | l = len(fmt.Sprint(count)) 231 | warning int 232 | st = time.Now() 233 | ) 234 | locGBK.Range(func(i int, start, end iploc.IP) bool { 235 | detail1 = locGBK.FindIP(start) 236 | detail2 = locUTF8.FindIP(start) 237 | if !compare(detail1, detail2, logger) { 238 | warning ++ 239 | } 240 | if !quiet { 241 | fmt.Printf("\rCheck %6.2f%% %*d/%d", float64(i)/float64(count)*100, l, i, count) 242 | } 243 | return true 244 | }) 245 | logger.Close() 246 | 247 | if !quiet { 248 | fmt.Printf("\rCheck %6.2f%% %*d/%d %s\n", 100.0, l, count, count, time.Since(st)) 249 | } 250 | if warning > 0 { 251 | fmt.Printf("%d warnings. please see iploc-conv.log for more details\n", warning) 252 | } else { 253 | os.Remove("iploc-conv.log") 254 | } 255 | } 256 | 257 | func compare(a, b *iploc.Detail, logger io.WriteCloser) (ok bool) { 258 | var s string 259 | defer func() { 260 | if !ok { 261 | if a.Region == " CZ88.NET" && b.Region == "N/A" { 262 | ok = true 263 | return 264 | } 265 | fmt.Fprintf(logger, " GBK: %s,%s %d,%d\n", a.Start, a.End, a.Start.Uint(), a.End.Uint()) 266 | fmt.Fprintf(logger, "UTF8: %s,%s %d,%d\n", b.Start, b.End, b.Start.Uint(), b.End.Uint()) 267 | if s != "" { 268 | fmt.Fprintf(logger, " GBK: %s %v\n", s, []byte(s)) 269 | fmt.Fprintf(logger, "UTF8: %s %v\n", b, []byte(b.String())) 270 | fmt.Fprint(logger, "\n") 271 | } else { 272 | fmt.Fprint(logger, "\n") 273 | } 274 | } 275 | }() 276 | if a.Start.Compare(b.Start) != 0 || a.End.Compare(b.End) != 0 { 277 | ok = false 278 | return 279 | } 280 | if _, ok = fix[a.End.Uint()]; ok { 281 | return 282 | } 283 | s = string(toUTF8(a.Bytes())) 284 | ok = s == b.String() 285 | if s == "" { 286 | s = "" 287 | } 288 | return 289 | } 290 | 291 | func toUTF8(s []byte) (b []byte) { 292 | r := transform.NewReader(bytes.NewReader(s), gbkDecoder) 293 | b, _ = ioutil.ReadAll(r) 294 | return 295 | } 296 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package iploc 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | ) 10 | 11 | const ( 12 | indexBlockSize = 7 13 | ipByteSize = 4 14 | positionByteSize = 3 15 | 16 | terminatorFlag = 0x00 17 | redirectAll = 0x01 18 | redirectPart = 0x02 19 | ) 20 | 21 | /* 22 | 23 | 原始下载地址 http://www.cz88.net/fox/ipdat.shtml 24 | 原版qqwry.dat, 字节序: LittleEndian 编码字符集: GBK 25 | 使用转换工具 iploc-conv 转换为UTF-8 26 | 文件内容结构, 所有偏移位置都是3字节的绝对偏移 27 | <文件头 8字节|数据区|索引区 7字节的倍数> 28 | [文件头] 索引区开始位置(4字节)|索引区结束位置(4字节) 29 | [数据区] 结束IP(4字节)|国家地区数据 30 | *国家地区数据 31 | 通常是 国家数据(0x00结尾)|地区数据(0x00结尾) 32 | 当国家地区数据的首字节是 0x01 或 0x02时为重定向模式,后3字节为偏移位置 33 | 34 | 0x01 国家和地区都重定向, 偏移位置(3字节) 35 | 0x02 国家重定向, 偏移位置(3字节)|地区数据 36 | 无重定向 国家数据(0x00结尾)|地区数据 37 | 38 | 地区数据可能还有一次重定向(0x02开头) 39 | 40 | [索引区] 起始IP(4字节)|偏移位置(3字节) = 每条索引 7字节 41 | */ 42 | 43 | type Parser struct { 44 | res resReadCloser 45 | min uint32 46 | max uint32 47 | total uint32 48 | count uint32 49 | size uint32 50 | } 51 | 52 | type indexIterator func(i int, start, end, pos uint32) bool 53 | 54 | type LocationRaw struct { 55 | Text [2][]byte 56 | Pos [2]uint32 57 | Mode [2]byte 58 | } 59 | 60 | func NewParser(qqwrySrc string, preload bool) (*Parser, error) { 61 | var ( 62 | err error 63 | size uint32 64 | fd *os.File 65 | b []byte 66 | res resReadCloser 67 | ) 68 | 69 | if preload { 70 | if b, err = ioutil.ReadFile(qqwrySrc); err != nil { 71 | return nil, err 72 | } 73 | size = uint32(len(b)) 74 | res = &resource{data: b} 75 | } else { 76 | if fd, err = os.OpenFile(qqwrySrc, os.O_RDONLY, 0400); err != nil { 77 | return nil, err 78 | } 79 | fi, err := fd.Stat() 80 | if err != nil { 81 | return nil, err 82 | } 83 | size = uint32(fi.Size()) 84 | res = fd 85 | } 86 | return NewParserRes(res, size) 87 | } 88 | 89 | func NewParserRes(res resReadCloser, size uint32) (*Parser, error) { 90 | if res == nil { 91 | return nil, fmt.Errorf("nil resource") 92 | } 93 | var ( 94 | p = &Parser{res: res} 95 | b []byte 96 | n int 97 | err error 98 | errInvalidDat = fmt.Errorf("invalid IP dat file") 99 | ) 100 | b = make([]byte, ipByteSize*2) 101 | if n, err = p.res.ReadAt(b, 0); err != nil || n != ipByteSize*2 { 102 | return nil, errInvalidDat 103 | } 104 | 105 | p.min = binary.LittleEndian.Uint32(b[:ipByteSize]) 106 | p.max = binary.LittleEndian.Uint32(b[ipByteSize:]) 107 | if (p.max-p.min)%indexBlockSize != 0 || size != p.max+indexBlockSize { 108 | return nil, errInvalidDat 109 | } 110 | p.total = (p.max - p.min) / indexBlockSize 111 | p.count = (p.max-p.min)/indexBlockSize + 1 112 | p.size = size 113 | return p, nil 114 | } 115 | 116 | func (p *Parser) Close() error { 117 | return p.res.Close() 118 | } 119 | 120 | func (p *Parser) Count() uint32 { 121 | return p.count 122 | } 123 | 124 | func (p *Parser) Size() uint32 { 125 | return p.size 126 | } 127 | 128 | func (p *Parser) Reader() io.Reader { 129 | return p.res 130 | } 131 | 132 | // (*Parser) ReadByte 读取1字节,用来识别重定向模式 133 | func (p *Parser) ReadByte(pos int64) byte { 134 | b := make([]byte, 1) 135 | n, err := p.res.ReadAt(b, pos) 136 | if err != nil || n != 1 { 137 | panic("ReadByte damaged DAT files, position: " + fmt.Sprint(pos)) 138 | } 139 | return b[0] 140 | } 141 | 142 | // (*Parser) ReadBytes 读取n字节并翻转 143 | func (p *Parser) ReadBytes(pos, n int64) (b []byte) { 144 | b = make([]byte, n) 145 | i, err := p.res.ReadAt(b, pos) 146 | if err != nil || int64(i) != n { 147 | panic("ReadBytes damaged DAT files, position: " + fmt.Sprint(pos)) 148 | } 149 | // reverse bytes 150 | for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 { 151 | b[i], b[j] = b[j], b[i] 152 | } 153 | return 154 | } 155 | 156 | // (*Parser) ReadPosition 读取3字节的偏移位置 157 | func (p *Parser) ReadPosition(offset int64) int64 { 158 | b := p.ReadBytes(offset, positionByteSize) 159 | // left padding to the 32 bits 160 | if i := 4 - len(b); i > 0 { 161 | b = append(make([]byte, i), b...) 162 | } 163 | // bytes has been reversed, so it won't use binary.LittleEndian 164 | return int64(binary.BigEndian.Uint32(b)) 165 | } 166 | 167 | // (*Parser) ReadText 读取国家地区数据,以0x00结尾 168 | func (p *Parser) ReadText(offset int64) ([]byte, int) { 169 | if uint32(offset) >= p.min { 170 | return nil, 0 171 | } 172 | var s []byte 173 | var b byte 174 | for { 175 | b = p.ReadByte(offset) 176 | if b != terminatorFlag { 177 | s = append(s, b) 178 | } else if len(s) > 0 { 179 | break 180 | } 181 | offset++ 182 | } 183 | return s, len(s) 184 | } 185 | 186 | func (p *Parser) ReadString(offset int64) (string, int) { 187 | s, n := p.ReadText(offset) 188 | return string(s), n 189 | } 190 | 191 | // (*Parser) ReadRegion 读取地区数据,处理可能的重定向 192 | func (p *Parser) ReadRegion(offset int64) (s []byte) { 193 | switch p.ReadByte(offset) { 194 | case redirectPart: 195 | s, _ = p.ReadText(p.ReadPosition(offset + 1)) 196 | default: 197 | s, _ = p.ReadText(offset) 198 | } 199 | return 200 | } 201 | 202 | func (p *Parser) ReadRegionString(offset int64) string { 203 | return string(p.ReadRegion(offset)) 204 | } 205 | 206 | func (p *Parser) digLocation(offset int64) (location Location) { 207 | var n int 208 | switch p.ReadByte(offset + ipByteSize) { 209 | case redirectAll: 210 | offset = p.ReadPosition(offset + ipByteSize + 1) 211 | switch p.ReadByte(offset) { 212 | case redirectPart: 213 | location.Country, _ = p.ReadString(p.ReadPosition(offset + 1)) 214 | location.Region = p.ReadRegionString(offset + 1 + positionByteSize) 215 | default: 216 | location.Country, n = p.ReadString(offset) 217 | // +1, skip 1 bytes 0x00 218 | location.Region = p.ReadRegionString(offset + 1 + int64(n)) 219 | } 220 | case redirectPart: 221 | location.Country, _ = p.ReadString(p.ReadPosition(offset + ipByteSize + 1)) 222 | location.Region = p.ReadRegionString(offset + ipByteSize + 1 + positionByteSize) 223 | default: 224 | location.Country, n = p.ReadString(offset + ipByteSize) 225 | // +1, skip 1 bytes 0x00 226 | location.Region = p.ReadRegionString(offset + ipByteSize + 1 + int64(n)) 227 | } 228 | location.raw = location.Country 229 | if location.Region != "" { 230 | location.raw += " " + location.Region 231 | } 232 | return 233 | } 234 | 235 | func (p *Parser) readRegionRaw(offset int64) (s []byte, pos uint32, mode byte) { 236 | switch p.ReadByte(offset) { 237 | case redirectPart: 238 | pos = uint32(p.ReadPosition(offset + 1)) 239 | mode = redirectPart 240 | default: 241 | s, _ = p.ReadText(offset) 242 | } 243 | return 244 | } 245 | 246 | // ReadLocationRaw 用于导出或索引 247 | func (p *Parser) ReadLocationRaw(offset int64) (raw LocationRaw) { 248 | var n int 249 | raw.Mode[0] = p.ReadByte(offset + ipByteSize) 250 | switch raw.Mode[0] { 251 | case redirectAll: 252 | offset = p.ReadPosition(offset + ipByteSize + 1) 253 | switch p.ReadByte(offset) { 254 | case redirectPart: 255 | raw.Mode[0] = redirectPart 256 | raw.Pos[0] = uint32(p.ReadPosition(offset + 1)) 257 | raw.Text[1], raw.Pos[1], raw.Mode[1] = p.readRegionRaw(offset + 1 + positionByteSize) 258 | if raw.Text[1] != nil { 259 | raw.Pos[1] = uint32(offset + 1 + positionByteSize) 260 | } 261 | default: 262 | raw.Pos[0] = uint32(offset) 263 | _, n = p.ReadText(offset) 264 | _, raw.Pos[1], _ = p.readRegionRaw(offset + 1 + int64(n)) 265 | if raw.Pos[1] == 0 { 266 | raw.Pos[1] = uint32(offset + 1 + int64(n)) 267 | } 268 | } 269 | case redirectPart: 270 | raw.Pos[0] = uint32(p.ReadPosition(offset + ipByteSize + 1)) 271 | raw.Text[1], raw.Pos[1], raw.Mode[1] = p.readRegionRaw(offset + ipByteSize + 1 + positionByteSize) 272 | if raw.Text[1] != nil { 273 | raw.Pos[1] = uint32(offset + ipByteSize + 1 + positionByteSize) 274 | } 275 | default: 276 | raw.Pos[0] = uint32(offset + ipByteSize) 277 | raw.Mode[0] = 0x00 278 | raw.Text[0], n = p.ReadText(offset + ipByteSize) 279 | raw.Text[1], raw.Pos[1], raw.Mode[1] = p.readRegionRaw(offset + ipByteSize + 1 + int64(n)) 280 | if raw.Text[1] != nil { 281 | raw.Pos[1] = uint32(offset + ipByteSize + 1 + int64(n)) 282 | } 283 | } 284 | return 285 | } 286 | 287 | // (*Parser) IndexRange 288 | // calls the iterator for every index within the range (i, start, end, Pos) 289 | // until iterator returns false. 290 | func (p *Parser) IndexRange(iterator indexIterator) { 291 | var ( 292 | count = int(p.count) 293 | index, pos int64 294 | ) 295 | for i := 0; i < count; i++ { 296 | index = int64(p.min) + indexBlockSize*int64(i) 297 | pos = p.ReadPosition(index + 4) 298 | if !iterator( 299 | i+1, 300 | ParseBytesIP(p.ReadBytes(index, ipByteSize)).Uint(), 301 | ParseBytesIP(p.ReadBytes(pos, ipByteSize)).Uint(), 302 | uint32(pos), 303 | ) { 304 | break 305 | } 306 | } 307 | } 308 | --------------------------------------------------------------------------------