1. ホーム
  2. python

[解決済み] 固定幅のファイルを効率的にパースするには?

2023-03-29 02:05:50

質問

固定幅の行を保持するファイルを解析する効率的な方法を見つけようとしています。たとえば、最初の20文字が列を表し、21:30から別の1つのようになります。

行が 100 文字を保持すると仮定して、行をいくつかのコンポーネントに解析する効率的な方法は何でしょうか。

行ごとの文字列スライスを使うこともできますが、行が大きいと少し醜いです。他に高速な方法はありますか?

どのように解決するのですか?

Pythonの標準ライブラリの struct モジュールはCで書かれているので、非常に簡単であり、非常に高速です。

以下は、あなたが望むことを行うために使用することができる方法です。また、フィールドの文字数に負の値を指定することで、文字の列をスキップすることができます。

import struct

fieldwidths = (2, -10, 24)  # negative widths represent ignored padding fields
fmtstring = ' '.join('{}{}'.format(abs(fw), 'x' if fw < 0 else 's')
                        for fw in fieldwidths)
fieldstruct = struct.Struct(fmtstring)
parse = fieldstruct.unpack_from
print('fmtstring: {!r}, recsize: {} chars'.format(fmtstring, fieldstruct.size))

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

出力します。

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

以下の修正により、Python 2または3でも動作するようになります(Unicodeの入力も扱えます)。

import struct
import sys

fieldstruct = struct.Struct(fmtstring)
if sys.version_info[0] < 3:
    parse = fieldstruct.unpack_from
else:
    # converts unicode input to byte string and results back to unicode string
    unpack = fieldstruct.unpack_from
    parse = lambda line: tuple(s.decode() for s in unpack(line.encode()))

ここで、あなたが検討していたが、あまりに醜くなるかもしれないと懸念していたように、文字列スライスを使用してそれを行う方法を紹介します。この方法の良いところは、それほど醜くないことに加えて、Python 2 と 3 の両方で変更なく動作し、Unicode 文字列を扱えることです。もちろん、速度的には struct モジュールに基づくバージョンよりも遅いですが、パディングフィールドを持つ能力を削除することによって、わずかにスピードアップすることができます。

try:
    from itertools import izip_longest  # added in Py 2.6
except ImportError:
    from itertools import zip_longest as izip_longest  # name change in Py 3.x

try:
    from itertools import accumulate  # added in Py 3.2
except ImportError:
    def accumulate(iterable):
        'Return running totals (simplified version).'
        total = next(iterable)
        yield total
        for value in iterable:
            total += value
            yield total

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(izip_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')