离线IP地理位置数据库-GeoLite2-City


mmdb 格式科普

.mmdb 文件格式通常指的是 MaxMind Database (MMDB) 格式,这是由 MaxMind 提供的一种用于存储 IP 地址地理定位数据的二进制文件格式。MMDB 文件包含了 IP 地址到地理位置信息的映射,这些信息可以包括国家、城市、纬度、经度等地理数据。这种格式特别适用于需要快速查询 IP 地址地理位置的场景,如网站分析、广告定位、网络安全等。

MMDB 文件的内容分为几个主要部分:

  1. 搜索树(Search Tree):这是 MMDB 文件的核心部分,它是一个二叉树结构,用于高效地从 IP 地址映射到地理位置数据。每个节点在树中代表一个 IP 地址范围,叶子节点包含实际的地理位置数据。
  2. 数据段(Data Section):这部分包含了实际的地理位置数据,如国家名称、城市名称等。数据段通常以 JSON 格式存储,但经过了特定的序列化处理,以便在检索时能够快速访问。
  3. 元数据(Metadata):包含了关于数据库的元信息,如版本号、创建时间、数据库类型(如 GeoIP2 City、GeoIP2 Country 等)、IP 版本(IPv4 或 IPv6)、语言、节点个数、节点记录规格等。

MMDB 文件的设计使得它在检索时非常高效,因为它不需要在内存中构建复杂的数据结构,而是直接在字节数组上进行操作。这使得 MMDB 文件非常适合作为内存数据库使用,因为它可以被整个加载到内存中,然后通过简单的内存计算来定位到具体的地理位置数据。

MaxMind 提供了多种编程语言的库来读取和解析 MMDB 文件,如 Python 的 geoip2 库,它允许开发者轻松地从 MMDB 文件中检索 IP 地址的地理位置信息。

获取 IP 地理位置

通过输入一个IP地址,解析并获取信息,比如国家、国家代码、省份、省份代码、城市、邮政编码、经纬度等等信息

api

  • ip-api.com
  • ip.taotao.com
  • 百度地图api
  • 新浪 iplookup

离线查询

  • 纯真库
  • GeoLite2
  • 埃文科技

需要速度用离线,需要准确率用在线,需要数据丰富性GeoLite2、埃文科技,需要免费用GeoLite2

需要下载GeoLite2-City.mmdb,地址:https://github.com/wp-statistics/GeoLite2-City.git,并将该包解压与下述代码放置同一文件夹。

需要安装geoip2模块:pip install geoip2

import re
import geoip2.database
reader = geoip2.database.Reader('GeoLite2-City.mmdb')
 
# 查询IP地址对应的物理地址
def ip_get_location(ip_address):
    # 载入指定IP相关数据
    response = reader.city(ip_address)
 
    #读取国家代码
    Country_IsoCode = response.country.iso_code
    #读取国家名称
    Country_Name = response.country.name
    #读取国家名称(中文显示)
    Country_NameCN = response.country.names['zh-CN']
    #读取州(国外)/省(国内)名称
    Country_SpecificName = response.subdivisions.most_specific.name
    #读取州(国外)/省(国内)代码
    Country_SpecificIsoCode = response.subdivisions.most_specific.iso_code
    #读取城市名称
    City_Name = response.city.name
    #读取邮政编码
    City_PostalCode = response.postal.code
    #获取纬度
    Location_Latitude = response.location.latitude
    #获取经度
    Location_Longitude = response.location.longitude
 
    if(Country_IsoCode == None):
        Country_IsoCode = "None"
    if(Country_Name == None):
        Country_Name = "None"
    if(Country_NameCN == None):
        Country_NameCN = "None"
    if(Country_SpecificName == None):
        Country_SpecificName = "None"
    if(Country_SpecificIsoCode == None):
        Country_SpecificIsoCode = "None"
    if(City_Name == None):
        City_Name = "None"
    if(City_PostalCode == None):
        City_PostalCode = "None"
    if(Location_Latitude == None):
        Location_Latitude = "None"
    if(Location_Longitude == None):
        Location_Longitude = "None"
 
    print('================Start===================')
    print('[*] Target: ' + ip_address + ' GeoLite2-Located ')
    print(u'  [+] 国家编码:        ' + Country_IsoCode)
    print(u'  [+] 国家名称:        ' + Country_Name)
    print(u'  [+] 国家中文名称:    ' + Country_NameCN)
    print(u'  [+] 省份或州名称:    ' + Country_SpecificName)
    print(u'  [+] 省份或州编码:    ' + Country_SpecificIsoCode)
    print(u'  [+] 城市名称 :       ' + City_Name)
    print(u'  [+] 城市邮编 :       ' + City_PostalCode)
    print(u'  [+] 纬度:            ' + str(Location_Latitude))
    print(u'  [+] 经度 :           ' + str(Location_Longitude))
    print('===============End======================')
 
 
