如何使用正则表达式进行多次替换?

65

我可以使用以下代码,使用正则表达式将a替换为aa,创建一个新文件。

import re

with open("notes.txt") as text:
    new_text = re.sub("a", "aa", text.read())
    with open("notes2.txt", "w") as result:
        result.write(new_text)

我在想,我是否需要使用这一行代码:new_text = re.sub("a", "aa", text.read()) 多次,并将要更改的字符串替换为其他字母,以便在我的文本中更改多个字母?
也就是说,a-->aab--> bbc--> cc
因此,我是否需要为所有我想要更改的字母编写该行代码,还是有更简单的方法。也许创建一个“翻译”字典。我应该将那些字母放入数组中吗?如果我这样做,我不确定如何调用它们。
10个回答

89
@nhahtdh 提出的答案是有效的,但我认为不如规范示例那样符合 Python 的风格。规范示例使用的代码比他的正则表达式操作更易懂,并且利用了 Python 内置的数据结构和匿名函数特性。
在这种情况下,使用一个翻译字典是有道理的。事实上,Python Cookbook 就是这样做的,就像这个例子所展示的(从 ActiveState 修改而来:单次多重替换)。
import re 

def multiple_replace(replacements, text):
    # Create a regular expression from the dictionary keys
    regex = re.compile("(%s)" % "|".join(map(re.escape, replacements.keys())))
    # For each match, look-up corresponding value in dictionary
    return regex.sub(lambda mo: replacements[mo.group()], text) 

if __name__ == "__main__":
    s = "Larry Wall is the creator of Perl"
    d = {
        "Larry Wall": "Guido van Rossum",
        "creator": "Benevolent Dictator for Life",
        "Perl": "Python",
    }
    print multiple_replace(d, s)

所以在你的情况下,你可以创建一个字典 trans = {"a": "aa", "b": "bb"},然后将其与你想要翻译的文本一起传入 multiple_replace 中。基本上,这个函数所做的就是创建一个包含所有需要翻译的字符串的大正则表达式,当找到其中一个时,使用 regex.sub 中的 lambda 函数进行翻译字典查找。

例如,你可以在读取文件时使用这个函数:

with open("notes.txt") as text:
    new_text = multiple_replace(replacements, text.read())

with open("notes2.txt", "w") as result:
    result.write(new_text)

实际上,我在生产中确实使用了这种准确的方法,在一个需要将捷克语的年份翻译成英语的网络爬虫任务中。

正如 @nhahtdh 指出的那样,这种方法的一个缺点是它不是前缀自由的:作为字典键的某些键是其他字典键的前缀,这会导致该方法失效。


1
@Euridice01:如果您想忽略大小写,请在re.compile中指定re.I标志。 - nhahtdh
2
您当前的解决方案尚未配置用于存在一对单词的情况,其中一个是另一个的前缀。出现顺序很重要。我认为至少您应该说明这个假设。 - nhahtdh
2
我无法让re:I在这种情况下工作(根据@nhahtdh的建议)。 Penny:我看不出在这种情况下如何使用通配符。我已经尝试过了,但没有成功。 - thescoop
2
@thescoop:使用您的代码提出一个新问题。如果您想在映射中使用正则表达式,您需要重写函数以删除编译中的re.escape,并更改自定义替换函数以查找哪个组负责匹配并查找相应的替换(在这种情况下,输入应该是元组数组而不是字典)。 - nhahtdh
1
可以通过将mo.string [mo.start():mo.end()]替换为mo.group()来简化此过程,因为它们是等效的,正如文档此处所述。 - Johnny Mayhew
显示剩余11条评论

26
您可以使用捕获组和反向引用:
re.sub(r"([characters])", r"\1\1", text.read())

把想要重复的字符放在[]之间。例如小写字母abc
re.sub(r"([abc])", r"\1\1", text.read())

在替换字符串中,你可以使用 \n 符号来引用由捕获组 () 匹配的内容,其中 n 是一些正整数(0 不包括在内)。\1 引用第一个捕获组。还有另一种符号 \g<n>,其中 n 可以是任何非负整数(0 允许);\g<0> 将引用表达式匹配的整个文本。
如果您想将所有字符都加倍,除了换行符:
re.sub(r"(.)", r"\1\1", text.read())

如果您想将所有字符(包括换行符)都加倍:
re.sub(r"(.)", r"\1\1", text.read(), 0, re.S)

9

如果您的模式本身是正则表达式,那么其他解决方案都无法使用。

为此,您需要:

def multi_sub(pairs, s):
    def repl_func(m):
        # only one group will be present, use the corresponding match
        return next(
            repl
            for (patt, repl), group in zip(pairs, m.groups())
            if group is not None
        )
    pattern = '|'.join("({})".format(patt) for patt, _ in pairs)
    return re.sub(pattern, repl_func, s)

可以用于以下方面:

>>> multi_sub([
...     ('a+b', 'Ab'),
...     ('b', 'B'),
...     ('a+', 'A.'),
... ], "aabbaa")  # matches as (aab)(b)(aa)
'AbBA.'

请注意,此解决方案不允许您在正则表达式中放置捕获组,也不能在替换中使用它们。

这个解决方案比被接受的答案好多了。非常感谢! - Constantin Mateescu
Python 有什么变化导致这个出错了吗?我大约半年前在 Python 3.10 中成功地使用了这个片段。现在我注意到,在转换到 Python 3.11 的过程中,multi_sub 不再替换任何内容。我的代码没有改变,输入也没有改变,但输出现在完全没有修改。(当然,如果我将其拆分为多个 re.sub 调用,一切都能正常工作)。 - Daniel Saner

9
你可以使用 pandas 库和 replace 函数。这里给出一个包含五个替换的例子:
df = pd.DataFrame({'text': ['Billy is going to visit Rome in November', 'I was born in 10/10/2010', 'I will be there at 20:00']})

to_replace=['Billy','Rome','January|February|March|April|May|June|July|August|September|October|November|December', '\d{2}:\d{2}', '\d{2}/\d{2}/\d{4}']
replace_with=['name','city','month','time', 'date']

print(df.text.replace(to_replace, replace_with, regex=True))

修改后的文本如下:

0    name is going to visit city in month
1                      I was born in date
2                 I will be there at time

你可以在这里找到示例。

在我的情况下,这种方法的效率比直接使用正则表达式要低,也许有些情况下不是这样的? - Pablo
1
如果您想要在pandas数据框中使用矢量化一次应用多个替换, - George Pipis

6

使用如何使一个类具有字符串属性的技巧,我们可以创建一个与字符串完全相同但多了一个sub方法的对象:

import re
class Substitutable(str):
  def __new__(cls, *args, **kwargs):
    newobj = str.__new__(cls, *args, **kwargs)
    newobj.sub = lambda fro,to: Substitutable(re.sub(fro, to, newobj))
    return newobj

这允许使用构建器模式,看起来更好,但仅适用于预定数量的替换。如果在循环中使用它,则再创建额外类就没有意义了。例如:

>>> h = Substitutable('horse')
>>> h
'horse'
>>> h.sub('h', 'f')
'forse'
>>> h.sub('h', 'f').sub('f','h')
'horse'

3

我发现我需要修改Emmett J.Butler的代码,将lambda函数更改为使用myDict.get(mo.group(1),mo.group(1))。原始代码对我来说无法正常工作;使用myDict.get()还可以提供默认值,如果找不到键。

OIDNameContraction = {
                                'Fucntion':'Func',
                                'operated':'Operated',
                                'Asist':'Assist',
                                'Detection':'Det',
                                'Control':'Ctrl',
                                'Function':'Func'
}

replacementDictRegex = re.compile("(%s)" % "|".join(map(re.escape, OIDNameContraction.keys())))

oidDescriptionStr = replacementDictRegex.sub(lambda mo:OIDNameContraction.get(mo.group(1),mo.group(1)), oidDescriptionStr)

1

我不知道为什么大多数解决方案都试图组合一个单一的正则表达式模式,而不是多次替换。这个答案只是为了完整性。

话虽如此,这种方法的输出与组合正则表达式方法的输出不同。也就是说,重复的替换可能会随着时间的推移而演变文本。然而,以下函数返回与调用unix sed相同的输出:

def multi_replace(rules, data: str) -> str:
    ret = data
    for pattern, repl in rules:
        ret = re.sub(pattern, repl, ret)
    return ret

用法:

RULES = [
    (r'a', r'b'),
    (r'b', r'c'),
    (r'c', r'd'),
]
multi_replace(RULES, 'ab')  # output: dd

使用相同的输入和规则,其他解决方案将输出“bc”。根据您的用例,您可能希望或不希望连续替换字符串。在我的情况下,我想重新构建sed行为。此外,请注意规则的顺序很重要。如果您颠倒规则顺序,则此示例也将返回“bc”。

与将模式组合成单个正则表达式相比,此解决方案更快(快100倍)。因此,如果您的用例允许,应优先选择重复替换方法。


