在Python中比较和替换键值对的最快方法

时间:2013-09-12 23:08:59

标签: python regex dictionary replace

我有很多文件要将特定字符串的所有实例替换为另一个。

我目前有这段代码:


    mappings = {'original-1': 'replace-1', 'original-2': 'replace-2'}

    # Open file for substitution
    replaceFile = open('file', 'r+')

    # read in all the lines
    lines = replaceFile.readlines()

    # seek to the start of the file and truncate
    # (this is cause i want to do an "inline" replace
    replaceFile.seek(0)
    replaceFile.truncate()

    # Loop through each line from file
    for line in lines:
        # Loop through each Key in the mappings dict
        for i in mappings.keys():
            # if the key appears in the line
            if i in line:
                # do replacement
                line = line.replace(i, mappings[i])
        # Write the line to the file and move to next line
        replaceFile.write(line)

这样做没问题,但是对于映射的大小和我正在处理的文件的大小来说速度非常慢。

例如,在“映射”字典中有60728个键值对。 我需要处理多达50个文件并用相应的值替换所有“key”实例,50个文件中的每一个大约是250000行。

还有多个实例需要在一行上替换多个键,因此我无法找到第一个匹配然后继续。

所以我的问题是:

有没有更快的方法来做到这一点? 我曾考虑使用正则表达式,但我不知道如何使用dict中的键/值对进行多个内联替换。

如果您需要更多信息,请与我们联系。

4 个答案:

答案 0 :(得分:1)

根据http://pravin.paratey.com/posts/super-quick-find-replace,正则表达式是Python的最快方法。 (构建Trie数据结构对于C ++来说是最快的):

import sys, re, time, hashlib

class Regex:

    # Regex implementation of find/replace for a massive word list.

    def __init__(self, mappings):
        self._mappings = mappings

    def replace_func(self, matchObj):
        key = matchObj.group(0)
        if self._mappings.has_key(key):
            return self._mappings[key]
        else:
            return key

    def replace_all(self, filename):
        text = ''
        with open(filename, 'r+') as fp
            text = fp.read()
        text = re.sub("[a-zA-Z]+", self.replace_func, text)
        fp = with open(filename, "w") as fp:
            fp.write(text)

# mapping dictionary of (find, replace) tuples defined 
mappings = {'original-1': 'replace-1', 'original-2': 'replace-2'}

# initialize regex class with mapping tuple dictionary
r = Regex(mappings)

# replace file
r.replace_all( 'file' )

答案 1 :(得分:1)

如果这种表现很慢,你必须找到一些奇特的东西。它几乎全部都是在C级运行:

for filename in filenames:
    with open(filename, 'r+') as f:
        data = f.read()
        f.seek(0)
        f.truncate()
        for k, v in mappings:
            data = data.replace(k, v)
        f.write(data)

请注意,您可以运行多个进程,其中每个进程都会处理整个文件列表的一部分。这应该使整个工作更快。没什么好看的,只需从shell运行多个实例,每个实例都有不同的文件列表。

显然是str.replace is faster than regex.sub


所以我要多考虑一下:假设你有一个非常巨大的mappings。这么多,以至于在您的文件中检测到mappings中的任何一个键的可能性非常低。在这种情况下,所有时间都将用于搜索(如@abarnert所指出的)。

在使用外来算法之前,multiprocessing至少可以用来并行进行搜索,然后在一个进程中进行替换(你不能在多个进程中进行替换,这是显而易见的原因:你如何结合结果?)。

所以我决定最终对multiprocessing有一个基本的了解,下面的代码看起来似乎可以正常工作:

import multiprocessing as mp

def split_seq(seq, num_pieces):
    # Splits a list into pieces
    start = 0
    for i in xrange(num_pieces):
        stop = start + len(seq[i::num_pieces])
        yield seq[start:stop]
        start = stop   

def detect_active_keys(keys, data, queue):
    # This function MUST be at the top-level, or
    # it can't be pickled (multiprocessing using pickling)
    queue.put([k for k in keys if k in data])

def mass_replace(data, mappings):
    manager = mp.Manager()
    queue = mp.Queue()
    # Data will be SHARED (not duplicated for each process)
    d = manager.list(data) 

    # Split the MAPPINGS KEYS up into multiple LISTS, 
    # same number as CPUs
    key_batches = split_seq(mappings.keys(), mp.cpu_count())

    # Start the key detections
    processes = []
    for i, keys in enumerate(key_batches):
        p = mp.Process(target=detect_active_keys, args=(keys, d, queue))
        # This is non-blocking
        p.start()
        processes.append(p)

    # Consume the output from the queues
    active_keys = []
    for p in processes:
        # We expect one result per process exactly
        # (this is blocking)
        active_keys.append(queue.get())

    # Wait for the processes to finish
    for p in processes:
        # Note that you MUST only call join() after
        # calling queue.get()
        p.join()

    # Same as original submission, now with MUCH fewer keys
    for key in active_keys:
        data = data.replace(k, mappings[key])

    return data

