1.13 访问子字符串
任务
获取字符串的某个部分。比如,你读取了一条定长的记录,但只想获取这条记录中的某些字段的数据。
解决方案
切片是个好方法,但是它一次只能取得一个字段:
afield = theline[3:8]
如果还需考虑字段的长度,struct.unpack可能更适合。比如:
import struct
# 得到一个5字节的字符串,跳过3字节,得到两个8字节字符串,以及其余部分:
baseformat = "5s 3x 8s 8s"
# theline超出的长度也由这个base-format确定
#(在本例中是24字节,但struct.calcsize是很通用的)
numremain = len(theline) - struct.calcsize(baseformat)
# 用合适的s或x字段完成格式,然后unpack
format = "%s %ds" % (baseformat, numremain)
l, s1, s2, t = struct.unpack(format, theline)
如果想跳过“其余部分”,只需要给出正确的长度,拆解出theline的开头部分的数据即可:
l, s1, s2 = struct.unpack(baseformat, theline[:struct.calcsize(baseformat)])
如果需要获取5字节一组的数据,可以利用带列表推导(LC)的切片方法,代码很简单:
fivers = [theline[k:k+5] for k in xrange(0, len(theline), 5)]
将字符切成一个个单独的字符更加容易:
chars = list(theline)
如果想把数据切成指定长度的列,用带LC的切片方法通常是最简单的:
cuts = [8, 14, 20, 26, 30]
pieces = [ theline[i:j] for i, j in zip([0]+cuts, cuts+[None]) ]
在LC中调用zip,返回的是一个列表,其中每项都是形如(cuts[k], cuts[k+1])这样的数对,除了第一项和最后一项,这两项分别是(0, cuts[0])和(cuts[len(cuts)-1], None)。换句话说,每一个数对都给出了用于切割的正确的(i, j),仅有第一项和最后一项例外,前者给出的是切割之前的切片方式,后者给出的是切割完成之后到字符串末尾的剩余部分。LC利用这些数对就可以正确地将theline切分开来。
讨论
本节受到了Perl Cookbook 1.1的启发。Python的切片方法,取代了Perl的substr。Perl的内建的unpack和Python的struct.unpack也非常相似。不过Perl的手段更丰富一点,它可以用*来指定最后一个字段长度,并指代剩余部分。在Python中,无论是为了获取或者跳过某些数据,我们都得计算和插入正确的长度。不过这不是什么大问题,因为此类抽取字段数据的任务往往可以被封装成小函数。如果该函数需要反复被调用的话,memoizing,通常也被称为自动缓存机制,能够极大地提高性能,因为它避免了为struct.unpack反复做一些格式准备工作。参见第18.5节中关于memoizing的更多细节。
在纯Python的环境中,struct.unpack作为字符串切片的一种替代方案,非常好用(当然不能和Perl的substr比,虽然它不接受用*指定的区域长度,但仍是值得推荐的好东西)。
这些代码片段,最好被封装成函数。封装的一个优点是,我们不需要每次使用时都计算最后一个区域的长度。下面的函数基本上等价于“解决方案”小节给出的直接使用struct.unpack的代码片段:
def fields(baseformat, theline, lastfield=False):
# theline超出的长度也由这个base-format确定
#(通过struct.calcsize计算确切的长度)
numremain = len(theline)-struct.calcsize(baseformat)
# 用合适的s或x字段完成格式,然后unpack
format = "%s %d%s" % (baseformat, numremain, lastfield and "s" or "x")
return struct.unpack(format, theline)
一个值得注意(或者说值得批评)的设计是该函数提供了lastfield=False这样一个可选参数。这基于一个经验,虽然我们常常需要跳过最后的长度未知的部分,有时候我们还是需要获取那段数据。采用lastfield and s or x(等同于C语言中的三元运算符,lastfield?"s":"c")这样的表达式,我们省去了一个if/else,不过是否需要为这点紧凑牺牲可读性还有值得商榷之处。参看第18.9节中有关在Python中模拟三元运算符的内容。
若fields函数在一个循环内部被调用,使用元组(baseformat, len(theline), lastfield)作为key来充分利用memoizing机制将极大地提高性能。这里给出一个使用memoizing机制的fields版本:
def fields(baseformat, theline, lastfield=False, _cache={ }):
# 生成键并尝试获得缓存的格式字符串
key = baseformat, len(theline), lastfield
format = _cache.get(key)
if format is None:
# 没有缓存的格式字符串,创建并缓存之
numremain = len(theline)-struct.calcsize(baseformat)
_cache[key] = format = "%s %d%s" % (
baseformat, numremain, lastfield and "s" or "x")
return struct.unpack(format, theline)
这种利用缓存的方法,目的是将比较耗时的格式准备工作一次完成,并存储在_cache字典中。当然,正像所有的优化措施一样,这种采用了缓存机制的优化也需要通过测试来确定究竟能在多大程度上提高性能。对这个例子,我的测试结果是,通过缓存优化的版本要比优化之前快约30%到40%,换句话说,如果这个函数不是你的程序的性能瓶颈部分,其实没有什么必要多此一举。
“解决方案中”给出的另一个关于 LC的代码片段,也可以封装成函数:
def split_by(theline, n, lastfield=False):
# 切割所有需要的片段
pieces = [theline[k:k+n] for k in xrange(0, len(theline), n)]
# 若最后一段太短或不需要,丢弃之
if not lastfield and len(pieces[-1]) < n:
pieces.pop( )
return pieces
对最后一个代码片段的封装:
def split_at(theline, cuts, lastfield=False):
#切割所有需要的片段
pieces = [ theline[i:j] for i j in zip([0]+cuts, cuts+[None]) ]
# 若不需要最后一段,丢弃之
if not lastfield:
pieces.pop( )
return pieces
在上面这些例子中,利用列表推导来切片要比用struct.unpack略好一些。
用生成器可以实现一个完全不同的方式,像这样:
def split_at(the_line, cuts, lastfield=False):
last = 0
for cut in cuts:
yield the_line[last:cut]
last = cut
if lastfield:
yield the_line[last:]
def split_by(the_line, n, lastfield=False):
return split_at(the_line, xrange(n, len(the_line), n), lastfield)
当需要循环遍历获取的结果序列时,无论是显式调用,还是借助一些可调用的“累加器”,比如’’.join来进行隐式调用,基于生成器的方式都会更加合适。如果需要的是各字段数据的列表,你手上得到的结果却是一个生成器,可以调用内建的list来完成转化,像这样:
list_of_fields = list(split_by(the_line, 5))