Python食谱-1.13.访问字符串的子串

原文作者:Alex Martelli
中文编译:Tony (digitalsatori)

问题

如何访问一个字符串的其中一部分。比如,读入一个固定长度的记录,如何才能提取出记录中各字段的值?

解决方法

字符串'切片'的方法不错,但是每次只能提取一个字段:

afield = theline[3:8]

如果还要考虑每个字段的长度,那 struct.unpack 再合适不过了。比如:

import struct
# 获取5个字节长度的字符串,跳过3个字节,获取2个长度为8个字节的字符串,获取剩余的全部字符
baseformat = "5s 3x 8s 8s"
# 从baseformat中可以知道字符串theline的最短的长度(本例中为24,我们可以使用
# struct.calcsize 计算来获得)
numremain = len(theline) - struct.calcsize(baseformat)
# 创建带's'的格式化子串,然后使用unpack
format = "%s %ds" % (baseformat, numremain)
l, s1, s2, t = struct.unpack(format, theline)

如果你想要跳过(而不是如上获取)剩余的字符,你可以:

l, s1, s2 = struct.unpack(baseformat, theline[:struct.calcsize(baseformat)])

如果你想要以5个字节为界来分割字符串,你可以对字符串‘切片’使用‘列表解析’(LC--list comprehension):

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)为分割字符串的左右边界。

讨论

该食谱配方借鉴了Perl Cookbook的配方1.1的内容。Python的切片取代了Perl的substr.Perl的内置unpack函数和Python的struct.unpack很类似。Perl的方法较强大些,因为它可以接受*作为最后剩下的所有字符。Python则必须算出这些字符的长度。这不是什么大问题,因为对于上述的字符串提取操作一般都会封装在一个小小的函数中。Memoizing, 也称作自动缓存,可以帮助我们在以上的函数被多次调用时提高效率。因为它可以防止每次都为struct unpacking重新准备format。关于 memoizing 的更多内容请参考 Python食谱-18.5

本食谱配方对于Python编程来说,关键在于提醒你,对于提取子字符串 struct.unpack 是除了字符串切片方法以外的一个行之有效,有时甚至更好的方法。

对于上述的Python程序片断,当然应该封装到函数中。除了那些众所周知的好处外,封装使我们不需要每次都要在使用时去设法计算最后一个字段的长度。以下的函数的功能与解决方法中的第一个程序片断相同:

def fields(baseformat, theline, lastfield=False):
  # by how many bytes does theline exceed the length implied by
  # base-format (struct.calcsize computes exactly that length)
  numremain = len(theline)-struct.calcsize(baseformat)
  # complete the format with the appropriate 's' or 'x' field, then unpack
  format = "%s %d%s" % (baseformat, numremain, lastfield and "s" or "x")
  return struct.unpack(format, theline)

在以上的函数中有一个可选参数,lastfield,默认为False,当其为Fasle时,不提取baseformat未设定的最后一部分的内容,当其为True时则提取该部分内容。在函数体中 lastfield and "s" or "x" 与Python2.5的ternary operator的表示法: "s" if lastfield else "x" 一样。

如果要在循环中调用函数 fields ,使用(baseformat, len(theline), lastfield)元组作为key的memoizing(缓存)可以大大提高函数的执行效率:

def fields(baseformat, theline, lastfield=False, _cache={  }):
# build the key and try getting the cached format string
    key = baseformat, len(theline), lastfield
    format = _cache.get(key)
    if format is None:
        # no format string was cached, build and cache it
        numremain = len(theline)-struct.calcsize(baseformat)
        _cache[key] = format = "%s %d%s" % (
            baseformat, numremain, lastfield and "s" or "x")
    return struct.unpack(format, theline)

memoizing的原理是对相同的函数参数只进行一次format的运算,并将其保存在 _cache 字典中。正如其他的优化措施,我们也要测试memoizing是否确实提高了程序的效率。我测量的结果是,memoized的版本能提高执行速度大约30%到40%。这意味如果该函数不是你的程序执行效率的瓶颈的话,我们可以不用去做这种优化。

与上述的第一个使用LC的代码片断功能相同的函数为:

def split_by(theline, n, lastfield=False):
# cut up all the needed pieces
  pieces = [theline[k:k+n] for k in xrange(0, len(theline), n)]
  # drop the last piece if too short and not required
  if not lastfield and len(pieces[-1]) < n:
      pieces.pop( )
  return pieces

与上述最后一个代码片断相同功能的函数:

def split_at(theline, cuts, lastfield=False):
# cut up all the needed pieces
  pieces = [ theline[i:j] for i, j in zip([0]+cuts, cuts+[None]) ]
  # drop the last piece if not required
  if not lastfield:
      pieces.pop( )
  return pieces

在以上两例中,列表解析加字符串切片的方法要略优于struct.unpack的方法。

还有一个使用generator的完全不一样的解法:

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)

对于要循环遍历所有的提取字段的情况使用Generator方法是最合适不过的。这种循环遍历可能是隐含的,通过调用比如 ''.join 这种汇聚类型的方法来实现。如果你确实要获得一个包含所有字段的列表,你可以对generator使用内置函数 list, 比如:

list_of_fields = list(split_by(the_line, 5))

Leave a Response