mattintosh note

技術書典に出たい人生だった…

Python GeoIP 系のメモ

最近 Python で GeoIP を使うことが多いけど、なんか色々種類があってわからなくなってきたのでちょっとまとめておく。

  • GeoIP2
  • maxminddb
  • GeoIP

データベースファイル(GeoLite2-City.mmdb)は下記からダウンロードできる。

GeoIP2

GeoLite2-City.mmdb が使えるモジュール。インストールは apt または pip から。

Console

sudo apt install python3-geoip2
pip3 install geoip2

Reader クラスのインスタンスを作って city() で IP アドレスを渡せばよい。Reader クラスは __init__(self, fileish, locales=None, mode=0) となっているので mode に以下のいずれかを渡すことでモードの指定が可能。

MODE_MMAP_EXT - use the C extension with memory map.
MODE_MMAP     - read from memory map. Pure Python.
MODE_FILE     - read database as standard file. Pure Python.
MODE_MEMORY   - load database into memory. Pure Python.
MODE_FD       - the param passed via fileish is a file descriptor, not a path. This mode implies MODE_MEMORY. Pure Python.
MODE_AUTO     - try MODE_MMAP_EXT, MODE_MMAP, MODE_FILE in that order. Default.

書き方は2種類かな?

Python3

import geoip2.database
reader = geoip2.database.Reader('GeoLite2-City.mmdb')
response = reader.city('54.70.157.111')
reader.close()

Python3

import geoip2.database
with geoip2.database.Reader('GeoLite2-City.mmdb') as reader:
    response = reader.city('54.70.157.111')

戻り値は geoip2.models.City オブジェクト。

Response

<class 'geoip2.models.City'>
geoip2.models.City({'country': {'names': {'ru': 'США', 'es': 'Estados Unidos', 'fr': 'États-Unis', 'zh-CN': '美国', 'en': 'United States', 'pt-BR': 'Estados Unidos', 'de': 'USA', 'ja': 'アメリカ合衆国'}, 'geoname_id': 6252001, 'iso_code': 'US'}, 'location': {'accuracy_radius': 1000, 'time_zone': 'America/Los_Angeles', 'metro_code': 810, 'longitude': -119.7143, 'latitude': 45.8491}, 'continent': {'names': {'ru': 'Северная Америка', 'es': 'Norteamérica', 'fr': 'Amérique du Nord', 'zh-CN': '北美洲', 'en': 'North America', 'pt-BR': 'América do Norte', 'de': 'Nordamerika', 'ja': '北アメリカ'}, 'code': 'NA', 'geoname_id': 6255149}, 'traits': {'ip_address': '54.70.157.111'}, 'registered_country': {'names': {'ru': 'США', 'es': 'Estados Unidos', 'fr': 'États-Unis', 'zh-CN': '美国', 'en': 'United States', 'pt-BR': 'Estados Unidos', 'de': 'USA', 'ja': 'アメリカ合衆国'}, 'geoname_id': 6252001, 'iso_code': 'US'}, 'subdivisions': [{'names': {'ru': 'Орегон', 'es': 'Oregón', 'fr': 'Oregon', 'zh-CN': '俄勒冈州', 'en': 'Oregon', 'pt-BR': 'Oregão', 'de': 'Oregon', 'ja': 'オレ ゴン州'}, 'geoname_id': 5744337, 'iso_code': 'OR'}], 'postal': {'code': '97818'}, 'city': {'names': {'ru': 'Бордман', 'en': 'Boardman'}, 'geoname_id': 5714964}}, ['en'])

各キーに入っている値を見て見ると geoip2.records というオブジェクトで入っている。(_locales を除いて)raw は辞書で入っているので全体が欲しければこれを取り出すのが簡単。

Python Console

>>> pprint({k: type(v) for k, v in response.__dict__.items()})
{'_locales': <class 'list'>,
 'city': <class 'geoip2.records.City'>,
 'continent': <class 'geoip2.records.Continent'>,
 'country': <class 'geoip2.records.Country'>,
 'location': <class 'geoip2.records.Location'>,
 'maxmind': <class 'geoip2.records.MaxMind'>,
 'postal': <class 'geoip2.records.Postal'>,
 'raw': <class 'dict'>,
 'registered_country': <class 'geoip2.records.Country'>,
 'represented_country': <class 'geoip2.records.RepresentedCountry'>,
 'subdivisions': <class 'geoip2.records.Subdivisions'>,
 'traits': <class 'geoip2.records.Traits'>}

