Массовая замена строки в python?

Скажем, у меня есть строка, которая выглядит так:

str = "The &yquick &cbrown &bfox &Yjumps over the &ulazy dog"

Вы заметите много мест в строке, где есть амперсанд, за которым следует символ (например, «&y» и «&c»). Мне нужно заменить эти символы соответствующим значением, которое у меня есть в словаре, например:

dict = {"&y":"\033[0;30m",
        "&c":"\033[0;31m",
        "&b":"\033[0;32m",
        "&Y":"\033[0;33m",
        "&u":"\033[0;34m"}

Каков самый быстрый способ сделать это? Я мог бы вручную найти все амперсанды, а затем просмотреть словарь, чтобы изменить их, но это кажется медленным. Выполнение множества замен регулярных выражений также кажется медленным (у меня будет словарь примерно из 30-40 пар в моем реальном коде).

Любые предложения приветствуются, спасибо.

Изменить:

Как было указано в комментариях к этому вопросу, мой словарь определяется до выполнения и никогда не будет меняться в течение жизненного цикла приложения. Это список escape-последовательностей ANSI, в котором будет около 40 элементов. Моя средняя длина строки для сравнения будет около 500 символов, но будут и строки до 5000 символов (хотя это будет редко). Я также использую Python 2.6 в настоящее время.

Редактировать №2 Я принял ответ Tor Valamos как правильный, так как он не только дал правильное решение (хотя это было не лучшее решение), но и принял все остальные во внимание и проделал огромную работу, чтобы сравнить их все. Этот ответ — один из лучших и самых полезных ответов, которые я когда-либо встречал на StackOverflow. Слава вам.


person Mike Trpcic    schedule 17.12.2009    source источник
comment
Как указывает Тор Валамо, вы также можете рассмотреть условия ошибки — например, если у вас есть последовательности амперсандов, которых нет в вашем словаре, — и случай, когда у вас есть амперсанд в строке, которую следует оставить в покое, поскольку она часть текстового содержания.   -  person Peter Hansen    schedule 17.12.2009
comment
Майк, помимо общей длины строки, для полноценного бенчмаркинга было бы важно знать плотность escape-последовательностей, или общее количество на строку, или что-то в этом роде.   -  person Peter Hansen    schedule 17.12.2009
comment
Питер: Это непредсказуемо, так как в некоторых строках будет 15 символов с 15 escape-последовательностями, а в некоторых — 500 символов с 1 escape-последовательностью. Строки исходят от пользователя и могут быть чем угодно. Для сравнения я бы предположил, что на каждые 25 обычных символов приходится одна управляющая последовательность.   -  person Mike Trpcic    schedule 17.12.2009
comment
Если строки исходят от пользователя, я бы сказал, что обработка ошибок — это неплохо, а, Питер? :П   -  person Tor Valamo    schedule 17.12.2009
comment
@Tor, конечно, если теперь требуется обработка ошибок, то ее нужно предоставить. Не было определено, что вы хотели бы сделать в случае ввода текста, содержащего, например, корневое пиво A&W, если &W также был escape-кодом.   -  person Peter Hansen    schedule 17.12.2009
comment
Кстати, обработка ошибок Tor (не игнорирование кодов, отсутствующих в словаре) тривиально обрабатывается с помощью регулярных выражений, просто гарантируя, что регулярное выражение указывает только допустимые коды вместо [a-zA-Z]. Если Q не является допустимым кодом в словаре, просто оставьте его вне набора символов: [a-zA-PR-Z]. Если на то пошло, в реальном решении вы бы просто динамически построили регулярное выражение на основе словаря.   -  person Peter Hansen    schedule 17.12.2009
comment
@Майк, пожалуйста, посмотри мой исправленный ответ. Возможно, более быстрые альтернативы будут работать, если вы сможете их настроить, но с тестовой программой, которая действительно соответствует вашим входным образцам (чего, я не верю, делает Tor), эти другие, похоже, не работают. Мое решение на самом деле кажется самым быстрым, по крайней мере, на данный момент. Решения для этих других приветствуются, или кто-то может указать, почему они не прошли мой тест.   -  person Peter Hansen    schedule 18.12.2009


Ответы (13)


mydict = {"&y":"\033[0;30m",
          "&c":"\033[0;31m",
          "&b":"\033[0;32m",
          "&Y":"\033[0;33m",
          "&u":"\033[0;34m"}
mystr = "The &yquick &cbrown &bfox &Yjumps over the &ulazy dog"

for k, v in mydict.iteritems():
    mystr = mystr.replace(k, v)

