【问题标题】:Efficiently make many multiple substitutions in a string有效地在字符串中进行多次替换
【发布时间】:2020-03-23 03:30:29
【问题描述】:

人们在如何根据字典在字符串中进行多次替换之前已经讨论过(请参阅for example)。似乎有一组基于string.replace 的选项和一组基于正则表达式的选项,还有几个选项。

但我对不同方法的效率感兴趣,具体取决于字典的大小,我发现这有非常重要的影响。

my_subs = {'Hello world': 'apple', 'is': 'ship'}
string = 'Hello world! This is nice.'
new_string = my_efficient_method(string, my_subs)

需要明确的是,这个问题不是关于如何进行这些替换,而是关于哪种方法在哪些条件下更有效,以及适用哪些注意事项。特别是,当有许多(> 100k)字符串很长(10-20k字符)并且字典很大(> 80k对)时,我正在寻找最实用的方法。在这些情况下,基于正则表达式的首选方法性能很差。

【问题讨论】:

    标签: python text replace


    【解决方案1】:

    如前所述,有不同的方法,每种方法都有不同的优点。我使用三种不同的情况进行比较。

    1. 短字典(847 个替换对)
    2. 中型词典(2528 对)
    3. 长字典(80430 对)

    对于字典 1 和 2(较短的),我将每个方法循环重复 50 次,以获得更一致的时间。对于一个较长的文件,一个文件的单次传递需要足够长的时间(可悲)。我使用 Python 3.8 使用在线 service tio 测试了 1 和 2。长的在我的笔记本电脑上用 Python 3.6 进行了测试。只有方法之间的相对性能是相关的,所以次要的细节并不重要。

    我的字符串介于 28k 和 29k 个字符之间。

    所有时间都以秒为单位。


    更新:Flashtext

    一位同事找到了FlashtextPython library 正是专门从事这方面的工作。它允许通过查询进行搜索并应用替换。它比其他替代方案快两个数量级。 在实验 3 中,我目前的最佳时间是 1.8 秒。 Flashtext 需要 0.015 秒


    正则表达式

    有很多变体,但the best 往往与此非常相似:

    import re
    rep = dict((re.escape(k), v) for k, v in my_dict.items())
    pattern = re.compile("|".join(rep.keys()))
    new_string = pattern.sub(lambda m: rep[re.escape(m.group(0))], string)
    

    执行时间为:

    1. 1.63
    2. 5.03
    3. 7.7


    替换

    此方法只是在循环中应用string.replace。 (稍后我会谈到这方面的问题。)

    for original, replacement in self.my_dict.items():
        string = string.replace(original, replacement)
    

    This solution 提出了一个使用reduce 的变体,它迭代地应用 Lambda 表达式。最好通过官方文档中的示例来理解这一点。表达式

    reduce(lambda x, y: x+y, [1, 2, 3, 4, 5])
    

    等于 ((((1+2)+3)+4)+5)

    import functools
    new_string = functools.reduce(lambda a, k: a.replace(*k), 
                                  my_dict.items(), string)
    

    Python 3.8 允许assignment expressions,如this method。这在其核心也依赖于string.replace

    [string := string.replace(f' {a} ', f' {b} ') for a, b in my_dict.items()]
    

    执行时间为(括号中的 results 表示 reduce 和 assignment 表达式 变体):

    1. 1.37 (1.39) (1.50)
    2. 4.10 (4.12) (4.07)
    3. 1.9 (1.8)(机器中没有 Python 3.8)


    递归 Lambda

    This proposal 涉及使用递归 Lambda。

    mrep = lambda s, d: s if not d else mrep(s.replace(*d.popitem()), d)
    new_string = mrep(string, my_dict)
    

    执行时间为:

    1. 0.07
    2. RecursionError
    3. RecursionError


    实战说明

    请参阅上面的更新:Flashtext 比其他替代品快得多。

    从执行时间可以看出,递归方法显然是最快的,但它只适用于小型字典。在 Python 中它是 not recommended to increase the recursion depth 很多,所以对于更长的字典,这种方法完全被丢弃了。

    正则表达式可以更好地控制您的替换。例如,您可以在元素之前或之后使用\b 以确保目标子字符串的那一侧没有单词字符(以防止将 {'a': '1'} 应用于'apple')。代价是更长的字典的性能会急剧下降,几乎是其他选项的四倍。

    赋值表达式reduce和简单循环replace提供类似的性能(赋值表达式无法用更长的字典测试)。考虑到可读性,string.replace 似乎是最好的选择。与正则表达式相比,这样做的问题是替换是按顺序发生的,而不是一次通过。所以 {'a': 'b', 'b': 'c'} 为字符串 'a' 返回 'c'。字典现在在 Python 中排序(但您可能想要 keep using OrderedDict),因此您可以仔细设置替换顺序以避免出现问题。当然,有 80k 替换你不能依赖这个。

    我目前正在使用带有替换的循环,并进行一些预处理以最大程度地减少麻烦。我在标点符号的两侧添加空格(也在字典中包含标点符号的项目)。然后我可以搜索被空格包围的子字符串,并用空格插入替换。当您的目标是多个单词时,这也适用:

    string = 'This is: an island'
    my_dict = {'is': 'is not', 'an island': 'a museum'}
    

    使用替换和正则表达式我得到string = ' This is : an island ' 以便我的替换循环

    for original, replacement in self.my_dict.items():
        string = string.replace(f' {original} ', f' {replacement} ')
    

    按预期返回' This is not : a museum '。请注意,“This”和“island”中的“is”被单独留下。正则表达式可以用来修复标点符号,虽然我不需要这一步。

    【讨论】:

    • (a) 你的reducereplaceassignment expression 策略基本上做同样的事情,所以它们都在循环中遇到与replace 相同的问题,(b) 有趣大字典如何更快,而我希望它更慢;我猜有很多长字符串被短字符串取代,所以字符串在早期变得更短了?
    • reduce 和 replace 是对的,它们基本上是一回事。也许我不够清楚:使用较短的字典,我将方法的重复计时 50 次!在长跑中,它是单次运行,慢得多。另请注意使用 SUPER FAST Flashtext 库进行的编辑!
    • 是的,我错过了。可能会直接将其添加到“结果”中。另外,我建议删除除基本循环之外的所有replace 变体,因为所有这些都只是同一主题的变体(更难阅读或有其他问题)。
    • 不知道为什么“递归 lambda” 这么快,但你可以用一种更易读的方式做同样的事情,并且没有递归问题 while d: s = s.replace(*d.popitem())
    • 顺便说一句,只是想建议使用 Trie,但这似乎正是 Flashtext 正在做的事情。
    猜你喜欢
    • 2011-08-15
    • 2019-04-19
    • 1970-01-01
    • 2010-09-22
    • 1970-01-01
    • 1970-01-01
    • 2011-09-30
    相关资源
    最近更新 更多