在Python中高效地执行多个字符串替换

8

如果我想进行多个字符串替换,最高效的方法是什么?

我在旅行中遇到的一种情况如下:

>>> strings = ['a', 'list', 'of', 'strings']
>>> [s.replace('a', '')...replace('u', '')  for s in strings if len(s) > 2]
['a', 'lst', 'of', 'strngs']

我不确定您打算如何使用查找表:键和值是什么?此外,您的示例有一些拼写错误/不一致之处。 - zdav
错别字在哪里?我使用省略号来截断示例以提高可读性。 - Tim McNamara
2个回答

12
你提供的特定示例(删除单个字符)非常适合使用字符串的translate方法,将单个字符替换为单个字符也是如此。如果输入字符串是Unicode,则除了上述两种“替换”之外,使用translate方法将单个字符替换为多个字符字符串也是可以的(但如果需要处理字节字符串,则不行)。
如果您需要替换多个字符的子字符串,则我还建议使用正则表达式--不过不是像@gnibbler的答案建议的那样;相反,我会从r'onestring|another|yetanother|orthis'构建正则表达式(使用垂直线连接要替换的子字符串-当然,如果它们包含特殊字符,还要re.escape),并编写一个基于字典的简单替换函数。
我现在不打算提供大量代码,因为我不知道哪个段落适用于您的实际需求,但是(等我回家检查SO时;-)我很乐意根据您对问题的编辑进行必要的修改以添加代码示例(比本答案的评论更有用;-)。 编辑:在评论中,OP说他想要一个“更通用”的答案(没有澄清这意味着什么),然后在他的Q的编辑中,他说他想研究各种片段之间的“权衡”所有都使用单个字符子字符串(检查其存在性,而不是像最初要求的替换-当然完全不同的语义)。
鉴于这种彻底的混乱,我只能说,为了“检查权衡”(在性能方面),我喜欢使用python -mtimeit -s'setup things here' 'statements to check'(确保要检查的语句没有副作用,以避免扭曲时间测量结果,因为timeit隐式循环以提供准确的时间测量)。
一般答案(没有任何权衡,并涉及多个字符子字符串,因此与他Q的编辑相矛盾,但与他的评论一致--两者完全相互矛盾,当然不可能同时满足):
import re

class Replacer(object):

  def __init__(self, **replacements):
    self.replacements = replacements
    self.locator = re.compile('|'.join(re.escape(s) for s in replacements))

  def _doreplace(self, mo):
    return self.replacements[mo.group()]

  def replace(self, s):
    return self.locator.sub(self._doreplace, s)

例子用法:

r = Replacer(zap='zop', zip='zup')
print r.replace('allazapollezipzapzippopzip')

如果要替换的一些子字符串是Python关键字,它们需要以略微不同的方式传递,例如下面这样:

r = Replacer(abc='xyz', def='yyt', ghi='zzq')

这会失败,因为def是一个关键字,所以您需要使用例如:

r = Replacer(abc='xyz', ghi='zzq', **{'def': 'yyt'})

我认为使用类(而不是过程式编程)是一个很好的选择,因为用于定位要替换的子字符串的正则表达式、表示要将其替换成什么的字典以及执行替换的方法真的应该“一起保持”,而类实例恰好是在 Python 中执行这种“保持在一起”的正确方式。闭包工厂也可以使用(因为replace方法实际上是实例中唯一需要在“外部”可见的部分),但可能不够清晰,难以调试:

def make_replacer(**replacements):
  locator = re.compile('|'.join(re.escape(s) for s in replacements))

  def _doreplace(mo):
    return replacements[mo.group()]

  def replace(s):
    return locator.sub(_doreplace, s)

  return replace

r = make_replacer(zap='zop', zip='zup')
print r('allazapollezipzapzippopzip')
唯一真正的优点可能是略微更好的性能(需要使用timeit在被认为对使用它的应用程序具有重要意义和代表性的“基准案例”上进行检查),因为在这种情况下对“自由变量”(replacementslocator_doreplace)的访问可能比在普通的基于类的方法中访问限定名称(例如self.replacements等)稍微快一些(这是否成立将取决于正在使用的Python实现,因此需要在重要的基准测试中使用timeit进行检查!)。

谢谢你详细的回答,Alex。我确实在寻找一个更一般性的答案。抱歉我的问题表述不够清晰。我会编辑问题以反映这一点。 - Tim McNamara

1
你可能会发现,创建一个正则表达式并一次性执行所有替换操作速度更快。
此外,将替换代码移出到一个函数中也是一个好主意,这样如果列表中有重复的内容,则可以进行记忆化处理。
>>> import re
>>> [re.sub('[aeiou]','',s) for s in strings if len(s) > 2]
['a', 'lst', 'of', 'strngs']


>>> def replacer(s, memo={}):
...   if s not in memo:
...     memo[s] = re.sub('[aeiou]','',s)
...   return memo[s]
... 
>>> [replacer(s) for s in strings if len(s) > 2]
['a', 'lst', 'of', 'strngs']

你能详细说明一下吗?re.sub('[aeiou]', '', s)会同时替换所有的元音字母吗?如果它是逐个字符检查,我担心 Python 字符串的不可变性。 - Tim McNamara
@Tim,是的,[aeiou]一次替换所有元音字母。不可变性并不是问题,因为您正在创建新的字符串。 - John La Rooy
@Tim 方括号 [] 是一个字符类,它匹配其中任何一个字符并用替换字符串替换该字符。如果所有替换都相同,并且您只匹配单个字符,则此解决方案非常好。 - zdav
我没有看到你的修改 - 你只是初始提供了关于正则表达式的答案。memo 参数是一个很好的补充!感谢你的帮助。 - Tim McNamara

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接