print mystr
The ←[0;30mquick ←[0;31mbrown ←[0;32mfox ←[0;33mjumps over the ←[0;34mlazy dog

Я взял на себя смелость сравнить несколько решений:

mydict = dict([('&' + chr(i), str(i)) for i in list(range(65, 91)) + list(range(97, 123))])

# random inserts between keys
from random import randint
rawstr = ''.join(mydict.keys())
mystr = ''
for i in range(0, len(rawstr), 2):
    mystr += chr(randint(65,91)) * randint(0,20) # insert between 0 and 20 chars

from time import time

# How many times to run each solution
rep = 10000

print 'Running %d times with string length %d and ' \
      'random inserts of lengths 0-20' % (rep, len(mystr))

# My solution
t = time()
for x in range(rep):
    for k, v in mydict.items():
        mystr.replace(k, v)
    #print(mystr)
print '%-30s' % 'Tor fixed & variable dict', time()-t

from re import sub, compile, escape

# Peter Hansen
t = time()
for x in range(rep):
    sub(r'(&[a-zA-Z])', r'%(\1)s', mystr) % mydict
print '%-30s' % 'Peter fixed & variable dict', time()-t

# Claudiu
def multiple_replace(dict, text): 
    # Create a regular expression  from the dictionary keys
    regex = compile("(%s)" % "|".join(map(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)

t = time()
for x in range(rep):
    multiple_replace(mydict, mystr)
print '%-30s' % 'Claudio variable dict', time()-t

# Claudiu - Precompiled
regex = compile("(%s)" % "|".join(map(escape, mydict.keys())))

t = time()
for x in range(rep):
    regex.sub(lambda mo: mydict[mo.string[mo.start():mo.end()]], mystr)
print '%-30s' % 'Claudio fixed dict', time()-t

# Andrew Y - variable dict
def mysubst(somestr, somedict):
  subs = somestr.split("&")
  return subs[0] + "".join(map(lambda arg: somedict["&" + arg[0:1]] + arg[1:], subs[1:]))

t = time()
for x in range(rep):
    mysubst(mystr, mydict)
print '%-30s' % 'Andrew Y variable dict', time()-t

# Andrew Y - fixed
def repl(s):
  return mydict["&"+s[0:1]] + s[1:]

t = time()
for x in range(rep):
    subs = mystr.split("&")
    res = subs[0] + "".join(map(repl, subs[1:]))
print '%-30s' % 'Andrew Y fixed dict', time()-t

Результаты в Python 2.6

Running 10000 times with string length 490 and random inserts of lengths 0-20
Tor fixed & variable dict      1.04699993134
Peter fixed & variable dict    0.218999862671
Claudio variable dict          2.48400020599
Claudio fixed dict             0.0940001010895
Andrew Y variable dict         0.0309998989105
Andrew Y fixed dict            0.0310001373291

Решения Клаудиу и Эндрю продолжали снижаться до 0, поэтому мне пришлось увеличить его до 10 000 прогонов.

Я запустил его в Python 3 (из-за юникода) с заменой символов с 39 на 1024 (38 — это амперсанд, поэтому я не хотел его включать). Длина строки до 10 000, включая около 980 замен с переменными случайными вставками длины 0-20. Значения Юникода от 39 до 1024 вызывают символы длиной 1 и 2 байта, что может повлиять на некоторые решения.

mydict = dict([('&' + chr(i), str(i)) for i in range(39,1024)])

# random inserts between keys
from random import randint
rawstr = ''.join(mydict.keys())
mystr = ''
for i in range(0, len(rawstr), 2):
    mystr += chr(randint(65,91)) * randint(0,20) # insert between 0 and 20 chars

from time import time

# How many times to run each solution
rep = 10000

print('Running %d times with string length %d and ' \
      'random inserts of lengths 0-20' % (rep, len(mystr)))

# Tor Valamo - too long
#t = time()
#for x in range(rep):
#    for k, v in mydict.items():
#        mystr.replace(k, v)
#print('%-30s' % 'Tor fixed & variable dict', time()-t)

from re import sub, compile, escape

# Peter Hansen
t = time()
for x in range(rep):
    sub(r'(&[a-zA-Z])', r'%(\1)s', mystr) % mydict
print('%-30s' % 'Peter fixed & variable dict', time()-t)

# Peter 2
def dictsub(m):
    return mydict[m.group()]

t = time()
for x in range(rep):
    sub(r'(&[a-zA-Z])', dictsub, mystr)
print('%-30s' % 'Peter fixed dict', time()-t)

# Claudiu - too long
#def multiple_replace(dict, text): 
#    # Create a regular expression  from the dictionary keys
#    regex = compile("(%s)" % "|".join(map(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)
#
#t = time()
#for x in range(rep):
#    multiple_replace(mydict, mystr)
#print('%-30s' % 'Claudio variable dict', time()-t)

# Claudiu - Precompiled
regex = compile("(%s)" % "|".join(map(escape, mydict.keys())))

t = time()
for x in range(rep):
    regex.sub(lambda mo: mydict[mo.string[mo.start():mo.end()]], mystr)
print('%-30s' % 'Claudio fixed dict', time()-t)

# Separate setup for Andrew and gnibbler optimized dict
mydict = dict((k[1], v) for k, v in mydict.items())

# Andrew Y - variable dict
def mysubst(somestr, somedict):
  subs = somestr.split("&")
  return subs[0] + "".join(map(lambda arg: somedict[arg[0:1]] + arg[1:], subs[1:]))

def mysubst2(somestr, somedict):
  subs = somestr.split("&")
  return subs[0].join(map(lambda arg: somedict[arg[0:1]] + arg[1:], subs[1:]))

t = time()
for x in range(rep):
    mysubst(mystr, mydict)
print('%-30s' % 'Andrew Y variable dict', time()-t)
t = time()
for x in range(rep):
    mysubst2(mystr, mydict)
print('%-30s' % 'Andrew Y variable dict 2', time()-t)

# Andrew Y - fixed
def repl(s):
  return mydict[s[0:1]] + s[1:]

t = time()
for x in range(rep):
    subs = mystr.split("&")
    res = subs[0] + "".join(map(repl, subs[1:]))
print('%-30s' % 'Andrew Y fixed dict', time()-t)

# gnibbler
t = time()
for x in range(rep):
    myparts = mystr.split("&")
    myparts[1:]=[mydict[x[0]]+x[1:] for x in myparts[1:]]
    "".join(myparts)
print('%-30s' % 'gnibbler fixed & variable dict', time()-t)

Результаты:

Running 10000 times with string length 9491 and random inserts of lengths 0-20
Tor fixed & variable dict      0.0 # disqualified 329 secs
Peter fixed & variable dict    2.07799983025
Peter fixed dict               1.53100013733 
Claudio variable dict          0.0 # disqualified, 37 secs
Claudio fixed dict             1.5
Andrew Y variable dict         0.578000068665
Andrew Y variable dict 2       0.56299996376
Andrew Y fixed dict            0.56200003624
gnibbler fixed & variable dict 0.530999898911

(** Обратите внимание, что в коде gnibbler используется другой словарь, в котором ключи не содержат «&». Код Эндрю также использует этот альтернативный словарь, но это не имеет большого значения, может быть, всего лишь ускорение в 0,01 раза.)

person Community    schedule 17.12.2009
comment
Это просто и, вероятно, быстрее, чем регулярное выражение, если реальный словарь замены не намного больше, чем 5 элементов. - person John La Rooy; 17.12.2009
comment
gnibbler: На самом деле мой словарь будет состоять примерно из 40 элементов. - person Mike Trpcic; 17.12.2009
comment
@Mike, вам нужно проверить, чтобы быть уверенным, но регулярные выражения действительно намного медленнее, чем простая замена. Я опубликовал ответ, в котором используется разделение/объединение, будет интересно посмотреть, какой подход лучше в различных условиях. - person John La Rooy; 17.12.2009
comment
Вы не очень справедливы к Клаудиу. Во-первых, вы вызываете его как функцию, а накладные расходы на вызов функции в Python нельзя игнорировать. Во-вторых, его шаг компиляции будет выполняться не каждый раз, а один раз при запуске программы. - person Peter Hansen; 17.12.2009
comment
@Tor, тебе уже достаточно весело: обязательно поддержи вопрос Майка! :-) - person Peter Hansen; 17.12.2009
comment
@Peter - Вы предполагаете, что словарь один и тот же для каждого вызова, что может быть не так. Поэтому я компилирую его для каждого запуска. Но да, я добавлю уведомление о том, что это является причиной этого. - person Tor Valamo; 17.12.2009
comment
@Tor, если вы посмотрите на то, что делает код, он вставляет escape-последовательности ANSI для печати таких вещей, как жирный шрифт, подчеркивание и т. д. Эти коды почти наверняка предопределены и неизменны для OP, поэтому я думаю, что это хорошая ставка на вас будет иметь статический dict и компилироваться при запуске. Я могу ошибаться. - person Peter Hansen; 17.12.2009
comment
Если вы правильно запустите мой ответ: regex = compile((%s) % |.join(map(escape, mydict.keys()))) t = time() for x in range(rep): regex.sub(lambda mo: mydict[mo.string[mo.start():mo.end()]], mystr) print(time()-t) Тогда для 10000 прогонов я получаю следующие числа: 0,375 1,28100013733 0,593999862671 Итак, ваше еще быстрее, но мой гораздо ближе. - person Claudiu; 17.12.2009
comment
@Andrew Эндрю - я рассчитал твою скорость вдвое меньше моей, но твоя предполагает, что словарь непротиворечив, что не всегда может быть так. Но если это так, вы выигрываете. - person Tor Valamo; 17.12.2009
comment
Питер прав. У меня будет предопределенный словарь escape-последовательностей ANSI (для переднего плана, фона, подчеркивания и т. д.). Этот список никогда не изменится во время выполнения, поэтому одной компиляции вполне достаточно. - person Mike Trpcic; 17.12.2009
comment
@Claudiu - Тогда вы также предполагаете, что dict постоянен, что нормально, но мне придется различать это. - person Tor Valamo; 17.12.2009
comment
Я хотел бы увидеть обновленный тест с учетом этих новых откровений, а также некоторые другие альтернативы. - person Mike Trpcic; 17.12.2009
comment
@Tor: на самом деле мое первоначальное ускорение в 40 раз произошло из-за глупой опечатки, с пересмотренным тестом мое время выполнения составляет около 2/3 вашего - хотя я раскомментировал оператор print (mystr) в вашем примере - и он печатает исходную строку? Я не очень много программирую на python, поэтому не уверен, что я делаю что-то не так или действительно где-то есть ошибка. - person Andrew Y; 17.12.2009
comment
Я обновил пост новыми тестами, в том числе Эндрю, и отличающимися между фиксированными и переменными диктовками. - person Tor Valamo; 17.12.2009
comment
Тор ты действительно мастер своего дела. Я приветствую ваше стремление помочь мне в этом. Спасибо. - person Mike Trpcic; 17.12.2009
comment
@Tor, Майк сказал в другом комментарии, что его строки могут быть немного больше (большинство ... меньше 500). Не могли бы вы также попробовать строку большего размера, может быть, такую, как у вас, но с дюжиной или около того некодовых символов между каждой парой кодов? Это может больше дифференцировать решения (ваши, вероятно, получат преимущество). - person Peter Hansen; 17.12.2009
comment
Если вы читали изменения в моем посте, я упомянул, что большинство строк будут менее 500 символов, но будут и такие, которые достигают 5000. - person Mike Trpcic; 17.12.2009
comment
@Tor: я добавил вариант, который включает ту же логику в функцию, которая принимает строку для обработки и словарь в качестве параметра. Где-то посередине между моим предыдущим результатом и вашим, кажется. - person Andrew Y; 17.12.2009
comment
Я обновил новые тесты строк с рандомными вставками. Резкое изменение результатов! - person Tor Valamo; 17.12.2009
comment
Также обновлен с последним Эндрю, который теперь побеждает всех как на переменных, так и на фиксированных диктовках. - person Tor Valamo; 17.12.2009
comment
Кажется, что решение Эндрю Y постоянно находится в верхней части списка. - person Mike Trpcic; 17.12.2009
comment
@Mike: мы должны протестировать мои на длинных строках с большим количеством заменяемых символов. Интересно, тогда это станет более проблематичным. - person Andrew Y; 17.12.2009
comment
@Andrew, @Peter, @Claudiu — теперь протестировано с МНОЖЕСТВОМ вставок в ДЛИННЫЕ строки юникода в python 3. - person Tor Valamo; 17.12.2009
comment
Эндрю Y по-прежнему на высоте. - person Mike Trpcic; 17.12.2009
comment
@Tor: я удалил лишнюю конкатенацию в примере с переменной - см. мой edit2. Согласно моим тестам, это должно сбрить еще один крошечный бит :-) - person Andrew Y; 17.12.2009
comment
Альтернативное решение Питера было протестировано (фиксированный диктофон) и немного медленнее (0,01 раза), чем фиксированное решение Клаудиу. - person Tor Valamo; 17.12.2009
comment
@ Андрей, да, немного быстрее, но ты пока на высоте. Я закончил редактировать свой пост на сегодня :P - person Tor Valamo; 17.12.2009
comment
@Tor: действительно, мне тоже пора спать. Спасибо за это, было весело! :) - person Andrew Y; 17.12.2009
comment
Я добавил его только для полноты, но только для результатов, а не для кода. Мое собственное решение в этом тесте было настолько медленным, что я до сих пор смеюсь. - person Tor Valamo; 17.12.2009
comment
@gnibbler, твой теперь самый быстрый. - person Tor Valamo; 17.12.2009
comment
за что дисквалифицированный материал? - person Claudiu; 17.12.2009
comment
Слишком долго бежал. Смотрите секунды позади. Поэтому я запускал его только один раз и больше никогда. - person Tor Valamo; 17.12.2009

Попробуйте это, используя подстановку регулярных выражений и стандартное форматирование строк:

# using your stated values for str and dict:
>>> import re
>>> str = re.sub(r'(&[a-zA-Z])', r'%(\1)s', str)
>>> str % dict
'The \x1b[0;30mquick \x1b[0;31mbrown \x1b[0;32mfox \x1b[0;33mjumps over the \x1b[0;34mlazy dog'

Вызов re.sub() заменяет все последовательности амперсанда, за которыми следует одна буква, шаблоном %(..)s, содержащим тот же шаблон.

Форматирование % использует преимущество форматирования строк, которое может использовать словарь для указания замены, а не более часто встречающиеся позиционные аргументы.

В качестве альтернативы это можно сделать прямо в re.sub, используя обратный вызов:

>>> import re
>>> def dictsub(m):
>>>    return dict[m.group()]
>>> str = re.sub(r'(&[a-zA-Z])', dictsub, str)

На этот раз я использую замыкание для ссылки на словарь из функции обратного вызова. Этот подход может дать вам немного больше гибкости. Например, вы можете использовать что-то вроде dict.get(m.group(), '??'), чтобы избежать возникновения исключений, если у вас есть строки с нераспознанными последовательностями кода.

(Кстати, и "dict", и "str" ​​являются встроенными функциями, и у вас возникнут проблемы, если вы будете часто использовать эти имена в своем собственном коде. На всякий случай, если вы этого не знали. Они подходят для такой вопрос конечно))

Редактировать: я решил проверить тестовый код Tor и пришел к выводу, что он далеко не репрезентативен и на самом деле содержит ошибки. Сгенерированная строка даже не содержит амперсандов (!). Пересмотренный код ниже создает репрезентативный словарь и строку, аналогичную примерам входных данных OP.

Я также хотел убедиться, что выходные данные каждого алгоритма одинаковы. Ниже приведена исправленная тестовая программа, включающая только код Tor, мой код и код Клаудиу, потому что остальные не работали на вводе примера. (Я думаю, что все они ненадежны, если только словарь не сопоставляет в основном все возможные последовательности амперсанда, что и делал тестовый код Tor.) Это правильно запускает генератор случайных чисел, поэтому каждый запуск будет одинаковым. Наконец, я добавил небольшую вариацию с использованием генератора, который позволяет избежать некоторых накладных расходов на вызовы функций, для небольшого улучшения производительности.

from time import time
import string
import random
import re

random.seed(1919096)  # ensure consistent runs

# build dictionary with 40 mappings, representative of original question
mydict = dict(('&' + random.choice(string.letters), '\x1b[0;%sm' % (30+i)) for i in range(40))
# build simulated input, with mix of text, spaces, ampersands in reasonable proportions
letters = string.letters + ' ' * 12 + '&' * 6
mystr = ''.join(random.choice(letters) for i in range(1000))

# How many times to run each solution
rep = 10000

print('Running %d times with string length %d and %d ampersands'
    % (rep, len(mystr), mystr.count('&')))

# Tor Valamo
# fixed from Tor's test, so it actually builds up the final string properly
t = time()
for x in range(rep):
    output = mystr
    for k, v in mydict.items():
        output = output.replace(k, v)
print('%-30s' % 'Tor fixed & variable dict', time() - t)
# capture "known good" output as expected, to verify others
expected = output

# Peter Hansen

# build charset to use in regex for safe dict lookup
charset = ''.join(x[1] for x in mydict.keys())
# grab reference to method on regex, for speed
patsub = re.compile(r'(&[%s])' % charset).sub

t = time()
for x in range(rep):
    output = patsub(r'%(\1)s', mystr) % mydict
print('%-30s' % 'Peter fixed & variable dict', time()-t)
assert output == expected

# Peter 2
def dictsub(m):
    return mydict[m.group()]

t = time()
for x in range(rep):
    output = patsub(dictsub, mystr)
print('%-30s' % 'Peter fixed dict', time() - t)
assert output == expected

# Peter 3 - freaky generator version, to avoid function call overhead
def dictsub(d):
    m = yield None
    while 1:
        m = yield d[m.group()]

dictsub = dictsub(mydict).send
dictsub(None)   # "prime" it
t = time()
for x in range(rep):
    output = patsub(dictsub, mystr)
print('%-30s' % 'Peter generator', time() - t)
assert output == expected

# Claudiu - Precompiled
regex_sub = re.compile("(%s)" % "|".join(mydict.keys())).sub

t = time()
for x in range(rep):
    output = regex_sub(lambda mo: mydict[mo.string[mo.start():mo.end()]], mystr)
print('%-30s' % 'Claudio fixed dict', time() - t)
assert output == expected

Я забыл включить результаты тестов раньше:

    Running 10000 times with string length 1000 and 96 ampersands
    ('Tor fixed & variable dict     ', 2.9890000820159912)
    ('Peter fixed & variable dict   ', 2.6659998893737793)
    ('Peter fixed dict              ', 1.0920000076293945)
    ('Peter generator               ', 1.0460000038146973)
    ('Claudio fixed dict            ', 1.562000036239624)

Кроме того, фрагменты входов и правильный вывод:

mystr = 'lTEQDMAPvksk k&z Txp vrnhQ GHaO&GNFY&&a...'
mydict = {'&p': '\x1b[0;37m', '&q': '\x1b[0;66m', '&v': ...}
output = 'lTEQDMAPvksk k←[0;57m Txp vrnhQ GHaO←[0;67mNFY&&a P...'

Сравнивая с тем, что я видел в выводе тестового кода Tor:

mystr = 'VVVVVVVPPPPPPPPPPPPPPPXXXXXXXXYYYFFFFFFFFFFFFEEEEEEEEEEE...'
mydict = {'&p': '112', '&q': '113', '&r': '114', '&s': '115', ...}
output = # same as mystr since there were no ampersands inside
person Peter Hansen    schedule 17.12.2009
comment
Однако у этого есть проблема... если строка содержит совпадение, которого нет в словаре... - person Tor Valamo; 17.12.2009
comment
ОП не уточнил, что ему требуется пуленепробиваемость. Он может сказать, что гарантируется, что все последовательности, найденные в строке, находятся в словаре. Если бы каждый ответ без идеальной обработки ошибок был удален из StackOverflow, осталось бы всего несколько... - person Peter Hansen; 17.12.2009
comment
Дело не только в обработке ошибок, но и в том, что при этом будет полный сбой при малейшей ошибке. Я вижу, что ваш второй вариант справляется с этим, но с возвращаемым значением по умолчанию. - person Tor Valamo; 17.12.2009
comment
Иногда мне очень хочется, чтобы код полностью выходил из строя при малейшей ошибке. Если бы это было не так, я бы не нашел проблему в другой части моей программы, которая в первую очередь вставляла escape-последовательности ампсеранда. Конечно, мои модульные тесты для этой другой части говорят мне, что они генерируют только те шаблоны, которые охватываются показанным словарем, поэтому я знаю, что мне не нужна избыточная обработка ошибок, которую вы говорите о добавлении в мою красивую чистую программу, обременяющую меня. с дополнительными затратами на обслуживание. (Как видите, бывают случаи, когда некоторые люди считают обработку ошибок ненужной.) - person Peter Hansen; 17.12.2009
comment
Я бы использовал для этого lambda m: dict[m.group()] (но я бы также абстрагировал эту функциональность в свою собственную функцию). - person Chris Lutz; 17.12.2009
comment
@Chris, я считаю лямбда в основном способом запутывания кода, побуждая его упаковывать больше его в одну строку, где красивую отдельную именованную функцию удобнее читать. Каждому свое. Я уверен, что мы все упаковали бы все это как отдельную функцию в реальном коде. - person Peter Hansen; 17.12.2009
comment
Я пытался заставить ваше решение работать (используя обратный вызов, который вызывает dict.get(m.group(), '??'), но я продолжаю получать следующую ошибку: exceptions.TypeError: дескриптор 'get' требует 'dict' объект, но получил 'str'. Любые предложения относительно того, почему? Платформа Python 2.6. - person Mike Trpcic; 19.12.2009
comment
Я тоже на Python 2.6, так что это сработает. Похоже, вы используете имя переменной dict вместо mydict или d или чего-то еще. В вашем случае это класс словаря из встроенных модулей, а не имя, которому вы что-то присвоили. Если вы убедитесь, что не затеняете встроенные имена (удалите любое использование dict, str и т. д. в качестве имен переменных), ваши проблемы должны исчезнуть. В противном случае, пожалуйста, отправьте код, и мы можем это исправить. - person Peter Hansen; 19.12.2009
comment
Кроме того, вам, вероятно, следует использовать технику в моем обновленном ответе, помеченном как Peter 2, но включая более раннюю часть, где я динамически строю кодировку из словаря. Таким образом, вы можете избежать вызова get() и '??' аргумент полностью, делая его быстрее и проще, но все же надежным. - person Peter Hansen; 19.12.2009
comment
@Mike, конкретная ошибка может возникнуть из-за того, что вы смешиваете два соглашения об именах. В вашем исходном коде для имени переменной использовалось dict, так что это то, что использовал мой пример кода. Однако dict в этом dict.get должен быть любым именем словаря variable. Теперь вы назвали его по-другому (mydict?), так что код в данный момент не работает. В обновленном тестовом коде единственным использованием dict является встроенное имя, поэтому попробуйте использовать код оттуда и убедитесь, что исходные переменные больше не являются str и dict. - person Peter Hansen; 19.12.2009

Если вы действительно хотите углубиться в тему, взгляните на это: http://en.wikipedia.org/wiki/Aho-Corasick_algorithm

Очевидное решение путем перебора словаря и замены каждого элемента в строке занимает O(n*m) времени, где n — размер словаря, m — длина строки.

Принимая во внимание, что алгоритм Ахо-Корасика находит все записи словаря в O(n+m+f), где f — количество найденных элементов.

person WolfgangP    schedule 17.12.2009
comment
+1. Однако для этой текущей проблемы это кажется излишним для случайной замены строки. :П - person Tor Valamo; 17.12.2009

Если количество ключей в списке велико, а количество вхождений в строке невелико (и в основном равно нулю), то вы можете перебирать вхождения амперсандов в строку и использовать словарь с первым ключом. характер подстрок. Я не часто пишу код на python, поэтому стиль может немного отличаться, но вот мой взгляд на это:

str = "The &yquick &cbrown &bfox &Yjumps over the &ulazy dog"

dict = {"&y":"\033[0;30m",
        "&c":"\033[0;31m",
        "&b":"\033[0;32m",
        "&Y":"\033[0;33m",
        "&u":"\033[0;34m"}

def rep(s):
  return dict["&"+s[0:1]] + s[1:]

subs = str.split("&")
res = subs[0] + "".join(map(rep, subs[1:]))

print res

Конечно, возникает вопрос, что происходит, когда есть амперсанд, исходящий из самой строки, вам нужно каким-то образом экранировать его, прежде чем проходить через этот процесс, а затем отменить экранирование после этого процесса.

Конечно, как это часто бывает с проблемами производительности, правильно выбрать время для различных подходов к вашему типичному (а также наихудшему) набору данных и сравнить их.

РЕДАКТИРОВАТЬ: поместите его в отдельную функцию для работы с произвольным словарем:

def mysubst(somestr, somedict):
  subs = somestr.split("&")
  return subs[0] + "".join(map(lambda arg: somedict["&" + arg[0:1]] + arg[1:], subs[1:]))

РЕДАКТИРОВАТЬ2: избавиться от ненужной конкатенации, кажется, все еще немного быстрее, чем предыдущая на многих итерациях.

def mysubst(somestr, somedict):
  subs = somestr.split("&")
  return subs[0].join(map(lambda arg: somedict["&" + arg[0:1]] + arg[1:], subs[1:]))
person Andrew Y    schedule 17.12.2009
comment
@ Эндрю, вы можете использовать отдельные буквы для ключей, как я сделал в своем ответе, поскольку & подразумевается разделением. Это экономит выполнение "&"+.. для каждого элемента - person John La Rooy; 17.12.2009
comment
Я изменил этот код, чтобы он работал без & dict, и это не имело большого значения. гнибблер еще быстрее. - person Tor Valamo; 17.12.2009
comment
@Tor: я перепроверил - и прав ли я, что в последнем тестовом коде вообще нет амперсандов? тогда гниблер и мой код действительно выиграют. Но завтра мы должны отладить тестовый набор немного лучше, имхо. - person Andrew Y; 17.12.2009
comment
Я опубликую свой тестовый код Python 3, который использует символы Unicode и ОГРОМНЫЙ словарь. Это подвергает решения экстремальным нагрузкам (по крайней мере, 10 000 запусков: P). Но вы также можете придумать словари получше, например словари переменной длины, хотя это аннулирует большинство решений, за исключением нескольких. - person Tor Valamo; 17.12.2009
comment
@Tor: с нетерпением жду :) @gnibbler: похоже, что избавление от concat не имеет большого значения между нашими сценариями, что интересно. Я думаю, что разница между вашим и моим связана с накладными расходами карты/лямбда в моем? (иначе они эквивалентны, кажется). - person Andrew Y; 17.12.2009
comment
@Andrew Y, только сейчас я заметил твой комментарий о том, что я перепроверил. Я считаю, что вы правы, тестовый код Tor не использовал репрезентативные входные данные. Мой обновленный ответ включает в себя лучшие входные данные, но ваш код не работает, возможно, потому, что он не обрабатывает последовательности ампсеров, которых нет в словаре. - person Peter Hansen; 18.12.2009
comment
@Peter: да, это исключение для последовательностей, которых нет в словаре. Я написал в начале, что автономные амперсанды должны быть экранированы перед запуском. - person Andrew Y; 20.12.2009

Вот подход расширений C для python

const char *dvals[]={
    //"0-64
    "","","","","","","","","","",
    "","","","","","","","","","",
    "","","","","","","","","","",
    "","","","","","","","","","",
    "","","","","","","","","","",
    "","","","","","","","","","",
    "","","","","",
    //A-Z
    "","","","","",
    "","","","","",
    "","","","","",
    "","","","","",
    "","","","","33",
    "",
    //
    "","","","","","",
    //a-z
    "","32","31","","",
    "","","","","",
    "","","","","",
    "","","","","",
    "34","","","","30",
    ""
};

int dsub(char*d,char*s){
    char *ofs=d;
    do{
        if(*s=='&' && s[1]<='z' && *dvals[s[1]]){

            //\033[0;
            *d++='\\',*d++='0',*d++='3',*d++='3',*d++='[',*d++='0',*d++=';';

            //consider as fixed 2 digits
            *d++=dvals[s[1]][0];
            *d++=dvals[s[1]][1];

            *d++='m';

            s++; //skip

        //non &,invalid, unused (&) ampersand sequences will go here.
        }else *d++=*s;

    }while(*s++);

    return d-ofs-1;
}

Коды Python, которые я тестировал

from mylib import *
import time

start=time.time()

instr="The &yquick &cbrown &bfox &Yjumps over the &ulazy dog, skip &Unknown.\n"*100000
x=dsub(instr)

end=time.time()

print "time taken",end-start,",input str length",len(x)
print "first few lines"
print x[:1100]

Результаты

time taken 0.140000104904 ,input str length 11000000
first few lines
The \033[0;30mquick \033[0;31mbrown \033[0;32mfox \033[0;33mjumps over the \033[0;34mlazy dog, skip &Unknown.
The \033[0;30mquick \033[0;31mbrown \033[0;32mfox \033[0;33mjumps over the \033[0;34mlazy dog, skip &Unknown.
The \033[0;30mquick \033[0;31mbrown \033[0;32mfox \033[0;33mjumps over the \033[0;34mlazy dog, skip &Unknown.
The \033[0;30mquick \033[0;31mbrown \033[0;32mfox \033[0;33mjumps over the \033[0;34mlazy dog, skip &Unknown.
The \033[0;30mquick \033[0;31mbrown \033[0;32mfox \033[0;33mjumps over the \033[0;34mlazy dog, skip &Unknown.
The \033[0;30mquick \033[0;31mbrown \033[0;32mfox \033[0;33mjumps over the \033[0;34mlazy dog, skip &Unknown.
The \033[0;30mquick \033[0;31mbrown \033[0;32mfox \033[0;33mjumps over the \033[0;34mlazy dog, skip &Unknown.
The \033[0;30mquick \033[0;31mbrown \033[0;32mfox \033[0;33mjumps over the \033[0;34mlazy dog, skip &Unknown.
The \033[0;30mquick \033[0;31mbrown \033[0;32mfox \033[0;33mjumps over the \033[0;34mlazy dog, skip &Unknown.
The \033[0;30mquick \033[0;31mbrown \033[0;32mfox \033[0;33mjumps over the \033[0;34mlazy dog, skip &Unknown.

Предполагается, что он может работать со скоростью O(n), и потребовалось всего 160 мс (в среднем) для строки 11 МБ в My Mobile Celeron 1.6. ГГц ПК

Он также будет пропускать неизвестные символы как есть, например, &Unknown будет возвращаться как есть.

Дайте мне знать, если у вас возникнут проблемы с компиляцией, баги и т.д...

person YOU    schedule 17.12.2009
comment
Можете ли вы сравнить это с помощью моего теста? Казалось бы, если вы хотите изменить словарь, вам придется проделать большую работу... - person Tor Valamo; 17.12.2009
comment
Я вижу баг, это не замена символа, только амперсанд. - person Tor Valamo; 17.12.2009
comment
Не могли бы вы сказать мне, какая часть кода? *d++=dvals[s[1]][0];*d++=dvals[s[1]][1]; должен был сделать эту замену на самом деле. - person YOU; 17.12.2009
comment
&yquick -› \033[0;30myбыстро. Что тебя там быть не должно. - person Tor Valamo; 17.12.2009
comment
для изменений словаря нужно только обновить dvals и перекомпилировать его, только компиляция будет дополнительным шагом. - person YOU; 17.12.2009

Это похоже на то, что вы хотите - несколько строк заменяются одновременно с использованием регулярных выражений. Вот соответствующий код:

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)

print multiple_replace(dict, str)
person Claudiu    schedule 17.12.2009
comment
уже изменено. не уверен, что этот код быстрее, чем выполнение самого цикла; у него есть дополнительный вызов функции для каждой замены. придется время это для этого. - person Claudiu; 17.12.2009
comment
Да, это стало бы ужасно дорого для любого большого словаря и большого текста. - person charstar; 17.12.2009
comment
В моем словаре будет около 40 записей, и большинство моих строк будут менее 500 символов. Насколько это будет дорого по сравнению с зацикливанием str.replace() или предложением Питера Хэнсона? - person Mike Trpcic; 17.12.2009

Общее решение для определения правил замены — использовать подстановку регулярных выражений с помощью функции для предоставления карты (см. re.sub()).

import re

str = "The &yquick &cbrown &bfox &Yjumps over the &ulazy dog"

dict = {"&y":"\033[0;30m",
        "&c":"\033[0;31m",
        "&b":"\033[0;32m",
        "&Y":"\033[0;33m",
        "&u":"\033[0;34m"}

def programmaticReplacement( match ):
    return dict[ match.group( 1 ) ]

colorstring = re.sub( '(\&.)', programmaticReplacement, str )

Это особенно удобно для нетривиальных замен (например, для всего, что требует математических операций для создания замены).

person charstar    schedule 17.12.2009

Вот версия с использованием split/join

mydict = {"y":"\033[0;30m",
          "c":"\033[0;31m",
          "b":"\033[0;32m",
          "Y":"\033[0;33m",
          "u":"\033[0;34m"}
mystr = "The &yquick &cbrown &bfox &Yjumps over the &ulazy dog"

myparts = mystr.split("&")
myparts[1:]=[mydict[x[0]]+x[1:] for x in myparts[1:]]
print "".join(myparts)

Если есть амперсанды с неверными кодами, вы можете использовать это, чтобы сохранить их.

myparts[1:]=[mydict.get(x[0],"&"+x[0])+x[1:] for x in myparts[1:]]

Питер Хансен указал, что это не работает, когда есть двойной амперсанд. В этом случае используйте эту версию

mystr = "The &yquick &cbrown &bfox &Yjumps over the &&ulazy dog"
myparts = mystr.split("&")
myparts[1:]=[mydict.get(x[:1],"&"+x[:1])+x[1:] for x in myparts[1:]]
print "".join(myparts)
person John La Rooy    schedule 17.12.2009
comment
Ваш код не работает, если я не заменю mydict[x[0]] на mydict[& + x[0]] — когда я это делаю, он немного быстрее, чем мой первый подход. - person Andrew Y; 17.12.2009
comment
@ Эндрю, я подозреваю, что вы используете версию mydict с символом «&» перед клавишами. у моего нет таких - person John La Rooy; 17.12.2009
comment
Ваш предполагает, что за каждым & следует замена, которая быстро выйдет из строя, как только внезапно появится символ, которого нет в словаре. - person Tor Valamo; 17.12.2009
comment
@gnibbler: ах да. Я использовал исходные данные. Извините, правда. - person Andrew Y; 17.12.2009
comment
Я считаю, что это не удается в случае двойных амперсандов. - person Peter Hansen; 18.12.2009
comment
@Peter, спасибо, я добавил исправление для double & - person John La Rooy; 20.12.2009
comment
@gnibbler, с корректировкой моего тестового кода для предварительной сборки вашей версии словаря (без & в ключах) это получается чуть медленнее, чем решение Клаудиу, примерно в два раза быстрее, чем у Tor. - person Peter Hansen; 20.12.2009

Не уверен и в скорости этого решения, но вы можете просто прокрутить свой словарь и неоднократно вызывать встроенный

str.replace(old, new)

Это может работать прилично, если исходная строка не слишком длинная, но очевидно, что она пострадает, когда строка станет длиннее.

person Michael Patterson    schedule 17.12.2009
comment
на самом деле он не страдает от длины строки, он страдает от длины словаря. - person Tor Valamo; 17.12.2009
comment
Интересно... причина, по которой я думал, что длина строки будет более важной, заключалась в том, что она просматривает словарь только один раз, но многократно выполняет поиск по строке. Я понимаю, что и то, и другое повлияет на скорость, но почему она больше страдает от длины словаря? (не сомневаясь, что вы правы, просто интересно, почему?) - person Michael Patterson; 17.12.2009
comment
Поскольку вы вызываете замену один раз для каждого элемента словаря, поэтому чем больше элементов словаря, тем больше вызовов. Если строка длиннее, это не повлияет на нее так сильно. Но в любом случае это не имеет большого значения, если вы видите мой ответ с тестами. :П - person Tor Valamo; 17.12.2009
comment
Верно, я имел в виду, что по сравнению с другими методами он пострадает из-за длины строки, потому что каждый метод должен будет перебирать весь словарь, но не каждому методу придется многократно искать строку. Впрочем, вы правы, это не имеет большого значения, просто любопытно. :-п - person Michael Patterson; 17.12.2009
comment
@Michael, настоящая причина, по которой он не такой масштабируемый, заключается просто в том, что замена строки представляет собой цикл в чистом C, а цикл словаря - это цикл в Python. Там, где производительность имеет значение, в Python вы обычно не хотите выполнять множество операций Python внутри циклов Python. - person Peter Hansen; 19.12.2009

Проблема с такой массовой заменой в Python заключается в неизменности строк: каждый раз, когда вы заменяете один элемент в строке, вся новая строка будет снова и снова перераспределяться из кучи.

Поэтому, если вам нужно самое быстрое решение, вам нужно либо использовать изменяемый контейнер (например, список), либо написать этот механизм на простом C (или лучше на Pyrex или Cython). В любом случае я бы предложил написать простой синтаксический анализатор на основе простого конечного автомата и подавать символы вашей строки один за другим.

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

person bialix    schedule 17.12.2009

Поскольку кто-то упомянул об использовании простого синтаксического анализатора, я подумал, что приготовлю его с помощью pyparsing. Используя метод transformString pyparsing, pyparsing внутренне сканирует исходную строку и создает список совпадающего текста и промежуточного текста. Когда все будет сделано, transformString затем ''.join's этот список, так что нет проблем с производительностью при построении строк с приращением. (Действие синтаксического анализа, определенное для ANSIreplacer, выполняет преобразование совпадающих символов &_ в желаемую управляющую последовательность и заменяет совпадающий текст выходными данными действия синтаксического анализа. Поскольку только совпадающие последовательности будут удовлетворять выражению синтаксического анализатора, нет необходимости в действие parse для обработки неопределенных последовательностей &_.)

FollowedBy('&') не является строго обязательным, но он сокращает процесс синтаксического анализа, проверяя, что синтаксический анализатор действительно расположен на амперсанде, прежде чем выполнять более дорогостоящую проверку всех параметров разметки.

from pyparsing import FollowedBy, oneOf

escLookup = {"&y":"\033[0;30m",
            "&c":"\033[0;31m",
            "&b":"\033[0;32m",
            "&Y":"\033[0;33m",
            "&u":"\033[0;34m"}

# make a single expression that will look for a leading '&', then try to 
# match each of the escape expressions
ANSIreplacer = FollowedBy('&') + oneOf(escLookup.keys())

# add a parse action that will replace the matched text with the 
# corresponding ANSI sequence
ANSIreplacer.setParseAction(lambda toks: escLookup[toks[0]])

# now use the replacer to transform the test string; throw in some extra
# ampersands to show what happens with non-matching sequences
src = "The &yquick &cbrown &bfox &Yjumps over the &ulazy dog & &Zjumps back"
out = ANSIreplacer.transformString(src)
print repr(out)

Отпечатки:

'The \x1b[0;30mquick \x1b[0;31mbrown \x1b[0;32mfox \x1b[0;33mjumps over 
 the \x1b[0;34mlazy dog & &Zjumps back'

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

person PaulMcG    schedule 17.12.2009
comment
Пол, по крайней мере, это работает на реальном вводе (проверено с помощью тестового кода в моем обновленном ответе), а некоторые другие - нет. К сожалению, он очень медленный по сравнению с другими: занимает в 282 раза больше времени, чем решение re.sub. - person Peter Hansen; 18.12.2009

попробуй это

tr.replace("&y",dict["&y"])

tr.replace("&c",dict["&c"])

tr.replace("&b",dict["&b"])

tr.replace("&Y",dict["&Y"])

tr.replace("&u",dict["&u"])

person fahad    schedule 01.04.2015

person    schedule
comment
Это будет работать только в том случае, если перед амперсандом всегда есть пробел. - person John La Rooy; 17.12.2009
comment
я не хочу предполагать слишком много. поскольку OP предоставил образцы, я буду работать с этим образцом. - person ghostdog74; 17.12.2009