47994


python3.x系です。

以下のようなテキストファイル（original.txt）があります。タブ区切りです。

### original.txt

[code]

    aaa data1_1 data1_2 data1_3
    aaa data2_1 data2_2 data2_3
    bbb data3_1 data3_2 data3_3
    ccc data4_1 data4_2 data4_3
    
[/code]

これを以下の３つのテキストファイルに分割したいのです。

### div001.txt

[code]

    aaa data1_1 data1_2 data1_3
    aaa data2_1 data2_2 data2_3
    
[/code]

### div002.txt

[code]

    bbb data3_1 data3_2 data3_3
    
[/code]

### div003.txt

[code]

    ccc data4_1 data4_2 data4_3
    
[/code]

・１列目のデータをキーワードにしてファイルを分割したい。  
・１列目のデータ（＝分割された後にできるファイル数）はこの例だと３つだが、何個であっても対応できるようにしたい

そこで、以下の部分までコードを考えました。

[code]

    import re
    
    with open("original.txt", "r") as fh_input:
        query_key = "" #キーワード
        list = [] #入力データの配列
        saved_key = "tekitou" #初期キーワード
        filenumber = 1 #ファイル番号用
        filename = "" #ファイル名用
    
        for line in fh_input:
            line_m = re.sub('[\r\n]+$', '', line)#改行コードの除去
            list = line_m.split('\t')#タブ区切りでリスト化
            query_key = list[0]
    
            if saved_key != query_key:
                filenumber_padded = '{0:03d}'.format(filenumber)
                filename = "div" + filenumber_padded + ".txt"
                filenumber += 1
    
                「何かのファイルハンドル」 = open(filename, "w")
                #ここに「filename」への書き込み処理がはいる
    
            saved_key = query_key
    
[/code]

とここまで書いて、ファイルハンドルを動的に（＝キーワードの種類数に合わせて自動的に）生成する方法を思いつかず、行き詰まりました。ファイルハンドルが動的に複数生成できれば、複数のファイルを開いて書き込むことができます。perlだと「ファイルハンドルとして未定義のスカラ変数が与えられたとき、ファイルハンドルを自動的に生成し変数に設定する」ということで、これを用いて動的に生成できるようです。しかし、pythonはどうも違うらしいのです。

そこで、ここから先どうしたらよいかお知恵を拝借したいのです。また、以上の方法だと、仮にファイルハンドルを動的に生成できたとしても、ファイルをいくつも同時に開きっぱなしにすることになる・・・かな。すると、original.txtが大きい場合、メモリに負担になりそうな気がします。この点もあわせてお知恵をお借りできればと思います。

よろしくおねがいします。


CC BY-SA 4.0
2018-08-31T15:04:06.567
47989
2018-09-03T07:44:46.653
2018-09-03T01:47:26.750
3060
29941
question
python python3 テキストファイル
pythonでテキストファイルの分割をしたい


ジェネレータを使ってみました(エラーチェックはしていません)。

[code]

    import sys
    
    def outputs():
      n = 1
      while True:
        yield open('div{0:03d}.txt'.format(n), 'a') 
        n += 1
    
    if __name__ == '__main__':
      out = outputs()
      files = {}
      with open(sys.argv[1], 'r') as f:
        for l in f:
          key = l.split('\t')[0]
          if key not in files:
             files[key] = next(out)
             files[key].truncate(0)
          files[key].write(l)
      [f.close() for f in files.values()]
    
[/code]

>
> 以上の方法だと、仮にファイルハンドルを動的に生成できたとしても、ファイルをいくつも同時に開きっぱなしにすることになる・・・かな。すると、original.txtが大きい場合、メモリに負担になりそうな気がします。

メモリ以外にもプロセスが同時にオープンできる file descriptor 数には上限値がありますので、対策が必要になります(以下は
Ubuntu/Linux の場合)。

[code]

    $ lsb_release -d
    Description:    Ubuntu 18.04.1 LTS
    $ uname -sr
    Linux 4.15.0-33-generic
    $ ulimit -n
    1024
    $ ulimit -Hn
    1048576
    
[/code]

letrec
さんの言われる「開いて閉じて」を繰り返すのも一手ですが、一行毎では少し効率がよろしくないと思いますので、一定量をバッファリングして定期的にファイルへ出力するなどすると良いかもしれません。


CC BY-SA 4.0
2018-08-31T17:24:38.860
47994
2018-08-31T17:31:23.273
2018-08-31T17:31:23.273
47989
answer


設問者のコメントを見る限りバッファリングやストリーミング処理は必要ないと思うので、オンメモリでの回答を示します。バッファリングが必要な場合はファイルストリームをappend
modeで開きread_csvのchunksizeという引数を設定すれば同じような考え方で処理できます。

pandasとnumpyを使用します。

[code]

    import numpy as np
    import pandas as pd
    
[/code]

要件を満たすような巨大なダミーデータ(1.2GB)を作成しテストしてみます、メモリが足りない場合は、N_sampleを小さくしてください。

[code]

    N_sample = 20000000
    dummy_idxs = [chr(n)*3 for n in range(ord('a'),ord('z')+1)] #リスト[aaa,bbb,...,zzz]を生成
    test_vals = np.random.random(N_sample*3).reshape((-1,3))
    test_idxs = np.random.choice(dummy_idxs,N_sample)
    dfdummy = pd.DataFrame(test_vals,index=test_idxs)
    dfdummy.to_csv('dummy.csv',sep='\t',header=None) #ダミーデータファイル作成
    
[/code]

