【问题标题】:Mass string replace in python?在python中替换大量字符串?
【发布时间】:2010-12-27 12:09:57
【问题描述】:

假设我有一个如下所示的字符串:

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 对的字典)。

任何建议都非常感谢,谢谢。

编辑:

正如 cmets 通过这个问题所指出的,我的字典是在运行时之前定义的,并且在应用程序生命周期的过程中永远不会改变。它是一个 ANSI 转义序列列表,其中包含大约 40 项。我要比较的平均字符串长度约为 500 个字符,但会有最多 5000 个字符的字符串(尽管这种情况很少见)。我目前也在使用 Python 2.6。

编辑#2 我接受 Tor Valamos 的回答是正确的,因为它不仅提供了一个有效的解决方案(尽管它不是 最佳 解决方案),而且还考虑了所有其他问题并做了大量工作比较所有这些。这个答案是我在 StackOverflow 上遇到过的最好、最有帮助的答案之一。向你致敬。

【问题讨论】:

  • 正如 Tor Valamo 指出的那样,您可能还需要考虑错误情况——例如,如果您的字典中没有 & 符号序列,以及您在应该单独保留的字符串,因为它是文本内容的一部分。
  • Mike,除了知道整个字符串长度之外,了解转义序列的密度或每个字符串的总数或其他信息对于完整的基准测试也很重要。
  • Peter:这是不可预测的,因为有些字符串有 15 个字符和 15 个转义序列,有些字符串有 500 个字符和 1 个转义序列。字符串来自用户,因此可以是他们想要的任何东西。对于基准测试,我假设每 25 个常规字符有一个转义序列。
  • 如果字符串来自用户,我会说错误处理有点好,是吗,彼得? :P
  • @Tor,当然,如果现在需要错误处理,那么可以提供它。如果“&W”也是转义码,则尚未定义在输入文本包含例如“A&W root beer”的情况下您想要做什么。

标签: python regex string performance replace


【解决方案1】:
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

claudiu 和 andrew 的解决方案都一直为 0,所以我不得不将其增加到 10 000 次运行。

我在 Python 3 中运行它(因为 unicode),替换了从 39 到 1024 的字符(38 是 & 符号,所以我不想包含它)。字符串长度可达 10.000,包括大约 980 次替换,长度为 0-20 的可变随机插入。从 39 到 1024 的 unicode 值会导致字符长度为 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 的代码使用不同的 dict,其中键不包含 '&'。Andrew 的代码也使用这个备用 dict,但并没有太大区别,可能只是 0.01x加速。)

【讨论】:

  • 这很简单,可能比正则表达式更快,除非真正的替换字典远大于 5 个元素
  • gnibbler:我的实际字典大约有 40 个元素。
  • @Mike,您必须进行测试才能确定,但​​正则表达式确实比简单替换要慢得多。我已经发布了一个使用 split/join 的答案,看看在各种条件下哪种方法更好
  • 你对克劳迪乌不太公平。首先,您将他作为函数调用,并且函数调用开销在 Python 中不可忽略。其次,他的编译步骤不会每次都进行,而是在程序启动时进行一次。
  • 我用包括 Andrew 在内的新基准更新了帖子,并在固定和可变 dicts 之间有所不同。
【解决方案2】:

试试这个,利用正则表达式替换和标准字符串格式:

# 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 的、我的和 Claudiu 的代码——因为其他人在样本输入上被破坏了。 (我认为它们都很脆弱,除非字典基本上映射了所有可能的&符号序列,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

我之前忘记包含基准测试结果:

使用字符串长度 1000 和 96 个 & 符号运行 10000 次 ('Tor 固定和可变字典',2.9890000820159912) ('彼得固定和可变字典',2.6659998893737793) ('彼得固定字典',1.0920000076293945) ('彼得发电机',1.0460000038146973) ('克劳迪奥固定字典',1.562000036239624)

另外,输入的sn-ps和正确的输出:

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