# 检验和处理ip地址
def seperate_ip(ip_address):
    ip_match = r"^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[1-9]|0?[1-9]0)\.)(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){2}(?:25[0-4]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[1-9]|0?[1-9]0)$"
    ip_match_list = r"^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[1-9]|0?[1-9]0)\.)(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){2}(?:25[0-4]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[1-9])-(?:25[0-4]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[1-9]|0?[1-9]0)$"
 
    if re.match(ip_match, ip_address):
        try:
            ip_get_location(ip_address)
        except Exception as e:
            print(e)
    elif re.match(ip_match_list, ip_address):
        ip_start =  ip_address.split('-')[0].split('.')[3]
        ip_end = ip_address.split('-')[1]
        # 如果ip地址范围一样,则直接执行
        if(ip_start == ip_end):
            try:
                seperate_ip(ip_address.split('-')[0])
            except Exception as e:
                print(e)
        elif ip_start > ip_end:
            print('the value of ip, that you input, has been wrong! try again!')
            exit(0)
        else:
            ip_num_list =  ip_address.split('-')[0].split('.')
            ip_num_list.pop()
            for ip_last in range(int(ip_start), int(ip_end)+1):
                ip_add = '.'.join(ip_num_list)+'.'+str(ip_last)
                try:
                    ip_get_location(ip_add)
                except Exception as e:
                    print(e)
    else:
        print('Wrong type of ip address!')
        print('100.8.11.58  100.8.11.58-100  alike!')
        
if __name__ == '__main__':
    seperate_ip('39.99.228.188')
    
'''
================Start===================
[*] Target: 39.99.228.188 GeoLite2-Located 
  [+] 国家编码:        CN
  [+] 国家名称:        China
  [+] 国家中文名称:    中国
  [+] 省份或州名称:    Zhejiang
  [+] 省份或州编码:    ZJ
  [+] 城市名称 :       Hangzhou
  [+] 城市邮编 :       None
  [+] 纬度:            30.294
  [+] 经度 :           120.1619
===============End======================
'''

使用过的实例

将 GeoLite2-City.mmdb 导入到 utils/GeoLite2-City/GeoLite2-City.mmdb,下面的代码 utils/ip_geo.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import

"""
@Author  : cxq
@Time    : 2019/12/28 11:57
@File    : ip_geo.py
"""
import os
import ipaddress
from IPy import IP
from geoip2 import errors
import geoip2.database as gip

AddressNotFoundError = errors.AddressNotFoundError


