Python类合并已排序的文件,如何对此进行改进?

Python类合并已排序的文件,如何对此进行改进?,python,merge,mergesort,large-file-support,Python,Merge,Mergesort,Large File Support,背景: 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

背景:

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
我正在清理大的(不能保存在内存中)制表符分隔的文件。当我清理输入文件时,我在内存中建立了一个列表;当它达到1000000个条目(内存中大约1GB)时,我对它进行排序(使用下面的默认键)并将列表写入一个文件。此类用于将已排序的文件重新组合在一起。它适用于我迄今为止遇到的文件。到目前为止,我最大的案例是合并66个排序文件

问题:

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
  • 我的逻辑有漏洞吗(哪里是脆弱的)
  • 我是否实现了合并排序 算法正确吗
  • 有什么明显的改进吗 那能做到吗
  • 示例数据:

    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
    
    这是其中一个文件中一行的抽象:

    'hash\u SomeStringId\t一些字符串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
    
    编辑:根据我的建议,我提出了以下解决方案:

    第二次编辑:根据的建议更新了代码:

    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的数据):

    • 分拣工班花了51分钟 分钟(3068.4秒)
    • 的解决方案耗时40分钟(2408.5秒)
    • 在添加了他的建议之后, 解决方案代码花了36分钟 (2214.0秒)

    请注意,在python2.6中,heapq有一个新函数,可以为您实现这一点

    要处理自定义键函数,只需使用修饰文件迭代器的内容包装文件迭代器,以便它基于键进行比较,然后将其删除:

    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])
    
    [Edit]即使在早期版本的python中,从后面的heapq模块中简单地实现merge也可能是值得的。它是纯python,在python2.5中未经修改地运行,并且由于它使用堆来获得下一个最小值,因此在合并大量文件时应该非常有效


    您应该能够简单地从python2.6安装中复制heapq.py,将其作为“heapq26.py”复制到源代码中,并使用“
    从heapq26导入合并”
    ”-其中没有使用2.6特定功能。或者,您可以复制合并函数(重写heappop等调用以引用python2.5 heapq模块)。

    事实上,我仍然在使用python2.5。这是一个很好的答案,虽然我在Google上搜索了几个星期都没有找到。谢谢,eval()对我来说也很奇怪,但我不知道其他方法。我从这个配方中得到了这个方法:这个配方只提供了eval()技巧作为可选功能,供那些有足够勇气在运行多GB排序时将密钥提取函数的源代码键入命令行的人使用:-)您会注意到这是完全分开的;merge和sort函数都为key arg使用一个函数,而不是string.u文件相当于((key(line),line)代表f中的line)@gnibler,这会加快处理速度还是干脆去掉该函数?