¿La mejor manera de reemplazar varios caracteres en una cadena?

Respuestas:

432

Reemplazar dos personajes

Tomé el tiempo de todos los métodos en las respuestas actuales junto con uno extra.

Con una cadena de entrada de abc&def#ghiy reemplazando y -> \ & y # -> \ #, la manera más rápida era encadenar los reemplazos como este: text.replace('&', '\&').replace('#', '\#').

Tiempos para cada función:

  • a) 1000000 bucles, lo mejor de 3: 1.47 μs por bucle
  • b) 1000000 bucles, lo mejor de 3: 1.51 μs por bucle
  • c) 100000 bucles, lo mejor de 3: 12.3 μs por bucle
  • d) 100000 bucles, lo mejor de 3: 12 μs por bucle
  • e) 100000 bucles, lo mejor de 3: 3.27 μs por bucle
  • f) 1000000 bucles, lo mejor de 3: 0.817 μs por bucle
  • g) 100000 bucles, lo mejor de 3: 3.64 μs por bucle
  • h) 1000000 bucles, lo mejor de 3: 0.927 μs por bucle
  • i) 1000000 bucles, lo mejor de 3: 0.814 μs por bucle

Aquí están las funciones:

def a(text):
    chars = "&#"
    for c in chars:
        text = text.replace(c, "\\" + c)


def b(text):
    for ch in ['&','#']:
        if ch in text:
            text = text.replace(ch,"\\"+ch)


import re
def c(text):
    rx = re.compile('([&#])')
    text = rx.sub(r'\\\1', text)


RX = re.compile('([&#])')
def d(text):
    text = RX.sub(r'\\\1', text)


def mk_esc(esc_chars):
    return lambda s: ''.join(['\\' + c if c in esc_chars else c for c in s])
esc = mk_esc('&#')
def e(text):
    esc(text)


def f(text):
    text = text.replace('&', '\&').replace('#', '\#')


def g(text):
    replacements = {"&": "\&", "#": "\#"}
    text = "".join([replacements.get(c, c) for c in text])


def h(text):
    text = text.replace('&', r'\&')
    text = text.replace('#', r'\#')


def i(text):
    text = text.replace('&', r'\&').replace('#', r'\#')

Programado así:

python -mtimeit -s"import time_functions" "time_functions.a('abc&def#ghi')"
python -mtimeit -s"import time_functions" "time_functions.b('abc&def#ghi')"
python -mtimeit -s"import time_functions" "time_functions.c('abc&def#ghi')"
python -mtimeit -s"import time_functions" "time_functions.d('abc&def#ghi')"
python -mtimeit -s"import time_functions" "time_functions.e('abc&def#ghi')"
python -mtimeit -s"import time_functions" "time_functions.f('abc&def#ghi')"
python -mtimeit -s"import time_functions" "time_functions.g('abc&def#ghi')"
python -mtimeit -s"import time_functions" "time_functions.h('abc&def#ghi')"
python -mtimeit -s"import time_functions" "time_functions.i('abc&def#ghi')"

Reemplazando 17 caracteres