class IpGeo:
    """支持with操作"""

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        del self

    def __del__(self):
        self.close()

    def __repr__(self):
        return "Object IpGeo(%s)" % self.mmdb_path

    __str__ = __repr__

    def __init__(self, mmdb_path=None):
        """
        传入一个mmdb 数据库文件, 建立一个连接,默认使用 GeoLite2-City.mmdb
        :param mmdb_path: 类型必须是(str, unicode), 必须是一个文件路径. 否则抛出断言异常 AssertionError
        """
        if not mmdb_path:
            mmdb_path = os.path.join(os.path.dirname(__file__), "GeoLite2-City/GeoLite2-City.mmdb")
        assert isinstance(mmdb_path, str), "{}不是str".format(mmdb_path)
        assert os.path.isfile(mmdb_path), "{}不是文件路径".format(mmdb_path)
        self.mmdb_path = mmdb_path
        self.__conn()

    def close(self):
        """
        关闭连接
        :return: None
        """
        if hasattr(self, "conn") and hasattr(self.conn, "close"):
            self.conn.close()

    def __conn(self):
        """
        建立连接
        :return: None
        """
        self.conn = gip.Reader(self.mmdb_path)

    def check_ipv4(self, address):
        """
        校验ipv4地址是否正确
        :param address: 一个ipv4地址, 类型是(str, unicode)
        :return: 第一个返回值是地址是否正确, 布尔型.
        第二个返回值分两种情况, 地址正确的前提下, 返回一个ipaddress.IPv4Address地址对象.
        地址错误的前提下, 返回一个ipaddress.AddressValueError异常
        """
        try:
            return True, ipaddress.IPv4Address(address)
        except ipaddress.AddressValueError as e:
            return False, e

    def check_ipv6(self, address):
        """
        校验ipv6地址是否正确
        :param address: 一个ipv6地址, 类型是(str, unicode)
        :return: 第一个返回值是地址是否正确, 布尔型.
        第二个返回值分两种情况, 地址正确的前提下, 返回一个ipaddress.IPv4Address地址对象.
        地址错误的前提下, 返回一个ipaddress.AddressValueError异常
        """
        try:
            return True, ipaddress.IPv6Address(address)
        except ipaddress.AddressValueError as e:
            return False, e

    def get_country(self, address):
        """
        # 不存在 数据库中的ip, 会报AddressNotFoundError, 可以从本模块导入AddressNotFoundError对象.
        # 不合法的ip地址, 会报系统默认的 ValueError
        :param address: 一个合法的IPv4或ipv6地址
        :return: 一个字典, 结构类似 {'en': u'China', 'cn': u'\u4e2d\u56fd'}
        """
        return self.get_country_and_city(address)["country"]

    def get_city(self, address):
        """
        # 不存在 数据库中的ip, 会报AddressNotFoundError, 可以从本模块导入AddressNotFoundError对象.
        # 不合法的ip地址, 会报系统默认的 ValueError
        :param address: 一个合法的IPv4或ipv6地址
        :return: 一个字典, 结构类似 {'en': u'Beijing', 'cn': u'\u5317\u4eac'}
        """
        return self.get_country_and_city(address)["city"]

    def get_country_and_city(self, address):
        """
        # 不存在 数据库中的ip, 会报AddressNotFoundError, 可以从本模块导入AddressNotFoundError对象.
        # 不合法的ip地址, 会报系统默认的 ValueError
        :param address: 一个合法的IPv4或ipv6地址
        :return: 一个字典, 结构类似: {
        'country': {'en': u'China', 'cn': u'\u4e2d\u56fd'},
        'city': {'en': u'Beijing', 'cn': u'\u5317\u4eac'}
        }
        """
        result = {"country": {"en": "", "cn": ""}, "city": {"en": "", "cn": ""}}
        try:
            res = self.conn.city(address)
            country_en = res.country.names.get("en") or ""
            country_cn = res.country.names.get("zh-CN") or ""
            city_en = res.city.names.get("en") or ""
            city_cn = res.city.names.get("zh-CN") or ""
            if country_en == u'Taiwan':
                country_en = u'China'
                country_cn = u'\u4e2d\u56fd\u53f0\u6e7e'
            if country_en == u'Hong Kong':
                country_en = u'China'
                country_cn = u'\u4e2d\u56fd\u9999\u6e2f'
            if country_en == u'Macao':
                if city_en == u'Macao':
                    country_en = u'China'
                    country_cn = u'\u4e2d\u56fd'
                else:
                    country_en = u'China'
                    country_cn = u'\u4e2d\u56fd\u6fb3\u95e8'
            result["country"] = {"en": country_en, "cn": country_cn}
            result["city"] = {"en": city_en, "cn": city_cn}
        except AddressNotFoundError:
            if IP(address).iptype() in ("PRIVATE", "LOOPBACK", "ULA"):
                result["country"] = {"en": u"local area network", "cn": u"\u5c40\u57df\u7f51"}  # 局域网
        finally:
            return result


if __name__ == "__main__":
    gip_path = r"./GeoLite2-City/GeoLite2-City.mmdb"
    # ip = "2001:abc:202:10::36"  # 法国的IP
    # ip = "122.100.160.253"  # 澳门的IP
    ip = "134.208.0.0"  # 台湾ip
    with IpGeo() as ret:
        print(ret.get_country_and_city(ip))
        print(ret.get_country(ip))
        print(ret.get_city(ip))

文章作者: Nico
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Nico !
  目录