Python reemplaza varias cadenas mientras soporta referencias

Hay algunas formas agradables de manejar el reemplazo simultáneo de cadenas múltiples en Python. Sin embargo, estoy teniendo problemas para crear una función eficiente que pueda hacer eso mientras que también admita referencias inversas.

Lo que me gustaría es usar un diccionario de expresiones / términos de reemplazo, donde los términos de reemplazo puedan contener referencias a algo que coincida con la expresión.

por ejemplo (note el \ 1)

repdict = {'&&':'and', '||':'or', '!([a-zA-Z_])':'not \1'} 

Puse la respuesta SO mencionada al principio en la siguiente función, que funciona bien para los pares de expresión / reemplazo que no contienen referencias inversas:

 def replaceAll(repdict, text): repdict = dict((re.escape(k), v) for k, v in repdict.items()) pattern = re.compile("|".join(repdict.keys())) return pattern.sub(lambda m: repdict[re.escape(m.group(0))], text) 

Sin embargo, no funciona para la clave que contiene una referencia inversa …

 >>> replaceAll(repldict, "!newData.exists() || newData.val().length == 1") '!newData.exists() or newData.val().length == 1' 

Si lo hago manualmente, funciona bien. p.ej:

 pattern = re.compile("!([a-zA-Z_])") pattern.sub(r'not \1', '!newData.exists()') 

Funciona como se espera:

 'not newData.exists()' 

En la función de fantasía, el escape parece estar arruinando la clave que usa la referencia, por lo que nunca coincide con nada.

Eventualmente se me ocurrió esto. Sin embargo, tenga en cuenta que el problema de compatibilidad con las referencias en los parámetros de entrada no se resuelve, solo lo estoy manejando manualmente en la función de reemplazo:

 def replaceAll(repPat, text): def replacer(obj): match = obj.group(0) # manually deal with exclamation mark match.. if match[:1] == "!": return 'not ' + match[1:] # here we naively escape the matched pattern into # the format of our dictionary key else: return repPat[naive_escaper(match)] pattern = re.compile("|".join(repPat.keys())) return pattern.sub(replacer, text) def naive_escaper(string): if '=' in string: return string.replace('=', '\=') elif '|' in string: return string.replace('|', '\|') else: return string # manually escaping \ and = works fine repPat = {'!([a-zA-Z_])':'', '&&':'and', '\|\|':'or', '\=\=\=':'=='} replaceAll(repPat, "(!this && !that) || !this && foo === bar") 

Devoluciones:

 '(not this and not that) or not this' 

Entonces, si alguien tiene una idea de cómo hacer una función de reemplazo de cadenas múltiples que admita referencias inversas y acepte los términos de reemplazo como entrada, agradecería mucho sus comentarios.

Related of "Python reemplaza varias cadenas mientras soporta referencias"

Actualización: Vea la respuesta de Angus Hollands para una mejor alternativa.


No podría pensar en una forma más fácil de hacerlo que en seguir con la idea original de combinar todas las claves dict en una expresión regular masiva.

Sin embargo, hay algunas dificultades. Asummos un repldict como este:

 repldict = {r'(a)': r'\1a', r'(b)': r'\1b'} 

Si los combinamos en una sola expresión regular, obtenemos (a)|(b) , por lo que ahora (b) ya no es el grupo 1, lo que significa que su referencia inversa no funcionará correctamente.

Otro problema es que no podemos decir qué reemplazo usar. Si la expresión regular coincide con el texto b , ¿cómo podemos descubrir que \1b es el reemplazo apropiado? No es posible; No tenemos suficiente información.

La solución a estos problemas es encerrar cada clave de dictado en un grupo con nombre, de esta manera:

 (?P(a))|(?P(b)) 

Ahora podemos identificar fácilmente la clave que coincide, y recalcular las referencias para hacerlas relativas a este grupo. de modo que \1b refiere a “el primer grupo después de group2”.


