如前所述,有不同的方法,每种方法都有不同的优点。我使用三种不同的情况进行比较。
- 短字典(847 个替换对)
- 中型词典(2528 对)
- 长字典(80430 对)
对于字典 1 和 2(较短的),我将每个方法循环重复 50 次,以获得更一致的时间。对于一个较长的文件,一个文件的单次传递需要足够长的时间(可悲)。我使用 Python 3.8 使用在线 service tio 测试了 1 和 2。长的在我的笔记本电脑上用 Python 3.6 进行了测试。只有方法之间的相对性能是相关的,所以次要的细节并不重要。
我的字符串介于 28k 和 29k 个字符之间。
所有时间都以秒为单位。
更新:Flashtext
一位同事找到了Flashtext,Python 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.63
- 5.03
- 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.37 (1.39) (1.50)
- 4.10 (4.12) (4.07)
- 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)
执行时间为:
- 0.07
RecursionError
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”被单独留下。正则表达式可以用来修复标点符号,虽然我不需要这一步。