在移动互联网的应用中,经常需要根据用户的位置信息等做一些统计和运算。拿到用户的位置,一般有两个方法:根据gps定位的信息和根据用户的ip地址。由于并不是每个手机都会打开gps,而且有时并不需要太精确的位置信息,所以根据ip地址来做一些划分反而是一个不错的选择。
互联网上流传着一些ip库,一些大公司内部会流传着一些ip地址(段)与地理位置的对应关系(请让我恶意揣测一下,这些库的名字会叫“哥伦布”,“麦哲伦”,“李四光”),一般来说,ip库的格式是这样的:
起始ip;结束ip-国家;省份;城市;blahblah
那现在一个实际的问题来了,如果给一个这样的ip库,每一行都是上面 描述的格式,怎么编写一个ip转地理位置的服务(这种服务一般处于基础服务的位置,每天可能要应付大量的查询,所以我们需要的不只是一个糙猛快的实现)。
下面是python写的一个实现的运行截图:
这是一个很有用的小工具,实现也不是很复杂,所以经常被用来问别人应该如何实现这样一个工具。下面介绍一个简单的实现。
1. ip字符串转unsigned
来回操作字符串可不行,所以先把ip地址转为一个4字节无符号整数是第一步要做的:
def ip_to_int(ip_str):
return reduce(lambda prev, curr: prev * 256 + int(curr),
ip_str.split('.'),
0)
2. ip地址搞定了,开始按照上面提到的格式,处理每一行ip段的地理位置信息,把每一行信息转换为pytho的一个turple:
def parse_ip_line(line):
info = line.split('-')
ip_info = info[0].split(';')
ip_start, ip_end = map(ip_to_int, ip_info)
geo_info = info[1].split(';')
geo_country, geo_province, geo_city, _, \
_, _ = map(lambda geo: geo != 'None' and geo or None,
geo_info)
return (ip_start, (ip_end, geo_country, geo_province, geo_city))
3. 每一行日志转换成一个turple之后,我们可以得到一个列表
[item0, item1, item2....]
列表中的每一个元素就是上面我们提到的那个turple
(ip_start, (ip_end, geo_country, geo_province, geo_city))
先不要着急,查找的时候我们显然不关心后面的国家啊那些信息,这些附带地信息会让列表中每一个元素占用的空间变大,导致在列表下标之间找来找去的时候太多的信息从内存放到处理器里面去(这点是我的臆测,并没有测试真正的影响有多大)。所以做下面这样的操作:把ip的起始值放到一个数组里去,每次只查找只操作一个int数组就好了:
rst = {'init_ips': [],
'ip_detail': {}}
with open(origin_file, 'r') as f:
for ln in f:
try:
init_ip, ip_detail = parse_ip_line(ln)
rst['init_ips'].append(init_ip)
rst['ip_detail'][init_ip] = ip_detail
except:
pass
rst中,init_ips储存了每一个ip段的起始ip,detail是一个字典,key就是起始ip,value是结束ip和其他信息。
4. ip查找
现在我们拿到了一个数组,如何查找一个值呢?显然是烂大街的查找算法了。如果不考虑“ip段”这个事,那肯定是这么写的(允许我偷懒,随便从so上粘贴一个下来 http://stackoverflow.com/questions/212358/binary-search-in-python):
def binary_search(a, x, lo=0, hi=None):
if hi is None:
hi = len(a)
while lo < hi:
mid = (lo+hi)//2
midval = a[mid]
if midval < x:
lo = mid+1
elif midval > x:
hi = mid
else:
return mid
return -1
关键的是下面这四行:
if midval < x:
lo = mid+1
elif midval > x:
hi = mid
这里面是判断,lo和hi之间的midval与你查找的数字的大小关系。i. >
判断大于很简单,如果midval比x大,那一定是大于,所以大于的判断不需要改。
ii. <
如果midval比x小,则需要满足:midval后面一个数字比x小,所以改成如下代码:
def binary_search(a, x, lo=0, hi=None):
if hi is None:
hi = len(a)
while lo < hi:
mid = (lo+hi)//2
midval = a[mid]
mn = a[mid+1]
if midval < x:
lo = mid+1
elif mn > x:
hi = mid
else:
return mid
return -1
拿到ip的起始值之后,就可以ip_detail那个字典来拿具体信息了,转成json,扔给请求的人。
5. 合并前面的操作
为了避免每次都要处理一次我们的ip库原始文件,可以把前面提到的rst使用pickle写道文件了,然后加载的时候直接从pickle里面取结果。
同样,为了避免每次去都pickle之后的文件,使用python的class variable来来存放我们的rst,然后使用tornado来写这样一个服务:
class IpHandler(tornado.web.RequestHandler):
ip_lookup = {}
def initialize(self):
if self.__class__.ip_lookup:
return
# init the lookup table
self.__class__.ip_lookup = ip_util.parse_ip_file()
if __name__ == "__main__":
application.listen(8000)
tornado.ioloop.IOLoop.instance().start()
最后,使用nohup来运行起来就好了:
nohup python ipconv.py 2>&1 >/dev/null
本来还想使用一颗什么树来储存ip段列表的,不过,目前这种方法似乎够用了,所以就没有继续再折腾下去了。