まるまろぐ

本ブログに記載の情報は全て飼い犬が書いたものであり、その内容に誤りや欠陥があった場合にも、私は一切の責任を負いません。

[Python]ファイルオブジェクトを特定の文字列で分割しながら読み込む

実行環境


たとえば、次のような txt ファイルがあったとします。

寒い冬が北方から、狐の親子の棲んでいる森へもやって来ました。
EOS
 或朝洞穴から子供の狐が出ようとしましたが、
「あっ」と叫んで眼を抑えながら母さん狐のところへころげて来ました。
EOS
「母ちゃん、眼に何か刺さった、ぬいて頂戴早く早く」と言いました。
EOS

このテキストをEOSごとに分割して何かをすることを考えます。 一番簡単な方法は、一度read()で読み込んでしまい、split()を使って分割することです。

with open('example.txt') as f:
    txt = f.read()
    txt_list = txt.split('EOS')
    for lines in txt_list:
        # 何か操作をする

これはうまくいきます。しかしファイルオブジェクトを一度に読み込んでしまっているので、ファイルサイズが小さい時は良いですが、大きすぎる時には避けたい手段です。
そこで、素直にEOSが来るまで各行をappend()してリストに保持しておき、EOSが来たらそのリストを処理することを考えます。

with open('example.txt') as f:
    lines = []
    for line in f:
        if line == 'EOS\n':
            for li in lines:
                # 何か操作をする
            lines = []
        else:
            lines.append(line)

これは先ほどより良いやり方ですが、EOSの間隔が極端に大きかった場合、結局一度にたくさん読み込んでしまうことになります。それに、できれば一度forで回したものを保持してまたforで回すというのは同じことの繰り返しなのでやりたくありません。
そこで、iter()を使うことを考えてみます。iter()公式ドキュメントにも記載されているように、第二引数に文字列を与えることで、その文字列が出てくると止まるイテレータを作ることができ、特定の文字列までファイルオブジェクトを読み込みたいときに便利です。

with open('example.txt') as f:
    while True:
        for line in iter(f.readline, 'EOS\n'):
            if line == '':
                break
            # 何か操作をする
        else:
            continue
        break

readlineは終端まで行くと空文字列を返すので、それを利用してループを抜けています。多重ループを抜けるためにfor-elsecontinueを使っています。これらについては公式ドキュメントのチュートリアルが、またPythonの多重ループの抜け方についてはこちらの記事が参考になるかと思います。
この方法は先ほどの問題を解決していますが、コードが複雑になります。そこで明示的にファイルの終端を求めることを考えてみます。

with open('example.txt') as f:
    f.seek(0,2)
    size = f.tell()
    f.seek(0)
    while f.tell() < size:
        for line in iter(f.readline, 'EOS\n'):
            # 何か操作をする

この方法は多重ループを抜けるために複雑な制御構文を書く必要がなく、for文を内包表記にすることも可能ですが、Pythonではファイルオブジェクトのサイズを一発で知ることはできないため、何行か余計に書く必要があります。 また、ファイルオブジェクト以外にこの手法を取ることはできません。

色々と検討してきましたが、どれも一長一短でたったひとつの冴えたやり方を見つけだすには至りませんでした。
最初の方法以外は一行ごとの読み込みであり純粋な分割ではないのも気になるところではあります(ほとんどの場合はメリットとして働くでしょうけど)。

指摘、追加、修正、質問などあればコメントまでよろしくお願いします。