本文部分翻译自《Redis in Action》(Josiah L Carlson)。
利用IP定位用户以提供地方化的服务是目前Web的常用做法。使用Redis,我们可以很方便的实现该功能。
对于开发,我们可以从http://dev.maxmind.com/geoip/geolite下载免费的IP数据库。这个数据库包含两个重要的文件:Geo-LiteCity-Blocks.csv和GeoLiteCity-Location.csv,分别为IP段与城市ID的映射以及ID所对应的城市信息(如市、区/周/省、国家名称等)。
我们可以建立两张表,分别对应以上两个csv文件。
第一张表,我们可以将其放入ZSET中并以城市ID为member,以IP为score。当然,这里我们会对IP做一定的转换(即通过a * 256 * 256 * 256 + b * 256 * 256 + c * 256 + d转换为一个整数)以达到该目的,具体的代码实现如下:
def ip_to_score(ip_address):
score = 0
for v in ip_address.split('.'):
score = score * 256 + int(v, 10)
return score
接下来,我们就可以将数据库导入了,因为一个城市会对应多个IP,因此我们可以利用ZSET的特性,记录城市ID对应的第一个IP地址,通过IP范围来定位城市(具体之后来看)。以下是数据导入的代码:
def import_ips_to_redis(conn, filename):
csv_file = csv.reader(open(filename, 'rb'))
for count, row in enumerate(csv_file):
start_ip = row[0] if row else ''
if 'i' in start_ip.lower():
continue
if '.' in start_ip:
start_ip = ip_to_score(start_ip)
elif start_ip.isdigit():
start_ip = int(start_ip, 10)
else:
continue
city_id = row[2] + '_' + str(count)
conn.zadd('ip2cityid:', city_id, start_ip)
另外一张表当然就是城市ID与详情的映射关系,可以用HASH来实现:
def import_cities_to_redis(conn, filename):
for row in csv.reader(open(filename, 'rb')):
if len(row) < 4 or not row[0].isdigit():
continue
row = [i.decode('latin-1') for i in row]
city_id = row[0]
country = row[1]
region = row[2]
city = row[3]
conn.hset('cityid2city:', city_id,
json.dumps([city, region, country]))
之前,我们利用ZSET建立了一张城市ID与起始IP地址的对应表。要查询一个IP,我们首先需要使用与之前一样的办法,即将IP转换为10进制整数。之后找到比该IP值相等或较小的最大起始IP。
之前已经提到,之所以用到ZSET,就是方便这里的查询。即我们可以利用ZREVRANGEBYSCORE,通过传递START=0,NUM=1,从而实现预想的查询方法。正确获取城市ID后,我们再利用ID到城市ID与信息映射表(HASH)中查询到对应的城市信息。
具体的实现代码如下:
def find_city_by_ip(conn, ip_address):
if isinstance(ip_address, str):
ip_address = ip_to_score(ip_address)
city_id = conn.zrevrangebyscore(
'ip2cityid:', ip_address, 0, start=0, num=1)
if not city_id:
return None
city_id = city_id[0].partition('_')[0]
return json.loads(conn.hget('cityid2city:', city_id))
了解更多,可以参看Josiah L Carlson的《Redis in Action》这本书。