location には latitudelongitude 以外にもデータが入っているので Kibana で geo_point として扱う場合は編集が必要。また、Region Map を使って都道府県別にマッピングしたい場合は {国コード}-{区域コード} を組み合わせるので {country.iso_code}-{subdivisions.iso_code}(例: JP-01)みたいになるのだけど、subdivisions のキーが存在しないことがあるので確認した方がいいかも。

response.raw

{'city': {'geoname_id': 5714964, 'names': {'en': 'Boardman', 'ru': 'Бордман'}},
 'continent': {'code': 'NA',
               'geoname_id': 6255149,
               'names': {'de': 'Nordamerika',
                         'en': 'North America',
                         'es': 'Norteamérica',
                         'fr': 'Amérique du Nord',
                         'ja': '北アメリカ',
                         'pt-BR': 'América do Norte',
                         'ru': 'Северная Америка',
                         'zh-CN': '北美洲'}},
 'country': {'geoname_id': 6252001,
             'iso_code': 'US',
             'names': {'de': 'USA',
                       'en': 'United States',
                       'es': 'Estados Unidos',
                       'fr': 'États-Unis',
                       'ja': 'アメリカ合衆国',
                       'pt-BR': 'Estados Unidos',
                       'ru': 'США',
                       'zh-CN': '美国'}},
 'location': {'accuracy_radius': 1000,
              'latitude': 45.8491,
              'longitude': -119.7143,
              'metro_code': 810,
              'time_zone': 'America/Los_Angeles'},
 'postal': {'code': '97818'},
 'registered_country': {'geoname_id': 6252001,
                        'iso_code': 'US',
                        'names': {'de': 'USA',
                                  'en': 'United States',
                                  'es': 'Estados Unidos',
                                  'fr': 'États-Unis',
                                  'ja': 'アメリカ合衆国',
                                  'pt-BR': 'Estados Unidos',
                                  'ru': 'США',
                                  'zh-CN': '美国'}},
 'subdivisions': [{'geoname_id': 5744337,
                   'iso_code': 'OR',
                   'names': {'de': 'Oregon',
                             'en': 'Oregon',
                             'es': 'Oregón',
                             'fr': 'Oregon',
                             'ja': 'オレゴン州',
                             'pt-BR': 'Oregão',
                             'ru': 'Орегон',
                             'zh-CN': '俄勒冈州'}}],
 'traits': {'ip_address': '54.70.157.111'}}

ついでに geoip2.models.City オブジェクトの属性も見てみる。

Python Console

>>> print(*dir(response), sep='\n')
__class__
__delattr__
__dict__
__dir__
__doc__
__eq__
__format__
__ge__
__getattribute__
__gt__
__hash__
__init__
__le__
__lt__
__metaclass__
__module__
__ne__
__new__
__reduce__
__reduce_ex__
__repr__
__setattr__
__sizeof__
__str__
__subclasshook__
__weakref__
_locales
city
continent
country
location
maxmind
postal
raw
registered_country
represented_country
subdivisions
traits

🤔 ホスト名はわかるけど IP アドレスがわからない

socket.gethostbyname() で変換するとか。

Python3

import socket
socket.gethostbyname('elastic.co')

Response

'54.70.157.111'

dnspython を使うとか。こっちは nameservers 属性で問い合わせ先の変更ができる。戻り値は dns.resolver.Answer オブジェクトで、リストに出来るのでそれぞれから address 属性を取り出す。

Python3

import dns.resolver
resolver = dns.resolver.Resolver()
resolver.nameservers = ['1.1.1.1']
response = resolver.query('elastic.co')
list(response)
[x.address for x in response]

Response

[<DNS IN A rdata: 52.11.225.213>, <DNS IN A rdata: 54.70.157.111>]
['52.11.225.213', '54.70.157.111']

