Python中带转义的递归字符串替换

2 投票
1 回答
1745 浏览
提问于 2025-04-18 14:59

我写了一个简单的Python脚本,用来根据源文件夹的结构生成项目。在这个脚本里,我使用了Formatter,因为它很方便,可以用字典(甚至是嵌套字典)来替换字符串。

不过现在,在扩展这个脚本的时候,我需要更复杂的替换功能。首先,我希望替换可以是递归的。也就是说,一个字段替换后的字符串可能还需要自己进行格式化(使用相同的参数)。其次,我需要根据提供的函数来处理最终的字符串(目前唯一的用例是re.escape,用于处理正则表达式的转义)。

我查找了Python内置的功能,但没有找到什么有用的。显然,Formatter(按原样提供的)不符合这些要求。


我第一次尝试是使用一个简单的函数,像这样:

def expand_vars(string, vars):
    while True:
        expanded = string.format(**vars)
        if expanded == string:
            break
        string = expanded
    return string

这个函数会不断调用format,直到字符串没有变化为止(这意味着所有字段都被替换了)。

但是在这里嵌入转义并不容易。我只需要转义被替换的值(而不是整个string),而且只需要对最终的值进行转义(每次调用都转义会导致字符串的某些部分被多次转义)。

这个函数的另一个问题是,它可能会无意中创建一些不是字段的字段。当一个字段以字符串{a结尾,而另一个字段在下一次迭代中以b}结尾时,我们会得到一个意想不到的字段{ab}。(这可能被视为一个特性,但在我的情况下我并不这样认为。)


另一种方法是对Formatter进行子类化。我最终得到了这样的代码:

class RecursiveEscapingFormatter(Formatter):
    def __init__(self, escape=None):
        Formatter.__init__(self)
        self.escape = escape

    def get_field(self, field_name, args, kwargs):
        obj, arg_used = super(RecursiveEscapingFormatter, self).get_field(field_name, args, kwargs)
        if self.escape is None:
            nonEscapingFormatter = self
        else:
            nonEscapingFormatter = copy.copy(self);
            nonEscapingFormatter.escape = None
        obj = nonEscapingFormatter.vformat(obj, args, kwargs)
        return obj, arg_used

    def convert_field(self, value, conversion):
        result = super(RecursiveEscapingFormatter, self).convert_field(value, conversion)
        if self.escape is not None:
            result = self.escape(result)
        return result

现在的问题是,我无法确保正确调用check_unused_args。我看不出有什么合理的方法(不需要完全重写整个类)来跟踪在get_field中的递归调用所使用的参数。我自己并不需要这个,但要创建一个合适的类(一个可以被后续继承的类……)就需要妥善处理check_unused_args。该怎么做呢?


或者,也许还有更好的方法来解决这个问题(递归替换和转义)?

1 个回答

2

我遇到过类似的问题,这是我解决它的方法。

from string import Formatter

class RecursivelySubstitutedDictionary:
    def __init__(self, dictionary):
        self.formatter = Formatter()
        self.dictionary = dictionary
        self.substituting = set([])

    def __getitem__(self, key):
        if(key in self.substituting):
            raise ValueError("Cyclic reference. Key: %s." % key)
        self.substituting.add(key)
        unsubstitutedval = self.dictionary[key]
        substitutedval = self.formatter.vformat(unsubstitutedval,[],self)
        self.substituting.remove(key)
        return substitutedval

使用示例

regulardict = {
    'hi': 'hello {arg}',
    'arg': '{arg_1}{arg_2}',
    'arg_1': 'wo',
    'arg_2': 'rld',
}

print RecursivelySubstitutedDictionary(regulardict)['hi']
# prints hello world



cyclicdict = {
    'hi': 'hello {arg}',
    'arg': '{hi}',
}

print RecursivelySubstitutedDictionary(cyclicdict)['hi']
# raises ValueError: Cyclic reference. Key: hi.

你也可以考虑把替换后的值缓存起来,如果调用了 __setitem__,就清空这个缓存。至少这是我在原始代码中所做的。

撰写回答