上一篇文章:Python--Redis实战:第五章:使用Redis构建支持程序:第2节:计数器和统计数据
下一篇文章:Python--Redis实战:第五章:使用Redis构建支持程序:第4节:服务的发现与配置

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

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

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

载入城市表格

为了开发IP所属地查找程序,我们将使用一个IP所属城市数据库作为测试数据。这个数据库包含两个非常重要的文件:

  • 一个是GeoLiteCity-Blocks.csv,它记录了多个IP地址段以及这些地址段所属城市的ID
  • 另一个是GeoLiteCity-Location.csv它记录了城市ID与城市名、地区名、州县名以及我们不会用到的其他信息之间的映射。

实现IP所属地查找程序会用到两个查找表:

  • 第一个查找表需要根据输入的IP地址来查找IP所属城市的ID
  • 第二个查找表则需要根据输入的城市ID来查找ID对应城市的实际信息(这个城市信息中还会包括城市所在地区的其他信息)

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


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

if __name__ == '__main__':
    y=ip_ti_score('117.61.12.128')
    print(y)

    x=117
    x=x*256+61
    x=x*256+12
    x=x*256+128
    print(x)

运行结果:

1966935168
1966935168

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

import csv


#这个函数在执行时需要输入GeoLiteCity-Blocks.csv文件所在的路径
def import_ips_to_redis(conn,filename):
    csv_file=csv.reader(open(filename,'rb'))
    #enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,
    # 同时列出数据和数据下标,一般用在 for 循环当中。
    for count,row in enumerate(csv_file):
        start_ip=row[0] if row else ''
        #按照需要将IP地址转换为分值
        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
        #构建唯一的城市ID
        city_id=row[2]+'_'+str(count)
        #将城市ID及其对应的IP地址分值添加到有序集合里面。
        conn_zadd('ip2cityid:',city_id,start_ip)

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

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]))

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

查找IP所属城市

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

下面清单展示了IP地址所属地查找程序的具体实现方法:;

def find_city_by_ip(conn,ip_address):
    if isinstance(ip_address,str):
        #将IP地址转换为分值以便执行zrevrangebyscore命令
        ip_address=ip_to_score(ip_address)

    #查找唯一城市ID
    city_id=conn.zrevrangebyscore('ip2cityed:',ip_address,0,start=0,num=1)

    if not city_id:
        return None
    #partition() 方法用来根据指定的分隔符将字符串进行分割。
    # 如果字符串包含指定的分隔符,则返回一个3元的元组,
    # 第一个为分隔符左边的子串,第二个为分隔符本身,第三个为分隔符右边的子串。
    #将唯一城市ID转换为普通城市ID
    city_id=city_id[0].partition('_')[0]
    #从散列里面取出城市信息
    return json.loads(conn.hget('cityid2city:',city_id))

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

本节介绍的【将数据转换为整数并搭配有序集合进行操作】的做法非常有用,它可以极大简化对特定元素或特定范围的查找工作。

上一篇文章:Python--Redis实战:第五章:使用Redis构建支持程序:第2节:计数器和统计数据
下一篇文章:Python--Redis实战:第五章:使用Redis构建支持程序:第4节:服务的发现与配置

Mark
662 声望344 粉丝

talk is cheap,show me the code