if __name__ == '__main__':
    # You MUST call the mass_replace function from
    # here, due to how multiprocessing works
    filenames = <...obtain filenames...>
    mappings = <...obtain mappings...>
    for filename in filenames:
        with open(filename, 'r+') as f:
            data = mass_replace(f.read(), mappings)
            f.seek(0)
            f.truncate()
            f.write(data)

一些注意事项:

  • 我还没有执行此代码!我希望有时候测试它,但是创建测试文件需要时间等等。请将它视为伪代码和有效python之间的某个位置。要让它运行起来应该不难。
  • 可以想象,使用多个物理机器应该非常容易,即具有相同代码的集群。 multiprocessing的文档显示了如何使用网络上的计算机。
  • 此代码仍然非常简单。我很想知道它是否能提高你的速度。
  • 使用多处理似乎有很多hackish警告,我试图在评论中指出。由于我还没有能够测试代码,可能是我还没有正确使用多处理。

答案 2 :(得分:1)

这个问题的缓慢部分是搜索,而不是替换。 (即使我错了,你可以通过首先搜索所有索引,然后从末尾分割和替换来轻松加速替换部分;它只是需要聪明的搜索部分。)

对于N长度的字符串和M个子字符串,任何天真的质量字符串搜索算法显然都是O(NM)(如果子字符串足够长,则可能更糟糕)。在每个位置搜索M次而不是在整个字符串上搜索M次的算法可能会提供一些缓存/分页优势,但它可能会复杂得多,可能只是一个小的好处。

所以,如果你坚持使用天真的算法,那么你不会比cjrh的实现做得更好。 (您可以尝试将其编译为Cython或在PyPy中运行它以查看它是否有帮助,但我怀疑它会有多大帮助 - 正如他解释的那样,所有内部循环都已经在C中。)

加速它的方法是以某种方式一次寻找许多子串。这样做的标准方法是构建前缀树(或后缀树),因此,例如,“original-1”和“original-2”都是相同子树“original-”的分支,因此它们不会需要单独处理,直到最后一个角色。

前缀树的标准实现是trie。但是,正如Efficient String Matching: An Aid to Bibliographic Search和维基百科文章Aho-Corasick字符串匹配算法所解释的那样,您可以通过使用带有额外回退链接的自定义数据结构来进一步优化此用例。 (IIRC,这通过logM改善了平均情况。)

Aho和Corasick通过从后备trie中编译有限状态机来进一步优化事物,这不适用于所有问题,但听起来就像是你的问题。 (你重复使用相同的映射50次。)

有许多变体算法具有额外的好处,因此可能值得进一步研究。 (常见用例是病毒扫描程序和程序包过滤器之类的东西,可能有助于您的搜索。)但我认为Aho-Corasick,或者甚至只是一个普通的trie,可能已经足够好了。

在纯Python中构建任何这些结构可能会增加很多开销,在M~60000时,额外的成本将破坏M / logM算法的改进。但幸运的是,你不必这样做。 PyPI上有many C-optimized trie implementationsat least one Aho-Corasick implementation。如果您认为后缀匹配可以更好地处理您的数据,那么也可能值得查看类似SuffixTree的内容,而不是颠倒使用通用的trie库。

不幸的是,如果没有您的数据集,其他任何人都很难进行有用的性能测试。如果您愿意,我可以编写使用几个不同模块的测试代码,然后您可以针对您的数据运行。但是这里有一个使用ahocorasick进行搜索的简单示例,以及替换为dumb的最终实现:

tree = ahocorasick.KeywordTree()
for key in mappings:
    tree.add(key)
tree.make()    
for start, end in reversed(list(tree.findall(target))):
    target = target[:start] + mappings[target[start:end]] + target[end:]

答案 3 :(得分:0)

这使用with块来防止泄漏文件描述符。字符串替换功能将确保在文本中替换所有键实例。

mappings = {'original-1': 'replace-1', 'original-2': 'replace-2'}

# Open file for substitution
with open('file', 'r+') as fd:

    # read in all the data
    text = fd.read()

    # seek to the start of the file and truncate so file will be edited inline
    fd.seek(0)
    fd.truncate()

    for key in mappings.keys():
        text = text.replace(key, mappings[key])

    fd.write(text)