Aquí hay un código similar para hacer lo mismo pero con más caracteres para escapar (\ `* _ {}> # + -.! $):

def a(text):
    chars = "\\`*_{}[]()>#+-.!$"
    for c in chars:
        text = text.replace(c, "\\" + c)


def b(text):
    for ch in ['\\','`','*','_','{','}','[',']','(',')','>','#','+','-','.','!','$','\'']:
        if ch in text:
            text = text.replace(ch,"\\"+ch)


import re
def c(text):
    rx = re.compile('([&#])')
    text = rx.sub(r'\\\1', text)


RX = re.compile('([\\`*_{}[]()>#+-.!$])')
def d(text):
    text = RX.sub(r'\\\1', text)


def mk_esc(esc_chars):
    return lambda s: ''.join(['\\' + c if c in esc_chars else c for c in s])
esc = mk_esc('\\`*_{}[]()>#+-.!$')
def e(text):
    esc(text)


def f(text):
    text = text.replace('\\', '\\\\').replace('`', '\`').replace('*', '\*').replace('_', '\_').replace('{', '\{').replace('}', '\}').replace('[', '\[').replace(']', '\]').replace('(', '\(').replace(')', '\)').replace('>', '\>').replace('#', '\#').replace('+', '\+').replace('-', '\-').replace('.', '\.').replace('!', '\!').replace('$', '\$')


def g(text):
    replacements = {
        "\\": "\\\\",
        "`": "\`",
        "*": "\*",
        "_": "\_",
        "{": "\{",
        "}": "\}",
        "[": "\[",
        "]": "\]",
        "(": "\(",
        ")": "\)",
        ">": "\>",
        "#": "\#",
        "+": "\+",
        "-": "\-",
        ".": "\.",
        "!": "\!",
        "$": "\$",
    }
    text = "".join([replacements.get(c, c) for c in text])


def h(text):
    text = text.replace('\\', r'\\')
    text = text.replace('`', r'\`')
    text = text.replace('*', r'\*')
    text = text.replace('_', r'\_')
    text = text.replace('{', r'\{')
    text = text.replace('}', r'\}')
    text = text.replace('[', r'\[')
    text = text.replace(']', r'\]')
    text = text.replace('(', r'\(')
    text = text.replace(')', r'\)')
    text = text.replace('>', r'\>')
    text = text.replace('#', r'\#')
    text = text.replace('+', r'\+')
    text = text.replace('-', r'\-')
    text = text.replace('.', r'\.')
    text = text.replace('!', r'\!')
    text = text.replace('$', r'\$')


def i(text):
    text = text.replace('\\', r'\\').replace('`', r'\`').replace('*', r'\*').replace('_', r'\_').replace('{', r'\{').replace('}', r'\}').replace('[', r'\[').replace(']', r'\]').replace('(', r'\(').replace(')', r'\)').replace('>', r'\>').replace('#', r'\#').replace('+', r'\+').replace('-', r'\-').replace('.', r'\.').replace('!', r'\!').replace('$', r'\$')

Aquí están los resultados para la misma cadena de entrada abc&def#ghi:

  • a) 100000 bucles, lo mejor de 3: 6,72 μs por bucle
  • b) 100000 bucles, lo mejor de 3: 2.64 μs por bucle
  • c) 100000 bucles, lo mejor de 3: 11.9 μs por bucle
  • d) 100000 bucles, lo mejor de 3: 4,92 μs por bucle
  • mi) 100000 bucles, lo mejor de 3: 2.96 μs por bucle
  • f) 100000 bucles, lo mejor de 3: 4.29 μs por bucle
  • g) 100000 bucles, lo mejor de 3: 4.68 μs por bucle
  • h) 100000 bucles, lo mejor de 3: 4,73 μs por bucle
  • i) 100000 bucles, lo mejor de 3: 4.24 μs por bucle

Y con una cadena de entrada más larga ( ## *Something* and [another] thing in a longer sentence with {more} things to replace$):

  • a) 100000 bucles, lo mejor de 3: 7,59 μs por bucle
  • b) 100000 bucles, lo mejor de 3: 6.54 μs por bucle
  • c) 100000 bucles, lo mejor de 3: 16.9 μs por bucle
  • d) 100000 bucles, lo mejor de 3: 7.29 μs por bucle
  • e) 100000 bucles, lo mejor de 3: 12.2 μs por bucle
  • f) 100000 bucles, lo mejor de 3: 5.38 μs por bucle
  • g) 10000 bucles, lo mejor de 3: 21.7 μs por bucle
  • h) 100000 bucles, lo mejor de 3: 5,7 μs por bucle
  • i) 100000 bucles, lo mejor de 3: 5,13 μs por bucle

Agregar un par de variantes:

def ab(text):
    for ch in ['\\','`','*','_','{','}','[',']','(',')','>','#','+','-','.','!','$','\'']:
        text = text.replace(ch,"\\"+ch)


def ba(text):
    chars = "\\`*_{}[]()>#+-.!$"
    for c in chars:
        if c in text:
            text = text.replace(c, "\\" + c)

Con la entrada más corta:

  • ab) 100000 bucles, lo mejor de 3: 7.05 μs por bucle
  • ba) 100000 bucles, lo mejor de 3: 2.4 μs por bucle

Con la entrada más larga:

  • ab) 100000 bucles, lo mejor de 3: 7.71 μs por bucle
  • ba) 100000 bucles, lo mejor de 3: 6.08 μs por bucle

Así que voy a usar bapara facilitar la lectura y la velocidad.

Apéndice

Impulsado por los hacks en los comentarios, una diferencia entre aby baes el if c in text:cheque. Probémoslos contra dos variantes más:

def ab_with_check(text):
    for ch in ['\\','`','*','_','{','}','[',']','(',')','>','#','+','-','.','!','$','\'']:
        if ch in text:
            text = text.replace(ch,"\\"+ch)

def ba_without_check(text):
    chars = "\\`*_{}[]()>#+-.!$"
    for c in chars:
        text = text.replace(c, "\\" + c)

Los tiempos en μs por ciclo en Python 2.7.14 y 3.6.3, y en una máquina diferente del conjunto anterior, por lo que no se pueden comparar directamente.

