说明:本文是《Python 数据分析与数据化运营》中的“3.12.2 网络用户日志解析”。
-----------------------------下面是正文内容--------------------------
网络用户日志属于非结构化数据的一种,其解析方法根据不同的服务器配合和跟踪实施需要自定义模块,本节将以一个示例来演示如何进行日志解析。
本示例中,将使用正则表达式配合自定义函数模块实现日志解析功能。数据源文件 traffic_log_for_dataivy 位于“附件-chapter3”中,默认工作目录为“附件-chapter3”(如果不是,请 cd 切换到该目录下,否则会报“IOError: File traffic_log_for_dataivy does not exist”)。
背景:我们在这个网站上部署了 Google Analytics 的代码用来监测用户的行为,由于该工具是 SAAS 工作模式,数据采集之后直接发送到 Google 云端服务器。
我们通过一定的方式将每次发送给谷歌的数据同时“备份”了一条保存到本地服务器日志。其中日志请求内容的部分以 GET /__ua.gif? 开头便是日志记录。
我们的目标是找到这些日志,然后将日志做初步的解析,并存放到本地文件便于后期做进一步数据和应用。
要实现这一目标的基本思路是:先从日志文件夹中读取日志文件列表(本示例中仅有 1 个文件因此省略该步骤),然后依次读取每个日志文件中的数据,接着对日志文件中的每条数据进行判断,符合条件才能成为我们要的目标数据,最后将数据做初步解析并写到文件里面。
本节我们依然以函数的方式来撰写各个功能模块,如下是完整代码。
1# 去除爬虫功能
2def remove_spider_data(single_log):
3 # 定义排除爬虫规则集
4 exclude_set = [
5 'AhrefsBot',
6 'archive.org_bot',
7 'baiduspider',
8 'Baiduspider',
9 'bingbot',
10 'DeuSu',
11 'DotBot',
12 'Googlebot',
13 'iaskspider',
14 'MJ12bot',
15 'msnbot',
16 'Slurp',
17 'Sogou web spider',
18 'Sogou Push Spider',
19 'SputnikBot',
20 'Yahoo! Slurp China',
21 'Yahoo! Slurp',
22 'YisouSpider',
23 'YodaoBot',
24 'bot.html'
25 ]
26 count = 0 # 初始计数用于计算日志中是否包含爬虫
27 for spider in exclude_set: # 循环读取每个爬虫
28 if single_log.find(spider) != -1: # 如果爬虫出现在日志里面
29 count += 1 # count + 1
30 if count > 0: # 如果结果不为0,意味着日志中有爬虫
31 return 0 # 返回 0
32 else: # 否则
33 return 1 # 返回1
34
35# 读取日志数据
36def get_raw_log(file):
37 fn_read = open(file, 'r') # 打开要读取的日志文件对象
38 content = fn_read.readlines() # 以列表形式读取日志数据
39 fn_read.close() # 关闭文件对象
40 for single_log in content: # 循环判断每天记录
41 rule1 = single_log.find('GET /__ua.gif?') != -1 # 定义日志规则:含ua.gif的请求
42 rule2 = remove_spider_data(single_log)
43 if rule1 == True and rule2 == True: # 如果同时符合2条规则,则执行
44 fn_write = open('ua_data.txt', 'a+') # 打开要保存的ua日志文件对象
45 fn_write.writelines(single_log) # 写入符合规则的日志
46 fn_write.close() # 关闭文件对象
47
48# 解析每条日志数据
49def split_ua(line):
50 import re
51 # 定义不同日志分割的正则表达式
52 ip_rule = r'[\d.]*' # 定义IP规则,例如203.208.60.230
53 time_rule = r'\[[^\[\]]*\]' # 定义时间规则,例如[02/Mar/2016:14:00:23 +0800]
54 request_rule = r'\"[^\"]*\"' # 定义请求规则
55 status_rule = r'\d+' # 定义返回的状态码规则,例如200
56 bytes_rule = r'\d+' # 返回的字节数,例如326
57 refer_rule = r'\"[^\"]*\"' # 定义refer规则
58 user_agent_rule = r'\"[^\"]*\"' # 定义user agnet规则
59 # 原理:主要通过空格和-来区分各不同项目,各项目内部写各自的匹配表达式
60 log_re_pattern = re.compile(r'(%s)\ -\ -\ (%s)\ (%s)\ (%s)\ (%s)\ (%s)\ (%s)' % (
61 ip_rule, time_rule, request_rule, status_rule, bytes_rule, refer_rule, user_agent_rule), re.VERBOSE) # 完整表达式模式
62 matchs = log_re_pattern.match(line) # 匹配
63 if matchs != None: # 如果不为空
64 allGroups = matchs.groups() # 获得所有匹配的列表
65 ip_info = allGroups[0] # IP信息
66 time_info = allGroups[1] # 时间信息
67 request_info = allGroups[2] # 请求信息
68 status_info = allGroups[3] # 状态信息
69 bytes_info = allGroups[4] # 字节数信息
70 refer_info = allGroups[5] # refer信息
71 user_agent_info = allGroups[6] # user agent信息
72 return ip_info, time_info, request_info, status_info, bytes_info, refer_info, user_agent_info
73
74# 主程序
75def get_final_data():
76 file = 'traffic_log_for_dataivy' # 定义原始日志
77 get_raw_log(file) # 读取非结构化文本数据
78 fn_r = open('ua_data.txt', 'r') # 打开要读取的ua日志文件
79 content = fn_r.readlines() # 读取ua所有日志记录
80 fn_r.close() # 关闭ua文件对象
81 fn_w = open('final_data.txt', 'a+') # 打开要写入的格式化文件
82 for line in content: # 按行循环
83 ip_info, time_info, request_info, status_info, bytes_info, refer_info, user_agent_info = split_ua(
84 line) # 获得分割后的数据
85 log_line = ip_info + '!' + time_info + '!' + request_info + '!' + status_info + '!' + bytes_info + '!' + refer_info \
86 + '!' + user_agent_info # 按指定规则组合用于写入文件
87 fn_w.writelines(log_line) # 写入文件
88 fn_w.writelines('\n') # 写入换行符用于换行
89 fn_w.close() # 关闭写入的文件对象
90
91# 执行程序
92get_final_data() # 执行所有程序
上述代码以空行分为 5 个部分。
第一部分去除爬虫功能。
该函数块的功能是从日志中去除属于爬虫产生的数据。在日志中,很多搜索引擎公司的网络爬虫在抓取页面时会产生日志数据,除了谷歌外,还百度、搜狗、雅虎、有道、必应等都有类似的爬虫(或者称为机器人)。
- 代码中我们先定义了一个爬虫列表,所有我们怀疑为爬虫的关键字字段都可加到列表里面,用来做爬虫记录过滤;
- 然后我们通过
for 循环读取爬虫列表的每个字段,并在每行日志里通过 find 犯法查找是否存在,如果存在则在计数器上加 1(count += 1),一旦 count 大于 0,我们就认为在日志记录里面至少包含 1 个爬虫信息。
- 最终的返回结果为 0 代表有爬虫;1 代表没有爬虫。
第二部分读取日志数据。
先通过 open 方法以只读模式打开日志文件,然后通过 readlines 方法以列表的形式读取日志记录,读取完成之后关闭日志文件对象。
通过一个 for 循环将列表中的每条日志读取出来,然后定义了两条规则用于判断日志是否符合条件:
- 一条规则是日志中必须包含
GET/__ua.gif? 字符串,这是我们定义 Google Analytics 日志的标志,
- 另一条规则是日志中不能包含爬虫数据,直接使用在
remove_spider_data 功能模块的返回值做判断;
当在 if 条件语句中同时满足增加 2 个条件时,将该条日志记录添加到名为 ua_data.txt 的新文件中。
该部分代码执行后,会在 Python 程序当前工作目录下产生一个名为 ua_data.txt 的新文件。
第三部分解析每条日志数据。
该模块定义了日志下所有字段的分割规则,
- 首先我们针对日志记录中的要解析的每个数据字段定义了一个正则表达式构成的规则集,以用于目标数据的解析;
- 接着将分散的规则通过
re 库的 compile 方法合成一个匹配模式;
- 然后基于匹配模式使用
match 方法匹配每条数据记录,
- 并由此返回匹配的字段值列表,
- 最后解析出所有定义的字段值并返回给其他函数使用。
第四部分是主要程序模块。
这部分功能的意义是将前文提到的功能整合到该模块中。
- 先是定义了一个日志文件用于读取日志数据,通过之前定义的
get_raw_log 模块获取符合要求的日志数据并保存为单独的数据文件;
- 再读取该上述保存的日志数据并按行循环,在循环中,我们将每条日志进行分割并以
! 作为最终分隔符写入目标文件;
- 最后执行程序。
**总结:**上述过程看似略显复杂,目的是为了提高每个模块的可读性和可理解性,我们按照功能单独定义,因此“忽略”了总体考虑和最优化规则,此代码有很多地方可以优化:
import re 应该放到全局里面导入
- 从
get_raw_log 获得的数据,其实不必先写入文件再从文件读取,该过程可以省略(本节之所以先存后读是为了便于读者理解过程),所有程序最终可以只有 1 个结果文件
- 日志被分割后又进行组合并再次写入文件,通常实际中不会这样执行,因为已经格式化的数据可以放到数据库里面。
- 分割后的
request_info 模块的数据还没有解析,原因是没有应用场景需求做支撑,所以暂时不做处理。该字段中每个请求的数据主体(URL 中 ? 之后的部分)都是以 key-value 形式存在的键值对,解析起来非常容易。但是,由于不同的请求里面包含的 key(即参数)可能不同,而且系统预定义的 key 的值域数量非常大(例如 il<listIndex>pi<productIndex>cm<metricIndex> 的最大笛卡尔为 200*200*200=8000000),因此这些数据无法直接分列存储到一张关系型数据库表里面。解决思路有两种:一是将字段按主题拆分,形成数据仓库或数据集市;二是使用非关系型数据仓库,直接支持海量数据(行和列)的扩展。目前两种方式都有公司在实行。
上述过程中,主要需要考虑的关键点是:
- 如何根据不同的服务器日志配置以及前端代码跟踪实施的具体情况,编写日志过滤规则。
- 有关爬虫数据的排除,也是需要额外注意的信息点。
代码实操小结:本小节示例中,主要用了几个知识点:
- 对文本文件的读写操作
- 通过
find 方法查找符合条件的字符串
- 通过
if 做多条件判断,并实现对符合条件的记录做处理
- 通过
re 的正则表达式功能,实现对于特定字段的查找和匹配
- 通过定义
function 函数来实现特定功能或返回特定结果
- 通过
for 循环读取数据列表