2017-07-16 6 views
4

some nice waysは、複数の文字列を同時にPythonで置換する処理を行います。しかし、私はそれを行うことができ、後方参照をサポートする効率的な機能を作成するのに問題があります。Pythonはバックリファレンスをサポートしている間に複数の文字列を置き換えます

表現/置換用語の辞書を使用してください。置換用語には、式と一致するものに逆参照が含まれている場合があります。

しかし

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) 

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

(\注1)私はSO後方参照が含まれていない表現/交換のペアのため正常に動作され、以下の機能に最初に述べた答えを置きます逆参照を含むキーでは機能しません。

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

を、エスケープは後方参照を使用して、キーをめちゃくちゃしているように見えるので、それは何も一致することはありません。

私は結局これを思いついた。しかし、入力パラメータに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])'はあなたの置き換えられたdictのキーではない '!n'とマッチします。 dictを使用して固定文字列を置き換えることはできます。 –

+0

@jonrsharpeありがとうございます、それは私が最後の例でやらなければならなかったことです。しかし、それは非常にclunkyです、例えば、一度マッチが行われたら、私はエスケープマッチ(naive_escaperを使用)に一致を変換するだけで、私は辞書(repPat)の置換値を参照することができます。私はおそらくreplacer関数に渡されている 'obj'パラメータがすでにどこかに入っていると考えているので、数時間の睡眠の後でこれをさらに調べます:-) – fzzylogic

答えて

3

更新:より良い代替方法については、Angus Hollands' answerを参照してください。


私は1つの大規模な正規表現にすべての辞書のキーを組み合わせるオリジナルのアイデアに固執するよりも、それを行うための簡単な方法を考えることができませんでした。

ただし、いくつかの問題があります。のは、このようなrepldictを想定してみましょう:私たちは、単一の正規表現にこれらを組み合わせた場合

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

、我々は(a)|(b)を取得する - ので、今(b)は、その後方参照が正しく動作しないことを意味し、もはやグループ1、ではありません。

もう1つの問題は、どの交換を使用するかわからないということです。正規表現がテキストbと一致する場合、\1bが適切な代替品であることをどのようにして知ることができますか?不可能です;十分な情報がありません。

これらの問題を解決するにはそれほどのような名前のグループ内のすべての辞書キーを囲むことである。

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

今、私たちは簡単にマッチしたキーを特定し、このグループにそれらを相対にするために後方参照を再計算することができます。その結果、\1bは「グループ2の後の最初のグループ」を参照します。


ここでの実装です:

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

シンプルは以下のように複雑で、コードよりも優れている(あなたが期待通りに動作しないコーディング理由は、([-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のアプローチをすべてのセレクタを1つの正規表現にして、ただ1つの.sub呼び出しを行うという方法に夢中になりました。 – fzzylogic

2

だけ後方参照にグループインデックスを変更することで、事前に高価なものを事前に計算Rawingと同様のソリューションを、。また、名前のないグループを使用します。

ここでは、キャプチャグループ内のそれぞれのケースを静かに折り返し、その後、絶対参照位置で適切なサブグループを正しく識別するために、後方参照の置換を更新します。 Replacer関数を使用するときは、デフォルトで後方参照が機能しないことに注意してください(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

フィードバックいただきありがとうございます。別のソリューションを試してみるといいです:) –

関連する問題