2017-07-16 89 views
4

some nice ways来处理python中的同时多字符串替换。但是,我无法创建一个可以做到这一点的高效函数,同时也支持反向引用。Python替换多个字符串,同时支持反向引用

我想要的是使用表达式/替换术语字典,其中替换术语可能包含反向引用到表达式匹配的内容。

例如(注意是\ 1)

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

,我把苏答案在一开始就提到到下面的功能,这对于表达/更换对正常工作不包含反向引用:

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

然而,它不会为确实包含反向引用的主要工作..

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

如果我做手工,它工作正常。如预期例如: -

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

作品:

'not newData.exists()' 

在花哨的功能,排出似乎搞乱使用了backref的关键,所以它永远不会匹配任何内容。

我终于想出了这个。但是,请注意在输入参数支持backrefs的问题不解决,我只是手动处理它的替代品功能:

def replaceAll(repPat, text): 
    def replacer(obj): 
     match = obj.group(0) 
     # manually deal with exclamation mark match.. 
     if match[:1] == "!": return 'not ' + match[1:] 
     # here we naively escape the matched pattern into 
     # the format of our dictionary key 
     else: return repPat[naive_escaper(match)] 

    pattern = re.compile("|".join(repPat.keys())) 
    return pattern.sub(replacer, text) 

def naive_escaper(string): 
    if '=' in string: return string.replace('=', '\=') 
    elif '|' in string: return string.replace('|', '\|') 
    else: return string 

# manually escaping \ and = works fine 
repPat = {'!([a-zA-Z_])':'', '&&':'and', '\|\|':'or', '\=\=\=':'=='} 
replaceAll(repPat, "(!this && !that) || !this && foo === bar") 

返回:

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

因此,如果任何人有一个想法如何制作支持反向引用的多字符串替换函数并接受替换条款作为输入,我非常感谢您的反馈。

+0

为什么不在'replDict'的键中包含任何必要的正则表达式转义,所以你不需要应用're.escape'? – jonrsharpe

+0

这不仅仅是一个转义问题:'!([a-zA-Z])'将匹配'!n',这不是替代字典的关键。您只能使用字典替换固定的字符串。 –

+0

@jonrsharpe谢谢,是的,这就是我最终要做的最后一个例子。但是它非常笨重,例如,一旦匹配成功,我必须将匹配转换为转义匹配(使用naive_escaper),以便我可以在字典中查找替换值(repPat)。我怀疑,也许传递给替代函数的'obj'参数已经在它的内部的某个地方了,所以在几个小时的睡眠之后会进一步探究:-)。 – fzzylogic

回答

3

更新:有关更好的选择,请参见Angus Hollands' answer


我想不出一个更简单的方式做到这一点,而不是与所有字典键组合成一个巨大的正则表达式的最初的想法坚持下去。

但是,有一些困难。让我们假设一个repldict这样的:

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

如果我们结合这些到一个单一的正则表达式,我们得到(a)|(b) - 所以现在(b)不再是第1组,这意味着它的反向引用将无法正常工作。

另一个问题是我们无法确定要使用哪个替换。如果正则表达式匹配文本b,我们怎么能发现\1b是合适的替代品?这是不可能的;我们没有足够的信息。

解决这些问题的解决方案是封闭的每个字典的关键在一个名为组,像这样:

(?P<group1>(a))|(?P<group2>(b)) 

现在,我们可以很容易地识别出相匹配的钥匙,并重新计算反向引用,使它们相对于这个群体。所以\1b是指“第二组之后的第一组”。


这里的实现:

def replaceAll(repldict, text): 
    # split the dict into two lists because we need the order to be reliable 
    keys, repls = zip(*repldict.items()) 

    # generate a regex pattern from the keys, putting each key in a named group 
    # so that we can find out which one of them matched. 
    # groups are named "_<idx>" where <idx> is the index of the corresponding 
    # replacement text in the list above 
    pattern = '|'.join('(?P<_{}>{})'.format(i, k) for i, k in enumerate(keys)) 

    def repl(match): 
     # find out which key matched. We know that exactly one of the keys has 
     # matched, so it's the only named group with a value other than None. 
     group_name = next(name for name, value in match.groupdict().items() 
          if value is not None) 
     group_index = int(group_name[1:]) 

     # now that we know which group matched, we can retrieve the 
     # corresponding replacement text 
     repl_text = repls[group_index] 

     # now we'll manually search for backreferences in the 
     # replacement text and substitute them 
     def repl_backreference(m): 
      reference_index = int(m.group(1)) 

      # return the corresponding group's value from the original match 
      # +1 because regex starts counting at 1 
      return match.group(group_index + reference_index + 1) 

     return re.sub(r'\\(\d+)', repl_backreference, repl_text) 

    return re.sub(pattern, repl, text) 

测试:

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

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

# output: not newData.exists() or newData.val().length == 1 
#   (not this and not that) or not this and foo == bar 

注意事项:

  • 仅支持数字反向引用;没有命名参考。
  • 默默接受无效的反向引用,如{r'(a)': r'\2'}。 (这些将有时抛出一个错误,但并非总是如此。)
+0

非常棒的解决方案谢谢你的分享! – fzzylogic

0

简单比复杂好,如下面的代码更易读(之所以按预期的代码不工作是([A-ZA-Z_])不应该在re.escape):

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

这不是什么fzzylogic问的答案。 – Alfran

+0

@williezh感谢您的回复,我很感激。是的,你说的正确,re.escape打破了正则表达式,所以只转义非正则表达式是一种方法。但是如果可能的话,我很想设计一些能够“知道”哪些东西可以逃脱,哪些不可以的东西,所以我可以把它忘掉,随时随地调用它。 for循环当然更具可读性,但我有点着迷于Andrew Clarke将所有选择器合并为一个正则表达式并仅执行一次.sub调用的方法。 – fzzylogic

2

类似的解决方案Rawing,只能通过反向引用修改组预先计算指数的昂贵的东西提前。另外,使用未命名的组。

在这里,我们静静地将每个案例包装在一个捕获组中,然后使用反向引用更新任何替换以通过绝对位置正确地识别合适的子组。请注意,使用替代功能时,反向引用在默认情况下不起作用(您需要拨打match.expand)。

import re 
from collections import OrderedDict 
from functools import partial 

pattern_to_replacement = {'&&': 'and', '!([a-zA-Z_]+)': r'not \1'} 


def build_replacer(cases): 
    ordered_cases = OrderedDict(cases.items()) 
    replacements = {} 

    leading_groups = 0 
    for pattern, replacement in ordered_cases.items(): 
     leading_groups += 1 

     # leading_groups is now the absolute position of the root group (back-references should be relative to this) 
     group_index = leading_groups 
     replacement = absolute_backreference(replacement, group_index) 
     replacements[group_index] = replacement 

     # This pattern contains N subgroups (determine by compiling pattern) 
     subgroups = re.compile(pattern).groups 
     leading_groups += subgroups 

    catch_all = "|".join("({})".format(p) for p in ordered_cases) 
    pattern = re.compile(catch_all) 

    def replacer(match): 
     replacement_pattern = replacements[match.lastindex] 
     return match.expand(replacement_pattern) 

    return partial(pattern.sub, replacer) 


def absolute_backreference(text, n): 
    ref_pat = re.compile(r"\\([0-99])") 

    def replacer(match): 
     return "\\{}".format(int(match.group(1)) + n) 

    return ref_pat.sub(replacer, text) 


replacer = build_replacer(pattern_to_replacement) 
print(replacer("!this.exists()")) 
+0

不错的解决方案,比我自己的更好(这支持命名组并且更高效)。+1,我在回答中添加了一个链接。 –

+0

感谢您的反馈。很高兴尝试不同的解决方案:) –