使用Redis查找IP所属城市

Published on 2017 - 06 - 08

通过将统计数据和日志存储到Redis里面,我们可以收集访客在系统中的行为信息。但是直到目前为止,我们都忽略了访客行为中非常重要的一个部分,那就是——这些访客是从哪里来的?为了回答这个问题,在这一节中,我们将构建一系列用于分析和载入IP所属地数据库的函数,并编写一个可以根据访客的IP地址来查找访客所在城市、行政区(州)以及国家的函数。让我们先来看看下面这个例子。

随着Fake Game公司的游戏越来越受追捧,来自世界各地的玩家也越来越多。尽管像Google Analytics这样的工具可以让Fake Game公司知道玩家主要来自哪些国家,但为了更深入地了解玩家,Fake Game公司还是希望自己能够知道玩家们所在的城市和州。而我们要做的就是将一个IP所属城市数据库载入Redis里面,然后通过搜索这个数据库来发现玩家所在的位置。

我们之所以使用Redis而不是传统的关系数据库来实现IP所属地查找功能,是因为Redis实现的IP所属地查找程序在运行速度上更具优势。另一方面,因为对用户进行定位所需的信息量非常庞大,在应用程序启动时载入这些信息将影响应用程序的启动速度,所以我们也没有使用本地查找表(local lookup table)来实现IP所属地查找功能。实现IP所属地查找功能首先要做的就是将一些数据表载入Redis里面,接下来的小节将对这个步骤进行介绍。

载入位置表格

为了开发IP所属地查找程序,我们将使用http://dev.maxmind.com/geoip/geolite提供的可免费使用的IP所属城市数据库作为测试数据。这个数据库包含两个非常重要的文件:一个是GeoLiteCity-Blocks.csv,它记录了多个IP地址段以及这些地址段所属城市的ID;另一个是GeoLiteCity-Location.csv,它记录了城市ID与城市名、地区名/州名/省名、国家名以及一些我们不会用到的其他信息之间的映射。

实现IP所属地查找程序会用到两个查找表,第一个查找表需要根据输入的IP地址来查找IP所属城市的ID,而第二个查找表则需要根据输入的城市ID来查找ID对应城市的实际信息(这个城市信息中还会包括城市所在地区和国家的相关信息)。

根据IP地址来查找城市ID的查找表由有序集合实现,这个有序集合的成员为具体的城市ID,而分值则是一个根据IP地址计算出来的整数值。为了创建IP地址与城市ID之间的映射,程序需要将点分十进制格式的IP地址转换为一个整数分值,代码清单9的ip_to_score()函数定义了整个转换过程:IP地址中的每8个二进制位会被看作是无符号整数中的1字节,其中IP地址最开头的8个二进制位为最高位。

def ip_to_score(ip_address):
    score = 0
    for v in ip_address.split('.'):
        score = score * 256 + int(v, 10)
    return score

在将IP地址转换为整数分值之后,程序就可以创建IP地址与城市ID之间的映射了。因为多个IP地址范围可能会被映射至同一个城市ID,所以程序会在普通的城市ID后面,加上一个_字符以及有序集合目前已有城市ID的数量,以此来构建一个独一无二的唯一城市ID。代码清单10展示了程序是如何创建IP地址与城市ID之间的映射的。

def import_ips_to_redis(conn, filename):                #A
    csv_file = csv.reader(open(filename, 'rb'))
    for count, row in enumerate(csv_file):
        start_ip = row[0] if row else ''                #B
        if 'i' in start_ip.lower():
            continue
        if '.' in start_ip:                             #B
            start_ip = ip_to_score(start_ip)            #B
        elif start_ip.isdigit():                        #B
            start_ip = int(start_ip, 10)                #B
        else:
            continue                                    #C

        city_id = row[2] + '_' + str(count)             #D
        conn.zadd('ip2cityid:', city_id, start_ip)      #E
#A Should be run with the location of the GeoLiteCity-Blocks.csv file
#B Convert the IP address to a score as necessary
#C Header row or malformed entry
#D Construct the unique city id
#E Add the IP address score and City ID

在调用import_ips_to_redis()函数并将所有IP地址都载入Redis之后,我们会像代码清单11所展示的那样,创建一个将城市ID映射至城市信息的散列。因为所有城市信息的格式都是固定的,并且不会随着时间而发生变化,所以我们会将这些信息编码为JSON列表然后再进行存储。

def import_cities_to_redis(conn, filename):         #A
    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]                            #B
        country = row[1]                            #B
        region = row[2]                             #B
        city = row[3]                               #B
        conn.hset('cityid2city:', city_id,          #C
            json.dumps([city, region, country]))    #C
#A Should be run with the location of the GeoLiteCity-Location.csv file
#B Prepare the information for adding to the hash
#C Actually add the city information to Redis

在将所需的信息全部存储到Redis里面之后,接下来要考虑的就是如何实现IP地址查找功能了。

查找IP所属城市

为了实现IP地址查找功能,我们在上一个小节已经将代表城市ID所属IP地址段起始端(beginning)的整数分值添加到了有序集合里面。要根据给定IP地址来查找所属城市,程序首先会使用ip_to_score()函数将给定的IP地址转换为分值,然后在所有分值小于或等于给定IP地址的IP地址里面,找出分值最大的那个IP地址所对应的城市ID。这个查找城市ID的操作可以通过调用ZREVRANGEBYSCORE命令并将选项START和NUM的参数分别设为0和1来完成。在找到城市ID之后,程序就可以在存储着城市ID与城市信息映射的散列里面获取ID对应城市的信息了。代码清单12展示了IP地址所属地查找程序的具体实现方法。

def find_city_by_ip(conn, ip_address):
    if isinstance(ip_address, str):                        #A
        ip_address = ip_to_score(ip_address)               #A

    city_id = conn.zrevrangebyscore(                       #B
        'ip2cityid:', ip_address, 0, start=0, num=1)       #B

    if not city_id:
        return None

    city_id = city_id[0].partition('_')[0]                 #C
    return json.loads(conn.hget('cityid2city:', city_id))  #D
#A Convert the IP address to a score for zrevrangebyscore
#B Find the uique city ID
#C Convert the unique city ID to the common city ID
#D Fetch the city information from the hash

通过find_city_by_ip()函数,我们现在可以基于IP地址来查找相应的城市信息并对用户的来源地进行分析了。

参考文档