2009-06-16 13 views
8

背景:私は大掃除だPythonクラスでソートされたファイルをマージするには、どのように改善できますか?

(メモリに保持することはできません)、タブ区切りファイル。入力ファイルを消去すると、メモリ内にリストが作成されます。 1,000,000エントリ(メモリで約1GB)になると、私はそれを(以下のデフォルトキーを使用して)ソートし、リストをファイルに書き出します。このクラスは、ソートされたファイルをまとめて戻すためのクラスです。これまでに遭遇したファイルで動作します。私の最大のケースは、これまでに66のソートされたファイルをマージしています。

質問:

  1. は私のロジックの穴は(ここではそれが壊れている)がありますか?
  2. マージソート アルゴリズムを正しく実装しましたか?
  3. 明らかに改善された はありますか?

例データ:

これは、これらのファイルのいずれかでラインを抽象化したものです:

'hash_of_SomeStringId\tSome String Id\t\t\twww.somelink.com\t\tOtherData\t\n'

お持ち帰りは私のソートキーとして'SomeStringId'.lower().replace(' ', '')を使用することです。

オリジナルコード:

class SortedFileMerger(): 
    """ A one-time use object that merges any number of smaller sorted 
     files into one large sorted file. 

     ARGS: 
      paths - list of paths to sorted files 
      output_path - string path to desired output file 
      dedup - (boolean) remove lines with duplicate keys, default = True 
      key - use to override sort key, default = "line.split('\t')[1].lower().replace(' ', '')" 
        will be prepended by "lambda line: ". This should be the same 
        key that was used to sort the files being merged! 
    """ 
    def __init__(self, paths, output_path, dedup=True, key="line.split('\t')[1].lower().replace(' ', '')"): 
     self.key = eval("lambda line: %s" % key) 
     self.dedup = dedup 
     self.handles = [open(path, 'r') for path in paths] 
     # holds one line from each file 
     self.lines = [file_handle.readline() for file_handle in self.handles] 
     self.output_file = open(output_path, 'w') 
     self.lines_written = 0 
     self._mergeSortedFiles() #call the main method 

    def __del__(self): 
     """ Clean-up file handles. 
     """ 
     for handle in self.handles: 
      if not handle.closed: 
       handle.close() 
     if self.output_file and (not self.output_file.closed): 
      self.output_file.close() 

    def _mergeSortedFiles(self): 
     """ Merge the small sorted files to 'self.output_file'. This can 
      and should only be called once. 
      Called from __init__(). 
     """ 
     previous_comparable = '' 
     min_line = self._getNextMin() 
     while min_line: 
      index = self.lines.index(min_line) 
      comparable = self.key(min_line) 
      if not self.dedup:      
       #not removing duplicates 
       self._writeLine(index) 
      elif comparable != previous_comparable: 
       #removing duplicates and this isn't one 
       self._writeLine(index) 
      else:         
       #removing duplicates and this is one 
       self._readNextLine(index) 
      previous_comparable = comparable 
      min_line = self._getNextMin() 
     #finished merging 
     self.output_file.close() 

    def _getNextMin(self): 
     """ Returns the next "smallest" line in sorted order. 
      Returns None when there are no more values to get. 
     """ 
     while '' in self.lines: 
      index = self.lines.index('') 
      if self._isLastLine(index): 
       # file.readline() is returning '' because 
       # it has reached the end of a file. 
       self._closeFile(index) 
      else: 
       # an empty line got mixed in 
       self._readNextLine(index) 
     if len(self.lines) == 0: 
      return None 
     return min(self.lines, key=self.key) 

    def _writeLine(self, index): 
     """ Write line to output file and update self.lines 
     """ 
     self.output_file.write(self.lines[index]) 
     self.lines_written += 1 
     self._readNextLine(index) 

    def _readNextLine(self, index): 
     """ Read the next line from handles[index] into lines[index] 
     """ 
     self.lines[index] = self.handles[index].readline() 

    def _closeFile(self, index): 
     """ If there are no more lines to get in a file, it 
      needs to be closed and removed from 'self.handles'. 
      It's entry in 'self.lines' also need to be removed. 
     """ 
     handle = self.handles.pop(index) 
     if not handle.closed: 
      handle.close() 
     # remove entry from self.lines to preserve order 
     _ = self.lines.pop(index) 

    def _isLastLine(self, index): 
     """ Check that handles[index] is at the eof. 
     """ 
     handle = self.handles[index]    
     if handle.tell() == os.path.getsize(handle.name): 
      return True 
     return False 