maxminddb

GeoIP2 よりもシンプル。

Console

sudo apt install python3-maxminddb
pip3 install maxminddb

Reader() または open_database() でデータベースファイルのパスを与えてインスタンスを作成する。open_database() ではモードの指定が出来る。モードについては help(maxminddb) を参照。

Python3

from pprint import pprint
import maxminddb
reader = maxminddb.Reader('GeoLite2-City.mmdb')
# reader = maxminddb.open_database('GeoLite2-City.mmdb', maxminddb.MODE_MMAP
response = reader.get('54.70.157.111')
print(type(response))
pprint(response)

戻り値は辞書型。

Response

<class 'dict'>

出力は GeoIP2 の raw とほぼ一緒。ただし、問い合わせた IP アドレスは含まれない。

Response

{'city': {'geoname_id': 5714964, 'names': {'en': 'Boardman', 'ru': 'Бордман'}},
 'continent': {'code': 'NA',
               'geoname_id': 6255149,
               'names': {'de': 'Nordamerika',
                         'en': 'North America',
                         'es': 'Norteamérica',
                         'fr': 'Amérique du Nord',
                         'ja': '北アメリカ',
                         'pt-BR': 'América do Norte',
                         'ru': 'Северная Америка',
                         'zh-CN': '北美洲'}},
 'country': {'geoname_id': 6252001,
             'iso_code': 'US',
             'names': {'de': 'USA',
                       'en': 'United States',
                       'es': 'Estados Unidos',
                       'fr': 'États-Unis',
                       'ja': 'アメリカ合衆国',
                       'pt-BR': 'Estados Unidos',
                       'ru': 'США',
                       'zh-CN': '美国'}},
 'location': {'accuracy_radius': 1000,
              'latitude': 45.8491,
              'longitude': -119.7143,
              'metro_code': 810,
              'time_zone': 'America/Los_Angeles'},
 'postal': {'code': '97818'},
 'registered_country': {'geoname_id': 6252001,
                        'iso_code': 'US',
                        'names': {'de': 'USA',
                                  'en': 'United States',
                                  'es': 'Estados Unidos',
                                  'fr': 'États-Unis',
                                  'ja': 'アメリカ合衆国',
                                  'pt-BR': 'Estados Unidos',
                                  'ru': 'США',
                                  'zh-CN': '美国'}},
 'subdivisions': [{'geoname_id': 5744337,
                   'iso_code': 'OR',
                   'names': {'de': 'Oregon',
                             'en': 'Oregon',
                             'es': 'Oregón',
                             'fr': 'Oregon',
                             'ja': 'オレゴン州',
                             'pt-BR': 'Oregão',
                             'ru': 'Орегон',
                             'zh-CN': '俄勒冈州'}}]}

GeoIP

(古い?)dat 形式のデータベースを使う方。new() または open()インスタンスを作成する。 open() の場合はデータベースファイルとモードを指定する。GeoIP2 と異なり、IP アドレスだけでなく record_by_name() でホスト名を与えることが出来る。

Python3

from pprint import pprint
import GeoIP
gi = GeoIP.open('/usr/share/GeoIP/GeoIPCity.dat', GeoIP.GEOIP_MEMORY_CACHE)
response = gi.record_by_name('elastic.co')
# response = gi.record_by_addr('54.70.157.111')
print(type(response))
pprint(response)

戻り値は辞書型。

Response

<class 'dict'>

GeoIP2 に比べると情報が少ないが、country_code3 を持っていたり、緯度経度情報が細かったりする。(ただしこれは日本の場合は都庁や皇居などであることが多く、ピンポイントで建物を示しているわけではない)

Response

{'area_code': 541,
 'city': 'Boardman',
 'country_code': 'US',
 'country_code3': 'USA',
 'country_name': 'United States',
 'dma_code': 810,
 'latitude': 45.869598388671875,
 'longitude': -119.68800354003906,
 'metro_code': 810,
 'postal_code': '97818',
 'region': 'OR',
 'region_name': 'Oregon',
 'time_zone': 'America/Los_Angeles'}