IPアドレスから国名を知る

IPアドレスから国名を知る方法について、ときどき必要になったり忘れたりするので、メモ。

MaxMind社がCreate Commonsライセンスで公開してくれている、GeoLite2っていうデータベースを使わせてもらう。
http://dev.maxmind.com/geoip/geoip2/geolite2/

これは有料版のGeoIP2ってやつの簡易版らしいのだけど、十分な精度だと思う。

ここでダウンロードできる、GeoLite2 CountryのCSVフォーマットをつかう。
(Cityのほうは多分都市まで分かるようなものなんだろうけど、僕には用がないし、データは大量だし、中身を見たことはない。)

このデータの中身はこんなふう。

"1.0.0.0","1.0.0.255","16777216","16777471","AU","Australia"
"1.0.1.0","1.0.3.255","16777472","16778239","CN","China"
"1.0.4.0","1.0.7.255","16778240","16779263","AU","Australia"
...
"223.255.252.0","223.255.253.255","3758095360","3758095871","CN","China"
"223.255.254.0","223.255.254.255","3758095872","3758096127","SG","Singapore"
"223.255.255.0","223.255.255.255","3758096128","3758096383","AU","Australia"

This product includes GeoLite2 data created by MaxMind, available from http://www.maxmind.com.

6つのフィールドの1つめと2つめが、IPアドレスの範囲。3つめと4つめもIPアドレス範囲なんだけど、これはIPを整数値に直したもの。0から255までの値が4つ並ぶのがよく見るIPアドレスの姿なのだけど、これは 256*256*256*256 = 4294967296 までの整数でも表せるというわけ。

5つめと6つめのフィールドは、国名ですね。

このデータを、まずは扱いやすいようにB-treeにする。B-treeなら、並んだデータの頭出しがしやすいから。使うのはpython

import bsddb

sourcefile = 'GeoIPCountryWhois.csv'
btreefile = 'geo'

ipdb = bsddb.btopen(btreefile)

for line in open(sourcefile):
  a = line[:-1].split(",")
  a = [i.strip('"') for i in a]
  k = '%010d' % int(a[3])
  ipdb[k] = a[5]

整数であらわしたIPアドレスを、10桁にそろえてbtreeのキーに使っている。文字列しかキーとして扱ってくれないっぽいので、こんな感じになった。

このデータを実際に検索して国名を出すのは、こんな感じ。

import bsddb

btreefile = 'geo'
ipdb = bsddb.btopen(btreefile)

def ip2country(ip):
  a = [int(i) for i in ip.split(".")]
  n = (a[0]<<24) + (a[1]<<16) + (a[2]<<8) + a[3]
  c = ipdb.set_location('%010d' % n)
  return c[1]

print ip2country("13.14.15.16")

btreeを頭出しして、調べるIPの直前の並びにいるものが、たぶん調べるレコード。

この例では 13.14.15.16 を調べてみた。結果は "United States" で、あってるっぽい。

実際には終了位置もチェックしないと万全ではないのだけど、データを眺める限り、ほぼスキマなくIPが網羅されているようなので、別にいいんじゃないかな。(ローカルIPとかそういうのを調べたらダメだけど)

B-treeファイルを作りたくないな、というときには、メモリ上のリストを二分探索する方式でもいいね。↓がサンプル。

import bisect

sourcefile = 'GeoIPCountryWhois.csv'

iplist = []
iplistdict = {}

for line in open(sourcefile):
  a = line[:-1].split(",")
  a = [i.strip('"') for i in a]
  k = int(a[3])
  iplist.append(k)
  iplistdict[k] = a[5]

def ip2country(ip):
  a = [int(i) for i in ip.split(".")]
  n = (a[0]<<24) + (a[1]<<16) + (a[2]<<8) + a[3]
  i = bisect.bisect_left(iplist, n)
  return iplistdict[iplist[i]]

print ip2country("13.14.15.16")