【讨论】:

  • 这有一个问题......如果字符串包含字典中没有的匹配......
  • OP 没有说明他需要防弹。他可能会说“保证在字符串中找到的所有序列都在字典中”。如果从 StackOverflow 中删除每个没有完美错误处理的答案,那么就只剩下少数几个了……
  • 这不仅仅是关于错误处理,而是事实上,在这个过程中,最小的错误会完全失败。我看到您的第二个替代方案使用默认返回值来处理这个问题。
  • 有时我非常想要“在最小的错误上完全失败”的代码。如果没有,我不会在我的程序的 other 部分中发现问题,该部分首先插入了 & 转义序列。当然,我对另一部分的单元测试告诉我它只生成显示的字典所涵盖的那些模式,所以我知道我不需要你所说的冗余错误处理添加到我漂亮的干净程序中,给我增加负担额外的维护开销。 (如您所见,有时有些人会认为没有必要进行错误处理。)
  • 我会为此使用lambda m: dict[m.group()](但我也会将此功能抽象为它自己的函数)。
【解决方案3】:

如果你真的想深入研究这个话题,看看这个:http://en.wikipedia.org/wiki/Aho-Corasick_algorithm

通过迭代字典并替换字符串中的每个元素的明显解决方案需要O(n*m)时间,其中n是字典的大小,m是字符串的长度。

而 Aho-Corasick 算法在 O(n+m+f) 中查找字典的所有条目,其中 f 是找到的元素的数量。

【讨论】:

  • +1。对于当前的这个问题,偶尔替换字符串似乎有点过分。 :P
【解决方案4】:

如果列表中的键数很大,而字符串中出现的次数很少(并且大部分为零),那么您可以遍历字符串中与符号的出现次数,并使用键控字典通过子字符串的第一个字符。我不经常在 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:]))

EDIT2:摆脱不需要的连接,在许多迭代中似乎仍然比以前快一点。

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

【讨论】:

  • @Andrew,您可以像我在回答中所做的那样使用单个字母作为键,因为拆分暗示了 &。这样可以节省为每个元素执行 "&"+..
  • 我修改了这段代码以使用 non-& dict 运行,但并没有太大的区别。 gnibbler 的仍然更快。
  • @Tor:我仔细检查了一遍——最新的测试代码中根本没有和号,我说得对吗?那么 gnibbler 和我的代码确实会赢。但是我们明天应该更好地调试测试套件,恕我直言。
  • 我将发布我的 python 3 测试代码,它使用 unicode 字符和一个巨大的字典。它将解决方案置于极端工作负载下(至少 10.000 次运行:P)。但是你也可以想出更好的字典,比如可变长度,尽管这会使大多数解决方案无效,除了少数。
  • @Tor:期待 :) @gnibbler:似乎摆脱 concat 对我们的场景没有太大影响,这很有趣。我认为你和我的区别是因为我的 map/lambda 开销? (否则它们似乎是等价的)。
【解决方案5】:

这里是python的C扩展方法

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) 运行,并且 在 My Mobile Celeron 1.6 GHz PC 中,11 MB 字符串仅需 160 毫秒(平均)

它也会按原样跳过未知字符,例如&amp;Unknown 将按原样返回

如果您对编译、错误等有任何问题,请告诉我...

【讨论】:

  • 您可以使用我的测试对其进行基准测试吗?看来,如果你想改变字典,你必须做很多工作......
  • 我看到了一个错误,它不是替换字符,而是替换符号。
  • 你能告诉我代码的哪一部分吗? *d++=dvals[s[1]][0];*d++=dvals[s[1]][1]; 应该实际替换。
  • &yquick -> \033[0;30my快。 y 不应该在那里。
  • 对于字典的修改,只需要更新dvals,重新编译,只是编译会多出一步。
【解决方案6】:

This 似乎可以满足您的需求 - 使用 RegExps 一次替换多个字符串。以下是相关代码:

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)

【讨论】:

  • 已经修改。不确定这段代码是否比循环本身更快;对于每个替换,它确实有一个额外的函数调用。将不得不为此计时。
  • 哎呀,这对于任何大型字典和大型文本来说都会变得非常昂贵。
  • 我的字典中大约有 40 个条目,而且我的大部分字符串都在 500 个字符以下。与循环的 str.replace() 或 Peter Hanson 的建议相比,这将有多昂贵?
【解决方案7】:

定义替换规则的一般解决方案是使用正则表达式替换,使用函数提供映射(请参阅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 )

这对于非平凡的替换特别好(例如任何需要数学运算来创建替换的东西)。

