Melhor maneira de substituir vários caracteres em uma string?

Respostas:

432

Substituindo dois caracteres

Cronometrei todos os métodos nas respostas atuais, juntamente com um extra.

Com uma cadeia de entrada de abc&def#ghie substituindo & -> \ & # e -> \ #, a maneira mais rápida foi para encadear as substituições como este: text.replace('&', '\&').replace('#', '\#').

Horários para cada função:

  • a) 1000000 loops, melhor de 3: 1,47 μs por loop
  • b) 1000000 loops, melhor de 3: 1,51 μs por loop
  • c) 100000 loops, melhor de 3: 12,3 μs por loop
  • d) 100000 loops, melhor de 3: 12 μs por loop
  • e) 100000 loops, melhor de 3: 3,27 μs por loop
  • f) 1000000 loops, melhor de 3: 0,817 μs por loop
  • g) 100000 loops, melhor de 3: 3,64 μs por loop
  • h) 1000000 loops, melhor de 3: 0,927 μs por loop
  • i) 1000000 loops, melhor de 3: 0,814 μs por loop

Aqui estão as funções:

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'\#')

Cronometrado assim:

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')"

Substituindo 17 caracteres

Aqui está um código semelhante para fazer o mesmo, mas com mais 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'\$')

Aqui estão os resultados para a mesma sequência de entrada abc&def#ghi:

  • a) 100000 loops, melhor de 3: 6,72 μs por loop
  • b) 100000 loops, melhor de 3: 2,64 μs por loop
  • c) 100000 loops, melhor de 3: 11,9 μs por loop
  • d) 100000 loops, melhor de 3: 4,92 μs por loop
  • e) 100000 loops, melhor de 3: 2,96 μs por loop
  • f) 100000 loops, melhor de 3: 4,29 μs por loop
  • g) 100000 loops, o melhor de 3: 4,68 μs por loop
  • h) 100000 loops, melhor de 3: 4,73 μs por loop
  • i) 100000 loops, o melhor de 3: 4,24 μs por loop

E com uma string de entrada mais longa ( ## *Something* and [another] thing in a longer sentence with {more} things to replace$):

  • a) 100000 loops, o melhor de 3: 7,59 μs por loop
  • b) 100000 loops, melhor de 3: 6,54 μs por loop
  • c) 100000 loops, melhor de 3: 16,9 μs por loop
  • d) 100000 loops, melhor de 3: 7,29 μs por loop
  • e) 100000 loops, melhor de 3: 12,2 μs por loop
  • f) 100000 loops, melhor de 3: 5,38 μs por loop
  • g) 10000 loops, melhor de 3: 21,7 μs por loop
  • h) 100000 loops, o melhor de 3: 5,7 μs por loop
  • i) 100000 loops, o melhor de 3: 5,13 μs por loop

Adicionando algumas 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)

Com a entrada mais curta:

  • ab) 100000 loops, melhor de 3: 7,05 μs por loop
  • ba) 100000 loops, o melhor de 3: 2,4 μs por loop

Com a entrada mais longa:

  • ab) 100000 loops, melhor de 3: 7,71 μs por loop
  • ba) 100000 loops, o melhor de 3: 6,08 μs por loop

Então, eu vou usar bapara facilitar a leitura e a velocidade.

Termo aditivo

Solicitado por hackers nos comentários, uma diferença entre abe baé a if c in text:verificação. Vamos testá-los em mais duas variantes:

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)

Os tempos em µs por loop no Python 2.7.14 e 3.6.3 e em uma máquina diferente do conjunto anterior, portanto, não podem ser comparados diretamente.

╭────────────╥──────┬───────────────┬──────┬──────────────────╮
 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          
└────────────╨──────┴───────────────┴──────┴──────────────────┘

Nos podemos concluir que:

  • Aqueles com o cheque são até 4x mais rápidos do que aqueles sem o cheque

  • ab_with_checkestá ligeiramente à frente no Python 3, mas ba(com verificação) tem uma vantagem maior no Python 2

  • No entanto, a maior lição aqui é que o Python 3 é até 3x mais rápido que o Python 2 ! Não há uma grande diferença entre os mais lentos no Python 3 e os mais rápidos no Python 2!