tsvの読み込みには注意が必要なので2つ例示します  
pandasではデータ型(dtype)という概念があり、例えば数値として解釈できる文字列が入力されると自動的にfloat64とかにキャストされます。数値であっても文字列として処理したい場合は下のほうの記述のほうが安全です。メモリ消費量については僕の環境では上の記述は0.6GB程度、下の記述では4.8GB程度メモリを消費しています。

[code]

    df=pd.read_csv("dummy.csv",sep='\t',header=None)
    df=pd.read_csv("dummy.csv",sep='\t',header=None,dtype='object') 
    
[/code]

最後に目的の処理を記述します、リスト内包表記を使えば一行で書けますが割愛します。

[code]

    for idx in df[0].unique():
        df[df[0]==idx].to_csv(str(idx)+".csv",sep="\t",index=None,header=None)
    
[/code]

df[0]はキーワードを表している列。  
df[0].unique()はユニークなキーワードのリストです、この例ではdummy_idxsと同じです。  
df[df[0]==idx]という表現はboolean
indexingと呼ばれているもので、例えばdf[df[0]=='aaa']とするとキーワードが'aaa'となっている行だけを抽出できます。それを'aaa.csv'と名付け、tsv形式で保存しています。  
1.2GBのファイルでも2分程度で処理できます、1MBなら一瞬でしょう。他にもキーワードと行番号でソートしてからファイル処理をする方法なども考えられますが割愛します。


CC BY-SA 4.0
2018-08-31T20:24:05.180
47996
2018-08-31T20:24:05.180
25980
47989
answer


itertools.count, itertools.groupby を使って実装してみました。  
original.txtがキーでソートされていると理想的です。

[code]

    import itertools
    from collections import defaultdict
    
    # ファイル名の連番を生成
    c = itertools.count(1)
    
    def filename():
        """ファイル名を生成"""
        return 'div{0:03d}.txt'.format(c.__next__())
    
    def main():
        # １列目のデータとファイル名の関連を保持
        files = defaultdict(filename)
    
        with open('original.txt', 'r') as f:
            # １列目のデータをキーにグルーピング
            for k, v in itertools.groupby(f, lambda l: l.split('\t')[0]):
                with open(files[k], 'a') as out_file:
                    for l in v:
                        out_file.write(l)
    
    
    if __name__ == '__main__':
        main()
    
[/code]


CC BY-SA 4.0
2018-09-02T10:54:34.980
48024
2018-09-02T10:54:34.980
5375
47989
answer


このトピックを立ち上げたものです。回答いただいた皆さんありがとうございました。自己レスになりますが、ちょっと考えたことがありますので、以下書きます。

今日になって「もとのoriginal.txtで、1列目のキーワードがきちんとソートさえされていれば、動的にファイルハンドルを生成する必要なない」ということに気づきました。なんと私は愚かなのでしょうか。そこで、「予めソートされたoriginal.txtを使う」という前提なら、以下のようにできることに気づきました。

[code]

    import re
    import os
    
    with open("original.txt", "r") as fh_input:
        query_key = "" #キーワード
        list = [] #入力データの配列
        saved_key = "tekitou" #初期キーワード
        filenumber = 1 #ファイル番号用
        filename = "" #ファイル名用
        fh_output = open("dummy_file.txt", "w")#1回目用にダミーの書き込みファイルを開けておく。この行はなくてもプログラムは走るが、下部の「前キーワードのファイルハンドルの解放」のときに、if文の1回目だけは解放すべきファイルハンドルがないので、ちょっと気持ち悪い。
    
        for line in fh_input:
            line_m = re.sub('[\r\n]+$', '', line)#改行コードの除去
            list = line_m.split('\t')#タブ区切りでリスト化
            query_key = list[0]
    
            if saved_key != query_key:
                fh_output.close()#キーワードが変わったら前キーワードのファイルハンドルを解放
                filenumber_padded = '{0:03d}'.format(filenumber)
                filename = "div" + filenumber_padded + ".txt"
                filenumber += 1
    
                fh_output = open(filename, "w")#新キーワードでファイルハンドルを再設定
                fh_output.write(line_m + "\n")
                saved_key = query_key
    
            else:
                fh_output.write(line_m + "\n") 
    
        fh_output.close()
    
    os.remove("dummy_file.txt")
    
[/code]

他の方が提案された方法よりもかなり不格好で長いですが、初学者の私には一番理解し易い、逐次的な方法になりました。

さて、これを書いていて感じたことがあります。pythonのファイルハンドルのスコープはどうなっているのだろう？ということです。globalのように思えるのですが、はっきりと記載したサイトなどがなく、ちょっと引っかかっています。

ちなみにperlでは、ファイルハンドルは基本的にはglobalですが、

my $fh;  
open $fh, "< inputfile.txt";

という未定義の変数を用いたopenの方法だとlocalにすることもできるようです。pythonだとこの方法はエラーになるのかな、たぶん・・・。一方でperlは

my $fh = FILEHANDLE;  
open $fh, "< inputfile.txt";

という、定義済みの変数だとエラーになるようです（use strictの条件で）。pythonだとこの方法でlocalになる...のかな？

この他、余談ですが、perlだとprintでのファイル書き込みは、デフォルトでブロックバッファリング（一定のデータ量までバッファにデータを蓄積して、超えたら書き出し）のようです。しかし、pythonでは違うようで、ファイルクローズ時に一気に書き出しのようですね。ここでの皆さんの解答をみて初めて知りました。ありがとうございます。また、perlはopenしたファイルをcloseし忘れても、スクリプト終了時に勝手にcloseされるのですが、pythonはopenしっぱなしになるようで、上記スクリプトで最後の「fh_output.close()」をつけ忘れていたら、div.003.txtには何も書かれていませんでした。


CC BY-SA 4.0
2018-09-03T07:44:46.653
48045
2018-09-03T07:44:46.653
29941
47989
answer