【讨论】:

    【解决方案8】:

    这是一个使用 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:]]
    

    Peter Hansen 指出,当有双与号时,这会失败。在这种情况下使用这个版本

    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)
    

    【讨论】:

    • 除非我将 mydict[x[0]] 替换为 mydict["&" + x[0]] - 当我做,它比我的第一种方法快一点。
    • @Andrew,我怀疑您使用的 mydict 版本在键前带有“&”。我的没有这些
    • 你的假设每个 & 后面都有一个替换,一旦突然出现不在字典中的字符,它会很快崩溃。
    • @gnibbler:是的。我使用了原始数据。确实很抱歉。
    • 我相信这在双&符号的情况下会失败。
    【解决方案9】:

    也不确定此解决方案的速度,但您可以遍历字典并反复调用内置的

    str.replace(old, new)

    如果原始字符串不太长,这可能会表现得不错,但随着字符串变长,它显然会受到影响。

    【讨论】:

    • 实际上它并没有受到字符串长度的影响,而是受到字典长度的影响。
    • 有趣...我认为字符串长度更重要的原因是因为它只循环遍历字典一次,但它会重复搜索字符串。我知道两者都会影响速度,但为什么字典长度会受到更多影响? (不是质疑你是对的,只是想知道为什么?)
    • 因为每个 dict 项调用一次 replace,所以 dict 项越多调用越多。如果字符串较长,则不会对其产生太大影响。但无论如何,如果你看到我对基准的回答,这并不重要。 :P
    • 对,我是说与其他方法相比,它会因字符串长度而受到影响,因为每个方法都必须遍历整个字典,但并非每个方法都必须重复搜索字符串.但是,您说得对,这并不重要,只是好奇。 :-p
    • @Michael,它不那么可扩展的实际原因仅仅是因为字符串替换是纯 C 中的循环,而字典循环是 Python 中的循环。在性能很重要的地方,在 Python 中,您通常不希望在 Python 循环中执行大量 Python 操作。
    【解决方案10】:

    在 Python 中进行这种大规模替换的问题在于字符串的不变性:每次替换字符串中的一项时,整个新字符串都会一次又一次地从堆中重新分配。

    因此,如果您想要最快的解决方案,您要么需要使用可变容器(例如列表),要么用普通 C 语言编写这个机器(或者在 Pyrex 或 Cython 中更好)。在任何情况下,我都建议基于简单的有限状态机编写简单的解析器,并逐个输入字符串的符号。

    建议的解决方案基于以类似方式工作的正则表达式,因为正则表达式在幕后使用 fsm。

    【讨论】:

      【解决方案11】:

      由于有人提到使用简单的解析器,我想我会使用 pyparsing 来做一个。通过使用 pyparsing 的 transformString 方法,pyparsing 在内部扫描源字符串,并构建匹配文本和中间文本的列表。全部完成后,transformString then ''.join's this list,所以按增量构建字符串没有性能问题。 (为 ANSIreplacer 定义的解析操作将匹配的 &_ 字符转换为所需的转义序列,并将匹配的文本替换为解析操作的输出。由于只有匹配的序列才能满足解析器表达式,所以不需要解析操作以处理未定义的 &_ 序列。)

      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'
      

      这肯定不会赢得任何性能竞赛,但如果您的标记开始变得更加复杂,那么拥有解析器基础将使扩展更容易。

      【讨论】:

      • Paul,至少它适用于真实输入(使用我更新的答案中的测试代码验证),而其他一些则没有。不幸的是,与其他解决方案相比,它非常慢:它需要的时间是 re.sub 解决方案的 282 倍。
      【解决方案12】:
      >>> a=[]
      >>> str = "The &yquick &cbrown &bfox &Yjumps over the &ulazy dog"
      >>> d={"&y":"\033[0;30m",                                                              
      ... "&c":"\033[0;31m",                                                                 
      ... "&b":"\033[0;32m",                                                                 
      ... "&Y":"\033[0;33m",                                                                 
      ... "&u":"\033[0;34m"}     
      >>> for item in str.split():
      ...  if item[:2] in d:
      ...    a.append(d[item[:2]]+item[2:])
      ...  else: a.append(item)
      >>> print ' '.join(a)
      

      【讨论】:

      • 只有在 & 符号前总是有空格时才有效
      • 我不想假设太多。由于 OP 提供了示例,我将使用该示例。
      【解决方案13】:

      试试这个

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

      【讨论】:

        猜你喜欢
        • 2017-03-16
        • 2012-04-23
        • 2014-07-08
        • 1970-01-01
        • 1970-01-01
        • 2023-03-21
        • 1970-01-01
        • 2019-10-01
        • 2022-11-17
        相关资源
        最近更新 更多