如何编写Unix管道风格的Python代码(1)
如何编写Unix管道风格的Python代码(1)
看过 SICP 就知道,其实函数式编程中的map, filter 都可以看作是管道思想的应用。但其实管道的思想不仅可以在函数式语言中使用,只要语言支持定义函数,有能够存放一组数据的数据结构,就可以使用管道的思想。
一个日志处理任务
应用场景如下:
◆ 某个目录及子目录下有一些 web 服务器的日志文件,日志文件名以 access-log 开头
◆ 日志格式如下
81.107.39.38 - ... "GET /ply/ply.html HTTP/1.1" 200 97238 81.107.39.38 - ... "GET /ply HTTP/1.1" 304 - |
其中最后一列数字为发送的字节数,若为 ‘-’ 则表示没有发送数据
◆目标是算出总共发送了多少字节的数据,实际上也就是要把日志记录的没一行的最后一列数值加起来
我不直接展示如何用 Unix 管道的风格来处理这个问题,而是先给出一些“不那么好”的代码,指出它们的问题,最后再展示管道风格的代码,并介绍如何使用 generator 来避免效率上的问题。
问题并不复杂,几个 for 循环就能搞定:
sum = 0 for path, dirlist, filelist in os.walk(top): for name in fnmatch.filter(filelist, "access-log*"): # 对子目录中的每个日志文件进行处理 with open(name) as f: for line in f: if line[-1] == '-': continue else: sum += int(line.rsplit(None, 1)[1]) |
利用 os.walk 这个问题解决起来很方便,由此也可以看出 python 的 for 语句做遍历是多么的方便,不需要额外控制循环次数的变量,省去了设置初始值、更新、判断循环结束条件等工作,相比 C/C++/Java 这样的语言真是太方便了。看起来一切都很美好。
然而,设想以后有了新的统计任务,比如:
1.统计某个特定页面的访问次数
2.处理另外的一些日志文件,日志文件名字以 error-log 开头
完成这些任务直接拿上面的代码过来改改就可以了,文件名的 pattern 改一下,处理每个文件的代码改一下。其实每次任务的处理中,找到特定名字为特定 pattern 的文件的代码是一样的,直接修改之前的代码其实就引入了重复。
如果重复的代码量很大,我们很自然的会注意到。然而 python 的 for 循环实在太方便了,像这里找文件的代码一共就两行,哪怕重写一遍也不会觉得太麻烦。for 循环的方便使得我们会忽略这样简单代码的重复。然而,再怎么方便好用,for 循环无法重用,只有把它放到函数中才能进行重用。
(先考虑下是你会如何避免这里的代码的重复。下面马上出现的代码并不好,是“误导性”的代码,我会在之后再给出“更好”的代码。)
因此,我们把上面代码中不变的部分提取成一个通用的函数,可变的部分以参数的形式传入,得到下面的代码:
def generic_process(topdir, filepat, processfunc): for path, dirlist, filelist in os.walk(top): for name in fnmatch.filter(filelist, filepat): with open(name) f: processfunc(f) sum = 0 # 很遗憾,python 对 closure 中的变量不能进行赋值操作, # 因此这里只能使用全局变量 def add_count(f): global sum for line in f: if line[-1] == '-': continue else: sum += int(line.rsplit(None, 1)[1]) generic_process('logdir', 'access-log*', add_count) |
看起来不变和可变的部分分开了,然而 generic_process 的设计并不好。它除了寻找文件以外还调用了日志文件处理函数,因此在其他任务中很可能就无法使用。另外 add_count 的参数必须是 file like object,因此测试时不能简单的直接使用字符串。
评论关闭