├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── pyipip ├── __init__.py └── ipipdb.py ├── setup.cfg ├── setup.py ├── tests ├── 17monipdb.dat ├── 17monipdb.datx ├── bench.py ├── ips.gz ├── reqs_bench.txt └── test_pyipip.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | tags 3 | .cache 4 | 5 | venv 6 | dev_venv 7 | *.egg-info 8 | dist 9 | build 10 | .tox 11 | 12 | *.dat 13 | *.datx 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | 4 | matrix: 5 | include: 6 | - python: 2.7 7 | - python: 3.4 8 | - python: 3.5 9 | - python: 3.6 10 | - python: 3.7 11 | dist: xenial 12 | - python: nightly 13 | - python: pypy 14 | - python: pypy3 15 | 16 | install: 17 | - pip install tox 18 | 19 | script: 20 | - tox -e py 21 | 22 | cache: 23 | - pip 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help clean test bench package publish 2 | 3 | help: 4 | @echo targets: test bench publish clean 5 | 6 | venv: 7 | virtualenv -q venv 8 | ./venv/bin/pip install -q -e . 9 | 10 | test: venv 11 | @./venv/bin/python tests/test_pyipip.py 12 | 13 | bench: venv 14 | ./venv/bin/pip install -q -r tests/reqs_bench.txt 15 | ./venv/bin/python tests/bench.py 16 | 17 | package: clean 18 | python setup.py sdist bdist_wheel 19 | 20 | publish: package 21 | twine upload dist/* 22 | 23 | clean: 24 | rm -rf venv 25 | rm -rf build dist *.egg-info 26 | find . -name '*.pyc' -delete 27 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | PyIPIP 2 | ====== 3 | 4 | .. image:: https://travis-ci.org/georgexsh/pyipip.svg?branch=master 5 | :target: https://travis-ci.org/georgexsh/pyipip 6 | 7 | .. image:: https://img.shields.io/pypi/v/pyipip.svg 8 | :target: https://pypi.org/project/pyipip/ 9 | 10 | 11 | Description 12 | ----------- 13 | 14 | ipip.net IPv4 地址归属地数据库 Python 查询库。同时支持 dat 与 datx 15 | 格式的数据文件,支持 Python 2 与 3。 需要先去 `ipip.net 16 | 官方网站 `__ 下载数据文件。 17 | 18 | 性能较官方库为高,在 E5-2682 2.5GHz 下 QPS 约为 490k:: 19 | 20 | pyipip 491507.74 21 | lxyu 101062.99 22 | official 5701.01 23 | 24 | 25 | Usage 26 | ----- 27 | 28 | .. code:: python 29 | 30 | >>> from pyipip import IPIPDatabase 31 | >>> db = IPIPDatabase('/path/to/your/ipipdb.dat') 32 | >>> db.lookup('202.112.80.106') 33 | '中国\t北京\t北京\t' 34 | 35 | Install 36 | ~~~~~~~ 37 | 38 | :: 39 | 40 | pip install pyipip 41 | 42 | Test & Benchmark 43 | ~~~~~~~~~~~~~~~~ 44 | 45 | :: 46 | 47 | make test 48 | 49 | :: 50 | 51 | make bench 52 | 53 | Note 54 | ---- 55 | 56 | 数据文件时时更新,请自行下载使用,代码仓库随附的数据文件只用于测试。 57 | -------------------------------------------------------------------------------- /pyipip/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.1' 2 | 3 | import logging 4 | logging.getLogger(__name__).addHandler(logging.NullHandler()) 5 | 6 | from .ipipdb import IPIPDatabase 7 | 8 | -------------------------------------------------------------------------------- /pyipip/ipipdb.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | import sys 5 | import time 6 | import array 7 | import struct 8 | import logging 9 | import platform 10 | 11 | # speed-up attr lookup 12 | from struct import unpack 13 | from socket import inet_aton 14 | from bisect import bisect_left 15 | 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | IS_PY3 = sys.version_info[0] == 3 20 | IS_PYPY = platform.python_implementation() == 'PyPy' 21 | 22 | unpack_uint32_little = lambda b: struct.unpack("L", b)[0] 24 | unpack_uint16_big = lambda b: struct.unpack(">H", b)[0] 25 | if IS_PY3: 26 | unpack_uint8 = lambda b: b 27 | else: 28 | unpack_uint8 = lambda b: struct.unpack("B", b)[0] 29 | 30 | 31 | class IPIPDatabase(object): 32 | 33 | def __init__(self, filename): 34 | self.filename = os.path.abspath(filename) 35 | self._ranges = array.array('I') 36 | self._offsets = array.array('I') 37 | self._strings = '' 38 | self.load_db() 39 | 40 | def __repr__(self): 41 | return '' % (self.filename) 42 | 43 | def load_db(self): 44 | start = time.time() 45 | filename = self.filename 46 | with open(filename, 'rb') as f: 47 | buff = f.read() 48 | if filename.endswith('.dat'): 49 | index_size = 256 * 4 50 | text_length_size = 1 51 | elif filename.endswith('.datx'): 52 | index_size = 256 * 256 * 4 53 | text_length_size = 2 54 | else: 55 | raise Exception('unknown database format') 56 | self._load_db(buff, index_size, text_length_size) 57 | logger.debug('loaded ipip data file: %s, in %.2fs, ranges: %s', 58 | filename, time.time()-start, len(self._ranges)) 59 | if not IS_PYPY: 60 | logger.debug('mem usage: idx %.2fM, offsets %.2fM, strings %.2fM', 61 | sys.getsizeof(self._ranges)/1048576.0, 62 | sys.getsizeof(self._offsets)/1048576.0, 63 | sys.getsizeof(self._strings)/1048576.0, 64 | ) 65 | 66 | def _load_db(self, buff, index_size, text_length_size): 67 | ns_buff = [] 68 | ns_offset = 0 69 | offset_old_to_new = {} 70 | text_start = unpack_uint32_big(buff[:4]) - index_size 71 | offset = index_size + 4 72 | while offset < text_start: 73 | ip_range = unpack_uint32_big(buff[offset:offset+4]) 74 | offset += 4 75 | self._ranges.append(ip_range) 76 | text_offset = unpack_uint32_little(buff[offset:offset+3] + b'\0') + text_start 77 | offset += 3 78 | if text_length_size == 1: 79 | text_length = unpack_uint8(buff[offset]) 80 | elif text_length_size == 2: 81 | text_length = unpack_uint16_big(buff[offset:offset+2]) 82 | offset += text_length_size 83 | if text_offset not in offset_old_to_new: 84 | s = buff[text_offset:text_offset+text_length].decode('utf-8') 85 | offset_old_to_new[text_offset] = (ns_offset, ns_offset+len(s)) 86 | ns_buff.append(s) 87 | ns_offset += len(s) 88 | self._offsets.extend(offset_old_to_new[text_offset]) 89 | self._strings = ''.join(ns_buff) 90 | 91 | def lookup(self, ip): 92 | n = unpack(">L", inet_aton(ip))[0] 93 | i = bisect_left(self._ranges, n) 94 | return self._strings[self._offsets[2*i]:self._offsets[2*i+1]] 95 | 96 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [metadata] 5 | license_file = LICENSE 6 | description-file = README.rst 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | def read_readme(): 5 | with open('README.rst', 'rb') as f: 6 | return f.read().decode('utf-8') 7 | 8 | 9 | setup(name='pyipip', 10 | version='0.1.1', 11 | description="ipip.net IP address geolocation database Python library", 12 | long_description=read_readme(), 13 | url='https://github.com/georgexsh/pyipip', 14 | author='georgexsh', 15 | author_email='georgexsh@gmail.com', 16 | license='MIT', 17 | packages=['pyipip'], 18 | classifiers=[ 19 | 'Development Status :: 4 - Beta', 20 | 'Intended Audience :: Developers', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Topic :: Software Development :: Libraries', 23 | 'Programming Language :: Python :: 2', 24 | 'Programming Language :: Python :: 3', 25 | 'Programming Language :: Python :: Implementation :: PyPy', 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /tests/17monipdb.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgexsh/pyipip/48f3ab44f11ad2cd41a478f08bfdccedcecf2247/tests/17monipdb.dat -------------------------------------------------------------------------------- /tests/17monipdb.datx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgexsh/pyipip/48f3ab44f11ad2cd41a478f08bfdccedcecf2247/tests/17monipdb.datx -------------------------------------------------------------------------------- /tests/bench.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import gzip 4 | import time 5 | import logging 6 | import resource 7 | import socket 8 | import os 9 | 10 | 11 | CWD = os.path.dirname(__file__) 12 | DATA_FILE = os.path.join(CWD, '17monipdb.dat') 13 | 14 | # logging.basicConfig(level=logging.DEBUG) 15 | 16 | 17 | with gzip.open(os.path.join(CWD, 'ips.gz'), 'rb') as f: 18 | testdata = [l.strip().decode('latin1') for l in f] 19 | 20 | 21 | class BenchBase(object): 22 | pass 23 | 24 | 25 | class Bench_official(BenchBase): 26 | 27 | def setup(self): 28 | import ipip 29 | self.db = ipip.IP 30 | self.db.load(DATA_FILE) 31 | 32 | def run(self): 33 | db = self.db 34 | for ip in testdata: 35 | db.find(ip) 36 | 37 | 38 | class Bench_lxyu(BenchBase): 39 | 40 | def setup(self): 41 | import IP 42 | self.db = IP.IPv4Database(DATA_FILE) 43 | 44 | def run(self): 45 | db = self.db 46 | for ip in testdata: 47 | db.find(ip) 48 | 49 | 50 | class Bench_pyipip(BenchBase): 51 | 52 | def setup(self): 53 | from pyipip import IPIPDatabase 54 | self.db = IPIPDatabase(DATA_FILE) 55 | 56 | def run(self): 57 | db = self.db 58 | for ip in testdata: 59 | db.lookup(ip) 60 | 61 | 62 | def main(): 63 | N = 3 64 | for b in BenchBase.__subclasses__(): 65 | n = b.__name__.split('_')[1] 66 | c = b() 67 | c.setup() 68 | c.run() # warm-up 69 | s = time.time() 70 | for _ in range(N): 71 | c.run() 72 | e = time.time() - s 73 | print(n, '%.2f' % (N*len(testdata) / e)) 74 | 75 | 76 | if __name__ == '__main__': 77 | main() 78 | -------------------------------------------------------------------------------- /tests/ips.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georgexsh/pyipip/48f3ab44f11ad2cd41a478f08bfdccedcecf2247/tests/ips.gz -------------------------------------------------------------------------------- /tests/reqs_bench.txt: -------------------------------------------------------------------------------- 1 | -e git+https://github.com/myiwen/ipip.git@c5aea92c14e0f900020ffb06aa8fcec23f87e55c#egg=ipip-master 2 | 17MonIP==0.2.7 3 | -------------------------------------------------------------------------------- /tests/test_pyipip.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import unicode_literals 4 | import unittest 5 | import logging 6 | import os 7 | 8 | import pyipip 9 | 10 | 11 | # logging.basicConfig(level=logging.DEBUG) 12 | 13 | CWD = os.path.dirname(__file__) 14 | DAT_FILE = os.path.join(CWD, '17monipdb.dat') 15 | DATX_FILE = os.path.join(CWD, '17monipdb.datx') 16 | 17 | 18 | class TestPyIPIP(unittest.TestCase): 19 | 20 | def test_dat(self): 21 | self._check(DAT_FILE) 22 | 23 | def test_datx(self): 24 | self._check(DATX_FILE) 25 | 26 | def _check(self, db_file): 27 | db = pyipip.IPIPDatabase(db_file) 28 | self.assertEqual(db.lookup('0.0.0.0'), u"保留地址\t保留地址\t\t") 29 | self.assertEqual(db.lookup('127.0.0.1'), u"本机地址\t本机地址\t\t") 30 | self.assertEqual(db.lookup('198.18.0.0'), u"保留地址\t保留地址\t\t") 31 | self.assertIn('北京', db.lookup('202.112.80.106')) 32 | self.assertEqual(db.lookup('255.255.255.255').split()[0], u"IPIP.NET") 33 | 34 | 35 | if __name__ == '__main__': 36 | unittest.main() 37 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{37,36,35,34,27,py,py3} 3 | 4 | [testenv] 5 | commands = python tests/test_pyipip.py 6 | --------------------------------------------------------------------------------