╭────────────╥──────┬───────────────┬──────┬──────────────────╮
 Py, input    ab   ab_with_check   ba   ba_without_check 
╞════════════╬══════╪═══════════════╪══════╪══════════════════╡
 Py2, short  8.81     4.22        3.45     8.01          
 Py3, short  5.54     1.34        1.46     5.34          
├────────────╫──────┼───────────────┼──────┼──────────────────┤
 Py2, long   9.3      7.15        6.85     8.55          
 Py3, long   7.43     4.38        4.41     7.02          
└────────────╨──────┴───────────────┴──────┴──────────────────┘

Podemos concluir que:

  • Aquellos con el cheque son hasta 4 veces más rápidos que aquellos sin el cheque

  • ab_with_checkestá ligeramente a la cabeza en Python 3, pero ba(con verificación) tiene una mayor ventaja en Python 2

  • Sin embargo, la lección más importante aquí es que Python 3 es hasta 3 veces más rápido que Python 2 . ¡No hay una gran diferencia entre el más lento en Python 3 y el más rápido en Python 2!

Hugo
fuente
44
¿Por qué no es esta la respuesta exceptuada?
Sopa de pollo
¿Es if c in text:necesario en ba?
piratea el
@haccks No es necesario, pero es 2-3 veces más rápido. Cadena corta, con: 1.45 usec per loopy sin: 5.3 usec per loop, larga cadena, con: 4.38 usec per loopy sin que: 7.03 usec per loop. (Tenga en cuenta que estos no son directamente comparables con los resultados anteriores, porque es una máquina diferente, etc.)
Hugo
1
@Hugo; Creo que esta diferencia en el tiempo se debe a que replacesolo se llama cuando cse encuentra texten caso de baque se llame en cada iteración ab.
piratea el
2
@haccks Gracias, he actualizado mi respuesta con más tiempos: agregar el cheque es mejor para ambos, ¡pero la lección más importante es que Python 3 es hasta 3 veces más rápido!
Hugo
73
>>> string="abc&def#ghi"
>>> for ch in ['&','#']:
...   if ch in string:
...      string=string.replace(ch,"\\"+ch)
...
>>> print string
abc\&def\#ghi
ghostdog74
fuente
¿Por qué se necesitaba una barra invertida doble? ¿Por qué no solo "\" funciona?
axolotl
3
La barra invertida doble escapa a la barra invertida; de lo contrario, Python interpretaría "\" como un carácter de cita literal dentro de una cadena aún abierta.
Riet
¿Por qué lo necesitas string=string.replace(ch,"\\"+ch)? ¿No es string.replace(ch,"\\"+ch)suficiente?
MattSom
1
@MattSom replace () no modifica la cadena original, pero devuelve una copia. Entonces necesita la asignación para que el código tenga algún efecto.
Ben Brian
3
¿Realmente necesitas el if? Parece una duplicación de lo que hará el reemplazo de todos modos.
lorenzo
32

Simplemente encadene las replacefunciones como esta

strs = "abc&def#ghi"
print strs.replace('&', '\&').replace('#', '\#')
# abc\&def\#ghi

Si los reemplazos van a ser más numerosos, puede hacerlo de esta manera genérica

strs, replacements = "abc&def#ghi", {"&": "\&", "#": "\#"}
print "".join([replacements.get(c, c) for c in strs])
# abc\&def\#ghi
thefourtheye
fuente
29

Aquí hay un método python3 que usa str.translatey str.maketrans:

s = "abc&def#ghi"
print(s.translate(str.maketrans({'&': '\&', '#': '\#'})))

La cadena impresa es abc\&def\#ghi.

tommy.carstensen
fuente
2
Esta es una buena respuesta, pero en la práctica hacer uno .translate()parece ser más lento que tres encadenados .replace()(usando CPython 3.6.4).
Changaco
@Changaco Gracias por cronometrarlo 👍 En la práctica, me usaría a replace()mí mismo, pero agregué esta respuesta en aras de la exhaustividad.
tommy.carstensen
Para cadenas grandes y muchos reemplazos, esto debería ser más rápido, aunque algunas pruebas serían buenas ...
Graipher
Bueno, no está en mi máquina (lo mismo para 2 y 17 reemplazos).
Graipher
¿Cómo es '\#'válido? no debería ser r'\#'o '\\#'? Podría ser un problema de formato de bloque de código, tal vez.
paridad3
16

¿Siempre vas a anteponer una barra invertida? Si es así, intente

import re
rx = re.compile('([&#])')
#                  ^^ fill in the characters here.
strs = rx.sub('\\\\\\1', strs)