編集:Brianからの提案を実装するには、私は、次の解決策を考え出した:

第二編集:John Machinさんの提案あたりのコードの更新:

def decorated_file(f, key): 
    """ Yields an easily sortable tuple. 
    """ 
    for line in f: 
     yield (key(line), line) 

def standard_keyfunc(line): 
    """ The standard key function in my application. 
    """ 
    return line.split('\t', 2)[1].replace(' ', '').lower() 

def mergeSortedFiles(paths, output_path, dedup=True, keyfunc=standard_keyfunc): 
    """ Does the same thing SortedFileMerger class does. 
    """ 
    files = map(open, paths) #open defaults to mode='r' 
    output_file = open(output_path, 'w') 
    lines_written = 0 
    previous_comparable = '' 
    for line in heapq26.merge(*[decorated_file(f, keyfunc) for f in files]): 
     comparable = line[0] 
     if previous_comparable != comparable: 
      output_file.write(line[1]) 
      lines_written += 1 
     previous_comparable = comparable 
    return lines_written 

ラフテスト

同一の入力ファイル(データ2.2 GB)の使用:

  • SortedFileMergerクラス51 分(3068.4秒)を要し
  • Brianの溶液を取った40分(2408.5秒)
  • John Machinさんの提案を追加した後、 解決方法には36分かかりました (2214.0秒)
+0

decorated_fileが(Fにおけるラインの(キー(ライン)、ライン)) –

+0

@gnibblerに相当し、意志をスピードアップすることプロセスまたは単に機能を取り除く? – tgray

答えて

16

python2.6では、heapqにはこれを行う新しいmerge関数があります。

カスタムキー機能を処理するために、あなたはそれがキーに基づいて比較するようにそれを飾る何かでファイルイテレータをラップすることができ、その後、それを取り除く:

def decorated_file(f, key): 
    for line in f: 
     yield (key(line), line) 

filenames = ['file1.txt','file2.txt','file3.txt'] 
files = map(open, filenames) 
outfile = open('merged.txt') 

for line in heapq.merge(*[decorated_file(f, keyfunc) for f in files]): 
    outfile.write(line[1]) 

[編集]以前のバージョンのPythonであっても、後でheapqモジュールからmergeを実装するだけの価値があります。それは純粋なpythonでpython2.5で修正されずに実行されます。また、多数のファイルをマージするときにヒープを使用して次の最小値を得ることは非常に効率的です。

python2.6のインストールからheapq.pyをコピーして、ソースに "heapq26.py"とコピーして "from heapq26 import merge"を使用することができます。2.6特有の機能はありません。あるいは、マージ関数をコピーするだけで(heapopなどの呼び出しを書き換えてpython2.5 heapqモジュールを参照する)ことができます。

+0

実際、私はまだPython 2.5を使用しています。 – tgray

+0

これは素晴らしい答えですが、私は数週間Googleを検索し、これを見つけることができませんでした。 – tgray

2

< <この「答えは」元質問者の結果のコードにコメントです>>

提案:evalのを使用して()ummmmで、何をやっていることは、ラムダを使用して、発信者を制限 - キー抽出が必要な場合がありますいずれの場合でも予備ソートステップで同じ機能が必要なのではないでしょうか?

だからこれを置き換えます。これで

def mergeSortedFiles(paths, output_path, dedup=True, key="line.split('\t')[1].lower().replace(' ', '')"): 
    keyfunc = eval("lambda line: %s" % key) 

def my_keyfunc(line): 
    return line.split('\t', 2)[1].replace(' ', '').lower() 
    # minor tweaks may speed it up a little 

def mergeSortedFiles(paths, output_path, keyfunc, dedup=True):  
+0

ありがとう、eval()も私に嫌に感じましたが、私はその代替案を知らなかった。私はこのレシピからメソッドを入手しました:http://code.activestate.com/recipes/576755/ – tgray

+0

そのレシピは、キー抽出機能のソースを入力するのに十分な勇敢さを持つ人のためのオプション機能としてeval()複数のGBのソートを実行しているときにはコマンドラインに入ります:-)これはきれいに分かれています。マージとソートの両方の関数は、文字列ではなく、キーargに対して関数をとる。 –

関連する問題