Aquí está la implementación:

 def replaceAll(repldict, text): # split the dict into two lists because we need the order to be reliable keys, repls = zip(*repldict.items()) # generate a regex pattern from the keys, putting each key in a named group # so that we can find out which one of them matched. # groups are named "_" where  is the index of the corresponding # replacement text in the list above pattern = '|'.join('(?P<_{}>{})'.format(i, k) for i, k in enumerate(keys)) def repl(match): # find out which key matched. We know that exactly one of the keys has # matched, so it's the only named group with a value other than None. group_name = next(name for name, value in match.groupdict().items() if value is not None) group_index = int(group_name[1:]) # now that we know which group matched, we can retrieve the # corresponding replacement text repl_text = repls[group_index] # now we'll manually search for backreferences in the # replacement text and substitute them def repl_backreference(m): reference_index = int(m.group(1)) # return the corresponding group's value from the original match # +1 because regex starts counting at 1 return match.group(group_index + reference_index + 1) return re.sub(r'\\(\d+)', repl_backreference, repl_text) return re.sub(pattern, repl, text) 

Pruebas:

 repldict = {'&&':'and', r'\|\|':'or', r'!([a-zA-Z_])':r'not \1'} print( replaceAll(repldict, "!newData.exists() || newData.val().length == 1") ) repldict = {'!([a-zA-Z_])':r'not \1', '&&':'and', r'\|\|':'or', r'\=\=\=':'=='} print( replaceAll(repldict, "(!this && !that) || !this && foo === bar") ) # output: not newData.exists() or newData.val().length == 1 # (not this and not that) or not this and foo == bar 

Advertencias:

  • Sólo se admiten referencias numéricas; No hay referencias nombradas.
  • Acepta silenciosamente las referencias inversas no válidas como {r'(a)': r'\2'} . (Esto a veces arrojará un error, pero no siempre).

Solución similar a Rawing, que solo precomputa las cosas caras antes de tiempo modificando los índices de grupo en las referencias inversas. Además, utilizando grupos sin nombre.

Aquí envolvemos silenciosamente cada caso en un grupo de captura, y luego actualizamos cualquier reemplazo con referencias inversas para identificar correctamente el subgrupo apropiado por posición absoluta. Tenga en cuenta que al usar una función de reemplazo, las referencias no funcionan de forma predeterminada (debe llamar a match.expand ).

 import re from collections import OrderedDict from functools import partial pattern_to_replacement = {'&&': 'and', '!([a-zA-Z_]+)': r'not \1'} def build_replacer(cases): ordered_cases = OrderedDict(cases.items()) replacements = {} leading_groups = 0 for pattern, replacement in ordered_cases.items(): leading_groups += 1 # leading_groups is now the absolute position of the root group (back-references should be relative to this) group_index = leading_groups replacement = absolute_backreference(replacement, group_index) replacements[group_index] = replacement # This pattern contains N subgroups (determine by compiling pattern) subgroups = re.compile(pattern).groups leading_groups += subgroups catch_all = "|".join("({})".format(p) for p in ordered_cases) pattern = re.compile(catch_all) def replacer(match): replacement_pattern = replacements[match.lastindex] return match.expand(replacement_pattern) return partial(pattern.sub, replacer) def absolute_backreference(text, n): ref_pat = re.compile(r"\\([0-99])") def replacer(match): return "\\{}".format(int(match.group(1)) + n) return ref_pat.sub(replacer, text) replacer = build_replacer(pattern_to_replacement) print(replacer("!this.exists()")) 

Simple es mejor que complejo, el código que se muestra a continuación es más legible (la razón por la que el código no funciona como se esperaba es que ([a-zA-Z_]) no debería estar en re.escape):

 repdict = { r'\s*' + re.escape('&&')) + r'\s*': ' and ', r'\s*' + re.escape('||') + r'\s*': ' or ', re.escape('!') + r'([a-zA-Z_])': r'not \1', } def replaceAll(repdict, text): for k, v in repdict.items(): text = re.sub(k, v, text) return text