なるべく早くPythonで固定長ファイルシーク

Python で TS ファイルをパースするにあたって、まず Python でバイナリファイルを扱う方法についておさらいをしたいと思います。 open 関数の第2引数に 'rb' を与えればバイナリモードでファイルを開けます。開いたファイルオブジェクトを for ループで回すと、先読みバッファを使って効率的にファイルを読んでくれます。

import sys

with open(sys.argv[1], 'rb') as ts:
    for packet in ts:
        pass

手元の2世代前の MacBook Air 梅モデル (CPU 1.4 GHz Core 2 Duo, メモリ 2 GB 1067 MHz DDR3) で、30分の TS ファイルを読ませた結果、37秒ほどかかりました。

real    0m37.927s
user    0m22.847s
sys     0m8.328s

このアプローチには問題があります。TS ファイルは188バイト単位のパケットから成り立っているのに、変数 packet のサイズがまちまちなのです。

with open(sys.argv[1], 'rb') as ts:
    for packet in ts:
        print(len(packet))
388
103
92
77
428
114
423
157
154
66
362
21
32
21
41
2
^C6

iter 関数を使って、必ず 188 バイト返すようにしてみます。

with open(sys.argv[1], 'rb') as ts:
    packets = iter(lambda: ts.read(188), b'')
    for packet in packets:
        pass
real    0m48.537s
user    0m31.532s
sys     0m8.579s

10秒ほど遅くなってしまいました。

これは正直に 188 バイトずつファイルシークしているからで、一度にまとまった量をシークして 188 バイトずつ返すようにすれば解決しそうです。

def packets(ts):
    packet_size = 188
    chunk_size = 10000
    buffer_size = packet_size * chunk_size
    packets = iter(lambda: ts.read(buffer_size), b'')
    for packet in packets:
        for start, stop in zip(range(0, buffer_size - packet_size, packet_size),
                               range(packet_size, buffer_size, packet_size)):
            yield packet[start:stop]

with open(sys.argv[1], 'rb') as ts:
    for packet in packets(ts):
        pass

とりあえず 1880000 バイトを読んで、そこから188バイトずつ返すようにしてみました。

real    0m20.419s
user    0m0.226s
sys     0m5.098s

なかなかいいかんじです。

10000倍にしたのは適当だったのですが、1000倍でも100000倍でもこれより悪い結果になったので、おそらくこのあたりが最適なのでしょう。