使用Python和DB-IP免費數據庫實現IP地理位置的查詢。復習一下二分查找算法。
我在工作中負責系統的日志部分,正好又對數據分析和挖掘有些興趣,所以想利用最近比較空閑的這段時間做一些日志分析方面的工作。之前沒有太多經驗,先從一些簡單的東西入手:看看用戶的地理分布情況。接入層日志中帶有請求來源的IP地址,我要做的就是提取請求IP,轉換成對應的地理位置,然后按區域統計。這里介紹一下我獲取地理位置的方法。
首先需要一個IP數據庫。谷歌之后有3個選擇:國外的DB-IP、MaxMind和國內的IPIP。其中后兩者不但提供免費的數據庫下載,還有封裝好的Python接口可以直接使用。不過什么都是拿來主義,就沒意思了。所以我決定使用DB-IP的數據庫,自己實現查找。這里使用DB-IP的城市級IP數據庫。數據庫以純文本的csv格式提供,每一行包含5列,對應一個IP地址段的起始地址、終止地址和國家、省份、城市等位置信息。下面是幾個例子:
"1.0.8.0","1.0.15.255","CN","Guangdong","Guangzhou"
"1.0.16.0","1.0.31.255","JP","Tokyo","Chiyoda"
"1.0.32.0","1.0.63.255","CN","Guangdong","Guangzhou"
"1.0.64.0","1.0.127.255","JP","Hiroshima","Hiroshima"
DB-IP的數據庫按照IP地址升序排列,因此這是個典型的有序序列查找問題,可以用二分查找來解決。首先要把文件讀進內存建立索引。IP地址其實是一個四字節(32位)的整數[1],寫成“192.168.1.1”這種形式只是為了方便人的閱讀和記憶,現在既然要讓機器來處理,不妨把它再轉換成整數。下面代碼實現了這個轉換過程。
from struct import pack, unpack
def ip2int(ip_str):
b = map(lambda x: int(x), ip_str.split('.'))
buf = pack('!BBBB', b[0], b[1], b[2], b[3])
o = unpack('!I', buf)[0]
return o
ip2int()函數的第一行將IP字符串按“.”分割成四部分,分別轉換成整數,然后放入一個list。這里使用了兩個函數式編程的小技巧,讓代碼更簡潔一些:
- map()函數。第一個參數是函數f,第二個參數是一個iterable對象(可以是list、tuple等),map函數將對參數2中的每一個對象調用函數f。
- lambda表達式。即匿名函數,lambda x表示該函數接受一個參數x,函數返回值就是“:”后面的表達式的值。
第二行將4個整數打包(pack)到一段4字節的緩沖區中,每個整數占一個字節,并以網絡序存放,第三行再以32位無符號整數(unsigned int)的形式將緩沖區解包(unpack)。這段利用了Python標準庫中的struct包提供的pack()和unpack()兩個函數,實現了將4個單字節整數合并成一個4字節整數的過程。如果用傳統的寫法,代碼可能是下面這個樣子:
o = 0
for x in b:
o = o << 8
o |= x
return o
load_ipdb()函數把數據庫文件讀入內存,每行記錄轉換成一個元組(tuple):(ip_start, ip_end, location),將所有元組依次追加到一個列表(list)中。由于文件本身是有序的,我們就得到了一個有序的索引。
def load_ipdb(file_path):
ip_range_list = []
with open(file_path) as f:
for line in f:
fields = line.strip().split(',')
fields = [f[1:-1] for f in fields]
if len(fields) != 5:
stderr.write(line)
continue
ip_start, ip_end, nation, province, city = fields
ip_start = ip2int(ip_start)
ip_end = ip2int(ip_end)
ip_range_list.append((ip_start, ip_end, province, city))
return ip_range_list
接下來就可以使用二分查找算法,根據給定的IP地址,找到對應的地址段,從而確定其地理位置。
def ip_lookup(ip_range_list, ip):
ip_bin = ip2int(ip)
min_idx = 0
max_idx = len(ip_range_list)
mid = 0
while True:
if min_idx > max_idx:
break
mid = (min_idx + max_idx) / 2
entry = ip_range_list[mid]
if ip_bin > entry[1]:
min_idx = mid + 1
continue
elif ip_bin < entry[0]:
max_idx = mid - 1
continue
else:
break
if ip_bin >= entry[0] and ip_bin <= entry[1]:
return entry[2]
else:
return None
DB-IP的數據庫包含了全球的IP地址,有630MB。而實際上我們感興趣的只是國內的部分,可以先篩選出國家代碼為CN的記錄,只需要一條grep命令,就可以大大縮短日志統計時查找地理位置的時間。最后附上一張根據統計結果繪制的熱度圖。繪圖使用的是百度ECharts??梢钥吹?,來自廣東的請求數量完爆其他省份,其次則是河南、河北、山東和江蘇這一片區域。用戶的熱度分布大致上跟各省的人口情況是相符的。
-
這里只討論IPv4,IPv6地址為6個字節。 ?