如何高效解析定宽文件?

新手上路,请多包涵

我试图找到一种有效的方法来解析包含固定宽度行的文件。例如,前 20 个字符代表一列,从 21:30 开始是另一列,依此类推。

假设该行包含 100 个字符,那么将一行解析为多个部分的有效方法是什么?

我可以每行使用字符串切片,但如果行很大,它会有点难看。还有其他快速方法吗?

原文由 hyperboreean 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 208
2 个回答

使用 Python 标准库的 struct 模块会相当容易,也相当快,因为它是用 C 编写的。下面的代码是如何使用它的。它还允许通过为字段中的字符数指定负值来跳过字符列。

 import struct

fieldwidths = (2, -10, 24)
fmtstring = ' '.join('{}{}'.format(abs(fw), 'x' if fw < 0 else 's') for fw in fieldwidths)

# Convert Unicode input to bytes and the result back to Unicode string.
unpack = struct.Struct(fmtstring).unpack_from  # Alias.
parse = lambda line: tuple(s.decode() for s in unpack(line.encode()))

print('fmtstring: {!r}, record size: {} chars'.format(fmtstring, struct.calcsize(fmtstring)))

line = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\n'
fields = parse(line)
print('fields: {}'.format(fields))

输出:

 fmtstring: '2s 10x 24s', recsize: 36 chars
fields: ('AB', 'MNOPQRSTUVWXYZ0123456789')

正如您正在考虑的那样,这是一种使用字符串切片来实现的方法,但担心它可能会变得太难看。 有点复杂和速度,它与基于 struct 模块的版本大致相同——尽管我有一个关于如何加速它的想法(这可能会使额外的复杂性变得值得)。请参阅下面关于该主题的更新。

 from itertools import zip_longest
from itertools import accumulate

def make_parser(fieldwidths):
    cuts = tuple(cut for cut in accumulate(abs(fw) for fw in fieldwidths))
    pads = tuple(fw < 0 for fw in fieldwidths) # bool values for padding fields
    flds = tuple(zip_longest(pads, (0,)+cuts, cuts))[:-1]  # ignore final one
    parse = lambda line: tuple(line[i:j] for pad, i, j in flds if not pad)
    # Optional informational function attributes.
    parse.size = sum(abs(fw) for fw in fieldwidths)
    parse.fmtstring = ' '.join('{}{}'.format(abs(fw), 'x' if fw < 0 else 's')
                                                for fw in fieldwidths)
    return parse

line = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\n'
fieldwidths = (2, -10, 24)  # negative widths represent ignored padding fields
parse = make_parser(fieldwidths)
fields = parse(line)
print('format: {!r}, rec size: {} chars'.format(parse.fmtstring, parse.size))
print('fields: {}'.format(fields))

输出:

 format: '2s 10x 24s', rec size: 36 chars
fields: ('AB', 'MNOPQRSTUVWXYZ0123456789')

更新

正如我所怀疑的那样, 一种方法可以使代码的字符串切片版本更快——在 Python 2.7 中使它的速度与使用 struct 的版本大致相同,但在 Python 3.x 中使它快 233%(以及其自身的未优化版本,其速度与 struct 版本大致相同)。

上面给出的版本所做的是定义一个 lambda 函数,它主要是一个在运行时生成一堆切片的限制的理解。

 parse = lambda line: tuple(line[i:j] for pad, i, j in flds if not pad)

这相当于如下语句,取决于 ijfor 循环中的值:

 parse = lambda line: tuple(line[0:2], line[12:36], line[36:51], ...)

然而,后者的执行速度是原来的两倍多,因为切片边界都是常量。

幸运的是,使用内置的 eval() 函数将前者转换和“编译”为后者相对容易:

 def make_parser(fieldwidths):
    cuts = tuple(cut for cut in accumulate(abs(fw) for fw in fieldwidths))
    pads = tuple(fw < 0 for fw in fieldwidths) # bool flags for padding fields
    flds = tuple(zip_longest(pads, (0,)+cuts, cuts))[:-1]  # ignore final one
    slcs = ', '.join('line[{}:{}]'.format(i, j) for pad, i, j in flds if not pad)
    parse = eval('lambda line: ({})\n'.format(slcs))  # Create and compile source code.
    # Optional informational function attributes.
    parse.size = sum(abs(fw) for fw in fieldwidths)
    parse.fmtstring = ' '.join('{}{}'.format(abs(fw), 'x' if fw < 0 else 's')
                                                for fw in fieldwidths)
    return parse

原文由 martineau 发布,翻译遵循 CC BY-SA 4.0 许可协议

我不太确定这是否有效,但它应该是可读的(而不是手动进行切片)。我定义了一个函数 slices 获取字符串和列长度,并返回子字符串。我把它变成了一个生成器,所以对于很长的行,它不会构建一个临时的子字符串列表。

 def slices(s, *args):
    position = 0
    for length in args:
        yield s[position:position + length]
        position += length

例子

In [32]: list(slices('abcdefghijklmnopqrstuvwxyz0123456789', 2))
Out[32]: ['ab']

In [33]: list(slices('abcdefghijklmnopqrstuvwxyz0123456789', 2, 10, 50))
Out[33]: ['ab', 'cdefghijkl', 'mnopqrstuvwxyz0123456789']

In [51]: d,c,h = slices('dogcathouse', 3, 3, 5)
In [52]: d,c,h
Out[52]: ('dog', 'cat', 'house')

但我认为,如果您同时需要所有列,那么生成器的优势就失去了。可以从中受益的地方是当您想一一处理列时,比如在循环中。

原文由 Reiner Gerecke 发布,翻译遵循 CC BY-SA 2.5 许可协议

推荐问题