Как я могу сделать несколько замен с помощью регулярных выражений в Python?

Я могу использовать этот код ниже, чтобы создать новый файл с заменой 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->aa,b-> bb а также c-> cc,

Поэтому я должен написать эту строку для всех букв, которые я хочу изменить, или есть более простой способ. Возможно создать "словарь" переводов. Должен ли я поместить эти буквы в массив? Я не уверен, как позвонить им, если я это сделаю.

10 ответов

Решение

Ответ, предложенный @nhahtdh, действителен, но я бы поспорил менее питонно, чем канонический пример, который использует код менее непрозрачный, чем его манипуляции с регулярным выражением, и использует преимущества встроенных структур данных Python и функции анонимных функций.

Словарь переводов имеет смысл в этом контексте. Фактически, именно так и поступает Поваренная книга Python, как показано в этом примере (скопировано с ActiveState http://code.activestate.com/recipes/81330-single-pass-multiple-replace/)

import re 

def multiple_replace(dict, text):
  # Create a regular expression  from the dictionary keys
  regex = re.compile("(%s)" % "|".join(map(re.escape, dict.keys())))

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

if __name__ == "__main__": 

  text = "Larry Wall is the creator of Perl"

  dict = {
    "Larry Wall" : "Guido van Rossum",
    "creator" : "Benevolent Dictator for Life",
    "Perl" : "Python",
  } 

  print multiple_replace(dict, text)

Так что в вашем случае вы могли бы сделать диктат trans = {"a": "aa", "b": "bb"} а затем передать его в multiple_replace вместе с текстом, который вы хотите перевести. По сути, все, что делает эта функция, - это создание одного огромного регулярного выражения, содержащего все ваши регулярные выражения для перевода, а затем, когда оно найдено, передача лямбда-функции в regex.sub выполнить перевод словаря поиска.

Вы можете использовать эту функцию при чтении из вашего файла, например:

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, одним из недостатков этого подхода является то, что он не свободен от префиксов: словарные ключи, являющиеся префиксами других словарных ключей, вызовут сбой метода.

Вы можете использовать группу захвата и обратную ссылку:

re.sub(r"([characters])", r"\1\1", text.read())

Поместите символы, которые вы хотите удвоить между [], Для случая нижнего регистра a, b, c:

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)

Вы можете использовать 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

Вы можете найти пример здесь

Ни одно из других решений не работает, если ваши шаблоны сами по себе являются регулярными выражениями.

Для этого вам понадобится:

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.'

Обратите внимание, что это решение не позволяет вам помещать группы захвата в регулярные выражения или использовать их для замены.

Используя советы о том, как создать "строковый" класс, мы можем сделать объект идентичным строке, но для дополнительного 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'

Я обнаружил, что мне пришлось изменить код Эммета Дж. Батлера, изменив лямбда-функцию для использования 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)

Если вы имеете дело с файлами, у меня есть простой код 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)

Я не знаю, почему большинство решений пытаются составить один шаблон регулярного выражения вместо многократной замены. Этот ответ только для полноты.

При этом вывод этого подхода отличается от вывода комбинированного подхода регулярных выражений. А именно, повторные замены могут изменять текст с течением времени. Однако следующая функция возвращает тот же результат, что и вызов 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

В моем контексте я могу использовать только выражения и 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)

Основываясь на отличном ответе Эрика , я придумал более общее решение, способное обрабатывать группы захвата и обратные ссылки:

      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_noиfull_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__.'
Другие вопросы по тегам