Puede que no sea el método más eficiente, pero creo que es el más fácil.

kennytm
fuente
15
aarrgghh tryr'\\\1'
John Machin
10

Tarde a la fiesta, pero perdí mucho tiempo con este problema hasta que encontré mi respuesta.

Corto y dulce, translatees superior areplace . Si está más interesado en la optimización de la funcionalidad con el tiempo, no usereplace .

Úselo también translatesi no sabe si el conjunto de caracteres que se reemplazará se superpone al conjunto de caracteres utilizado para reemplazar.

Caso en punto:

Usarlo replaceingenuamente esperaría que el fragmento "1234".replace("1", "2").replace("2", "3").replace("3", "4")regrese "2344", pero de hecho volverá "4444".

La traducción parece realizar lo que OP originalmente deseaba.

Sebastialonso
fuente
6

Puede considerar escribir una función de escape genérica:

def mk_esc(esc_chars):
    return lambda s: ''.join(['\\' + c if c in esc_chars else c for c in s])

>>> esc = mk_esc('&#')
>>> print esc('Learn & be #1')
Learn \& be \#1

De esta manera, puede configurar su función con una lista de caracteres que se deben escapar.

Victor Olex
fuente
3

Para su información, esto es de poca o ninguna utilidad para el OP, pero puede ser útil para otros lectores (por favor, no desestime, estoy al tanto de esto).

Como ejercicio algo ridículo pero interesante, quería ver si podía usar la programación funcional de Python para reemplazar múltiples caracteres. Estoy bastante seguro de que esto NO supera simplemente llamando a replace () dos veces. Y si el rendimiento fuera un problema, podría superarlo fácilmente en rust, C, julia, perl, java, javascript y quizás incluso awk. Utiliza un paquete externo de 'ayudantes' llamado pytoolz , acelerado a través de cython ( cytoolz, es un paquete pypi ).

from cytoolz.functoolz import compose
from cytoolz.itertoolz import chain,sliding_window
from itertools import starmap,imap,ifilter
from operator import itemgetter,contains
text='&hello#hi&yo&'
char_index_iter=compose(partial(imap, itemgetter(0)), partial(ifilter, compose(partial(contains, '#&'), itemgetter(1))), enumerate)
print '\\'.join(imap(text.__getitem__, starmap(slice, sliding_window(2, chain((0,), char_index_iter(text), (len(text),))))))

Ni siquiera voy a explicar esto porque nadie se molestaría en usar esto para lograr un reemplazo múltiple. Sin embargo, me sentí algo logrado al hacer esto y pensé que podría inspirar a otros lectores o ganar un concurso de ofuscación de código.

paridad3
fuente
1
"programación funcional" no significa "usar tantas funciones como sea posible", ya sabes.
Craig Andrews
1
Este es un sustituto multi-char perfectamente funcional y puro: gist.github.com/anonymous/4577424f586173fc6b91a215ea2ce89e Sin asignaciones, sin mutaciones, sin efectos secundarios. Legible también.
Craig Andrews
1

Usando reduce, que está disponible en python2.7 y python3. *, Puede reemplazar fácilmente las subcadenas múltiples de una manera limpia y pitónica.

# Lets define a helper method to make it easy to use
def replacer(text, replacements):
    return reduce(
        lambda text, ptuple: text.replace(ptuple[0], ptuple[1]), 
        replacements, text
    )

if __name__ == '__main__':
    uncleaned_str = "abc&def#ghi"
    cleaned_str = replacer(uncleaned_str, [("&","\&"),("#","\#")])
    print(cleaned_str) # "abc\&def\#ghi"

En python2.7 no tiene que importar reduce, pero en python3. * Debe importarlo desde el módulo functools.

CasualCoder3
fuente
1

Tal vez un bucle simple para reemplazar los caracteres:

a = '&#'

to_replace = ['&', '#']

for char in to_replace:
    a = a.replace(char, "\\"+char)

print(a)

>>> \&\#
Tiago Wutzke de Oliveira
fuente
1

¿Qué tal esto?

def replace_all(dict, str):
    for key in dict:
        str = str.replace(key, dict[key])
    return str

luego

print(replace_all({"&":"\&", "#":"\#"}, "&#"))

salida

\&\#

similar a la respuesta

judios
fuente
0
>>> a = '&#'
>>> print a.replace('&', r'\&')
\&#
>>> print a.replace('#', r'\#')
&\#
>>> 

Desea usar una cadena 'sin procesar' (denotada por el prefijo 'r' de la cadena de reemplazo), ya que las cadenas sin procesar no tratan especialmente la barra invertida.

jonesy
fuente