任务
需要计算一个文件中有多少行。
解决方案
对于尺寸不大的文件,最简单的方式是将文件读取放入一个行列表中,然后计算列表的长度即可。假设文件路径由变量 teflepath 指定,那么以这种方式实现的代码如下:
count = len(open(thefilepath,'rU').readlines())
对于非常大的文件,这种简单的处理方式极有可能会很慢,甚至会失败。如果你确实担心大文件的问题,用循环来计数是一个可行的办法:
count = -1
for count,line in enumerate(open(thefilepath,'rU')):pass
count +=1
如果行结束标记是"\n"(或者含有“\n”,就像 Windows 平台),我们还有一个更巧妙的,对于大文件也更快的方式:
count =0
thefile = open(thefilepath,'rb')
while True:buffer = thefile.read(8192*1024)if not buffer:breakcount += buffer.count('\n')
thefile.close()
给 open 的’rb’参数是必要的,如果你追求速度,那么没有那个参数,这段代码在 Windows上的运行可能会比较慢。
讨论
如果有外部程序提供文件行统计的功能,比如类UNIX平台中的wc-1,你当然也可以选择使用它们(比如,通过 os.popen)。但是,如果能够实现自己的行计算程序,代码通常会更简单、更快,也更具移植性。对于那些大小比较适合的文件,一次全部读取到内存中再处理是最简单的方式。对于这种文件,用len对readlines 返回的结果计算长度即可获取行数。
如果文件大到超过了可用的内存(比如,几百兆字节,对于今天的典型的个人计算机来说),这种最简单的方式将慢得根本无法接受。操作系统费尽九牛二虎之力,试图把文件内容放入虚拟内存,而且这个过程还可能失败,如果交换区空间耗尽,虚拟内存也无以为继。假设在一个典型的个人计算机上,装有256MB 内存和无限制的虚拟磁盘,你尝试一次性读取超过 1GB 或 2GB的文件,需要当心这个过程中可能发生的错误,不过这跟你使用的操作系统还有一些关系。(一些操作系统在极端的高负载压力下处理虚拟内存会比其他系统脆弱得多)在这个场合中,用循环来处理文件对象,如本节解决方案所示,会更好一些。内建的 enumerate 函数会自行计算行数,无须你用代码明确指定。
一次读取适量的字节,并计算其中的换行符,这是本节第三个处理方式的思路。这可能不是那么直观,而且也不能很完美地跨平台,但它可能是最快的办法(可以将它和Per Cookbook8.2节比较一下)。
然而,大多数时候,性能不是那么重要。如果性能的确值得考虑,你的第一感直觉也往往不能告诉你程序中真正耗时的代码段是哪个部分,事实上,你绝不应该相信直觉而应该进行基准测试。比如,有个典型的中等大小的 UNIXsyslog 文件,18MB 略多一点,230 000 行文本:
[situ@tioni nuc]$ we nuc
231581 2312730 18508908 nuc
考虑下面的基准测试框架脚本,bench.py:
import time
def timeo(fun,n=10):start = time.clock()for i in xrange(n):fun()stend = time.clock()thetime = stend-startreturn fun.__name__,thetime
import os
def linecount_w():return int(os.popen('wc -l nuc').read().split()[0])
def linecount_1():return len(open('nuc').readlines())
def linecount_2():count =-1for count,line inenumerate(open('nuc')):passreturn count+1
def linecount_3():count =0thefile=open('nuc',rb')while True:buffer=thefile.read(65536)if not buffer:breakcount += buffer.count(n')return countfor f in linecount_w,linecount_l,linecount_2,linecount_3:print f.__name__, f()for f in linecount_l,linecount_2,linecount_3:print "%s: %.2f"%timeo(f)
首先,将各种方法统计行数的结果打印出来,以确保没有什么错误或反常发生(众所周知,行统计任务会因为一点小错而失败)。然后,通过控制和计时函数timeo,我再将各个任务都运行10次,并观察结果。在一台可靠的老机器上,我的程序得出如下结果:
[situ@tioni nuc]$python -O bench.py
linecount_w 231581
linecount_1 231581
linecount_2 231581
linecount_3 231581
linecount_1:4.84
linecount_2:4.54
linecount_3:5.02
正如你所见的,性能差异几乎可以忽略:用户对这类辅助性任务从来都感觉不出10%的性能差异。然而,最快的方式却是简单朴实地循环遍历每一行(我的测试环境是,一台老旧但可靠的个人计算机,运行着一个流行的 Linux 发行版本),最慢的竟然是更具技巧性的逐次读取数据并计算换行符的方式。在实践中,除非需要处理非常大的文件,我一般总会选择最简单的方式(本节提供的第一个方法)。
准确地衡量代码的性能(要比盲目使用一些复杂的方式并寄希望能提高性能好的多)非常重要—重要到 Python 标准库要专门提供一个模块,timeit,用来测量程序的速度。我建议你用timeit,而不是用自己创造的一些测量方法,就像我在这里所做的。但是这个测试方法我多年前就在使用了,甚至比timeit 模块出现在 Python 标准库的时间还早,所以,在这个例子中我没有用 timeit 也算情有可原吧。