Hugo
fonte
4
Por que essa não é a resposta exceto?
Chicken Soup
É if c in text:necessário entrar ba?
Hackcks #
@haccks Não é necessário, mas é 2-3 vezes mais rápido. Corda curta, com: 1.45 usec per loope sem: 5.3 usec per loop, cadeia longa, com: 4.38 usec per loope sem: 7.03 usec per loop. (Observe que estes não são diretamente comparáveis ​​com os resultados acima, porque é uma máquina diferente etc.) #
Hugo
1
@Hugo; Eu acho que essa diferença no tempo é devido a que replaceé chamada apenas quando cé encontrada textno caso de baenquanto é chamada em todas as iterações em ab.
haccks
2
@haccks Obrigado, atualizei minha resposta com mais intervalos: adicionar a verificação é melhor para ambos, mas a maior lição é que o Python 3 é até 3x mais 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
fonte
Por que era necessária uma barra invertida dupla? Por que simplesmente "\" não funciona?
axolotl
3
A barra invertida dupla escapa da barra invertida; caso contrário, o python interpretaria "\" como um caractere de citação literal dentro de uma string ainda aberta.
Riet 21/07
Por que você precisa string=string.replace(ch,"\\"+ch)? Não é apenas o string.replace(ch,"\\"+ch)suficiente?
MattSom
1
@MattSom replace () não modifica a string original, mas retorna uma cópia. Portanto, você precisa que a atribuição do código tenha qualquer efeito.
Ben Brian
3
Você realmente precisa do if? Parece uma duplicação do que a substituição fará de qualquer maneira.
lorenzo 30/06
32

Simplesmente encadeie replacefunções como esta

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

Se as substituições forem mais numerosas, você poderá fazer isso dessa maneira genérica

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

Aqui está um método python3 usando str.translatee str.maketrans:

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

A sequência impressa é abc\&def\#ghi.

tommy.carstensen
fonte
2
Essa é uma boa resposta, mas, na prática, a execução de uma .translate()parece ser mais lenta que a de três cadeias .replace()(usando o CPython 3.6.4).
Changaco
@Changaco Obrigado por cronometrar 👍 Na prática, eu replace()me usaria , mas adicionei esta resposta por uma questão de integridade.
Tommy.carstensen
Para grandes cordas e muitas substituições este deve ser mais rápido, embora alguns testes seria bom ...
Graipher
Bem, não está na minha máquina (o mesmo para 2 e 17 substituições).
Graipher
como é '\#'válido? não deveria ser r'\#'ou '\\#'? Talvez seja um problema de formatação do bloco de código.
parity3 10/05/19
16

Você sempre vai acrescentar uma barra invertida? Se sim, tente

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

Pode não ser o método mais eficiente, mas acho que é o mais fácil.

kennytm
fonte
15
tente aarrgghhr'\\\1'
John Machin
10

Cheguei atrasado na festa, mas perdi muito tempo com esse problema até encontrar minha resposta.

Curto e doce, translateé superior areplace . Se você estiver mais interessado na otimização da funcionalidade ao longo do tempo, não use replace.

Use também translatese você não souber se o conjunto de caracteres a ser substituído se sobrepõe ao conjunto de caracteres usado para substituir.

Caso em questão:

Usando replacevocê, você esperaria ingenuamente que o snippet "1234".replace("1", "2").replace("2", "3").replace("3", "4")retornasse "2344", mas ele retornará de fato "4444".

A tradução parece executar o OP originalmente desejado.

Sebastialonso
fonte
6

Você pode escrever uma função 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

Dessa forma, você pode configurar sua função com uma lista de caracteres que devem ser escapados.

Victor Olex
fonte
3

Para sua informação, isso é de pouca ou nenhuma utilidade para o OP, mas pode ser útil para outros leitores (por favor, não diminua o voto, estou ciente disso).

Como um exercício um tanto ridículo, mas interessante, queria ver se eu poderia usar a programação funcional python para substituir vários caracteres. Eu tenho certeza que isso não bate apenas chamando replace () duas vezes. E se o desempenho fosse um problema, você poderia facilmente superar isso com ferrugem, C, julia, perl, java, javascript e talvez até awk. Ele usa um pacote de 'ajudantes' externo chamado pytoolz , acelerado via cython ( cytoolz, é um pacote 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),))))))

Eu nem vou explicar isso, porque ninguém se incomodaria em usar isso para realizar substituições múltiplas. No entanto, me senti um pouco realizado ao fazer isso e pensei que poderia inspirar outros leitores ou ganhar um concurso de ofuscação de código.

parity3
fonte
1
"programação funcional" não significa "usar tantas funções quanto possível", você sabe.
Craig Andrews
1
Este é um substituto multi-char funcional perfeitamente bom e puro: gist.github.com/anonymous/4577424f586173fc6b91a215ea2ce89e Sem alocações, sem mutações, sem efeitos colaterais. Legível também.
Craig Andrews
1

Usando o reduzir disponível em python2.7 e python3. *, Você pode substituir facilmente substrings múltiplos de maneira limpa e 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"

No python2.7 você não precisa importar o reduzir, mas no python3. * Você precisa importá-lo do módulo functools.

CasualCoder3
fonte
1

Talvez um loop simples para caracteres substituir:

a = '&#'

to_replace = ['&', '#']

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

print(a)

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

Que tal agora?

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

então

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

resultado

\&\#

semelhante a responder

jewishmoses
fonte
0
>>> a = '&#'
>>> print a.replace('&', r'\&')
\&#
>>> print a.replace('#', r'\#')
&\#
>>> 

Você deseja usar uma cadeia de caracteres 'bruta' (indicada pelo 'r' que prefixa a cadeia de substituição), pois as cadeias brutas não tratam a barra invertida especialmente.

jonesy
fonte