当然,你可以编译正则表达式模式:

class Sed:
    def __init__(self, rules) -> None:
        self._rules = [(re.compile(pattern), sub) for pattern, sub in rules]

    def replace(self, data: str) -> str:
        ret = data
        for regx, repl in self._rules:
            ret = regx.sub(repl, ret)
        return ret

1

如果你在处理文件,我有一个关于这个问题的简单Python代码。更多信息在这里

import re 

 def multiple_replace(dictionary, text):
  # Create a regular expression  from the dictionaryary keys

  regex = re.compile("(%s)" % "|".join(map(re.escape, dictionary.keys())))

  # For each match, look-up corresponding value in dictionaryary
  String = lambda mo: dictionary[mo.string[mo.start():mo.end()]]
  return regex.sub(String , text)


if __name__ == "__main__":

dictionary = {
    "Wiley Online Library" : "Wiley",
    "Chemical Society Reviews" : "Chem. Soc. Rev.",
} 

with open ('LightBib.bib', 'r') as Bib_read:
    with open ('Abbreviated.bib', 'w') as Bib_write:
        read_lines = Bib_read.readlines()
        for rows in read_lines:
            #print(rows)
            text = rows
            new_text = multiple_replace(dictionary, text)
            #print(new_text)
            Bib_write.write(new_text)

0

基于 Eric 优秀的答案, 我提出了一种更通用的解决方案,能够处理捕获组和反向引用:

import re
from itertools import islice

def multiple_replace(s, repl_dict):
    groups_no = [re.compile(pattern).groups for pattern in repl_dict]

    def repl_func(m):
        all_groups = m.groups()

        # Use 'i' as the index within 'all_groups' and 'j' as the main
        # group index.
        i, j = 0, 0

        while i < len(all_groups) and all_groups[i] is None:
            # Skip the inner groups and move on to the next group.
            i += (groups_no[j] + 1)

            # Advance the main group index.
            j += 1

        # Extract the pattern and replacement at the j-th position.
        pattern, repl = next(islice(repl_dict.items(), j, j + 1))

        return re.sub(pattern, repl, all_groups[i])

    # Create the full pattern using the keys of 'repl_dict'.
    full_pattern = '|'.join(f'({pattern})' for pattern in repl_dict)

    return re.sub(full_pattern, repl_func, s)

示例。 使用以下内容调用

s = 'This is a sample string. Which is getting replaced. 1234-5678.'

REPL_DICT = {
    r'(.*?)is(.*?)ing(.*?)ch': r'\3-\2-\1',
    r'replaced': 'REPLACED',
    r'\d\d((\d)(\d)-(\d)(\d))\d\d': r'__\5\4__\3\2__',
    r'get|ing': '!@#'
}

给出:

>>> multiple_replace(s, REPL_DICT)
'. Whi- is a sample str-Th is !@#t!@# REPLACED. __65__43__.'

为了更高效的解决方案,可以创建一个简单的包装器来预计算groups_nofull_pattern,例如:
import re
from itertools import islice

class ReplWrapper:
    def __init__(self, repl_dict):
        self.repl_dict = repl_dict
        self.groups_no = [re.compile(pattern).groups for pattern in repl_dict]
        self.full_pattern = '|'.join(f'({pattern})' for pattern in repl_dict)

    def get_pattern_repl(self, pos):
        return next(islice(self.repl_dict.items(), pos, pos + 1))

    def multiple_replace(self, s):
        def repl_func(m):
            all_groups = m.groups()

            # Use 'i' as the index within 'all_groups' and 'j' as the main
            # group index.
            i, j = 0, 0

            while i < len(all_groups) and all_groups[i] is None:
                # Skip the inner groups and move on to the next group.
                i += (self.groups_no[j] + 1)

                # Advance the main group index.
                j += 1

            return re.sub(*self.get_pattern_repl(j), all_groups[i])

        return re.sub(self.full_pattern, repl_func, s)

使用方法如下:

>>> ReplWrapper(REPL_DICT).multiple_replace(s)
'. Whi- is a sample str-Th is !@#t!@# REPLACED. __65__43__.'

0
在我的情况下,我只能使用表达式和re.sub,所以这里是一个单行解决方案(实际上是从其他解决方案中衍生出来的):
import re xx = "a&amp;b&lt;c"
yy = re.sub('&((amp)|(lt));', lambda x: ' and ' if x.group(2) else (' less ' if x.group(3) else ''), xx)

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