解析字符串中的嵌套引用

Parse nested references in string

上下文

在 Python 中,给定的是包含嵌套的点标记引用的任意字符串,稍后将用实际值替换。

str = 'Allocate {ref:network.node.{ref:global.environment}.api} with {ref:local.value}'

引用需要从里到外替换,所以先ref:global.environment='prod',然后ref:network.node.prod.api='/prod/api ', ref:local.value='UUID', 所以结果为:

result = 'Allocate /prod/api with UUID'

正在解析

尝试使用 pyparsing 解决此问题,因为正则表达式在嵌套引用中有点丢失。目标是获得一个参考列表,我可以在以后的后续步骤中 process/replace。

lbrack = '{ref:'
rbrack = '}'

ref = Forward()
ref << lbrack + Word(alphas, alphanums + '.') + ZeroOrMore(ref) + rbrack

ref.parseString(str)

与此类似的结果会有所帮助:

references = [['{ref:global.environment}'], '{ref:network.node.{ref:global.environment}.api}', '{ref:local.value}']

但是我错过了一些解析指令来让它工作,也许你有一个想法。感谢您的支持。

#更新 1 - 解决方案

正在提取PaulMcG的答案,当前代码为:

lbrack = "{ref:"
rbrack = "}"
ref = pp.Forward()
ident = pp.Word(pp.alphas, pp.alphanums)
ref <<= pp.Group(lbrack + pp.delimitedList(ref | ident, delim=".") + rbrack)

def eval_ref(tokens):
    # Skip lbrack and rbrack, i.e. [1:-1]
    return reduce(operator.getitem, tokens[0][1:-1], ns)

ref.addParseAction(eval_ref)
test = 'Allocate {ref:network.node.{ref:global.environment}.api} with {ref:local.value}'
print(ref.transformString(test))

作为旁注,将 eval_ref() 的代码缩减为一行。

如果您可以执行扫描并自行替换,您可以这样做(例如使用一个简单的字典来插入值):

from typing import List, Tuple

string = 'Allocate {ref:network.node.{ref:global.environment}.api} with {ref:local.value}'

values = {
    'global.environment': 'prod',
    'network.node.prod.api': '/prod/api',
    'local.value': 'UUID'
}


# Returns the start, end, and name of the next reference to replace.
# Note that for nested references, this will always be the most nested one.
def get_next_ref(input_string: str) -> Tuple[int, int, str]:
    starting_stack = []
    for i, char in enumerate(input_string):
        if char == '{':
            starting_stack.append(i)
        elif char == '}':
            start = starting_stack.pop()
            return start, i, input_string[start:i+1]
    return -1, -1, ''


next_ref = get_next_ref(string)
while next_ref[0] != -1:
    value = values[next_ref[2][5:-1]]
    print("Replacing '%s' with '%s'" % (next_ref[2], value))
    string = string.replace(next_ref[2], value)
    next_ref = get_next_ref(string)

print("Final string:", string)

显然它需要扩展以处理不属于引用的大括号(如果它们可能存在于您正在处理的字符串中)并且我没有包含错误检查,例如,不匹配大括号或对未知值的引用。

你的方向是正确的,但我需要帮助你解决一个常见的基本问题。

当人们使用以下方式定义限定标识符时,我经常看到这种情况:

Word(alphas, alphanums + ".")

它有一些固有的问题,因为它不仅会匹配“a”和“a.b.c.d”,还会匹配“a.”、“a...”、“a.c”。 ”和“a..c..0”。都作为标识符。在您的情况下,您还希望支持嵌入式引用来代替限定标识符。

因此,请这样想:

qualified_ident ::= ident_term ["." ident_term]...
ident_term := reference | identifier
reference := "{ref:" qualified_ident "}"
identifier := "A-Za-z" "A-Za-z0-9"...

现在您的限定标识可以由引用组成,引用本身可以由限定标识组成。

在 pyparsing 中,这看起来像(使用带“.”的分隔列表作为合格的 ident):

ref = Forward()
ident = Word(alphas, alphanums)
ref <<= Group(lbrack + delimitedList(ref | ident, delim=".") + rbrack)

现在 delimitedList 将取消“.”定界符,但我们并不关心,因为无论如何我们都必须跨过它们。我们将编写一个解析操作来解析对某些查找数据的引用。

首先,让我们从一些 JSON 创建一个简单的嵌套字典来支持您的示例字符串:

# define a nested dict for lookup values from refs
import json
ns = json.loads("""
{
    "network" : {
        "node" : {
            "prod": {
                "api": "prod_api"
            },
            "test": {
                "api": "test_api"
            }
        }
    },
    "local" : {
        "value" : 1000
    },
    "global": {
        "environment" : "prod"
    }
}
""")

现在我们将编写一个解析操作,它将使用此命名空间字典评估引用的路径:

def eval_ref(tokens):
    ret = ns

    # uncomment for debugging
    # print(tokens[0])

    # resolve next level down in the reference path
    for t in tokens[0][1:-1]:
        ret = ret[t]
    return ret

# and add as a parse action to ref
ref.addParseAction(eval_ref)

应该可以了,让我们在你的测试字符串上试试(我重命名了它,因为 str 是 Python 中的内置类型,用你的变量名掩盖它不好)。不过,我们将使用 transformString 而不是 parseStringtransformString 将用任何解析操作发出的文本替换任何源文本(或者如果包含在 Suppress 中则被抑制),这将递归地为您发生,以便您的内部 ref 将得到评估,然后外部 ref 将得到使用该内部解析值进行评估。

test = 'Allocate {ref:network.node.{ref:global.environment}.api} with {ref:local.value}'
print(ref.transformString(test))

应该给:

Allocate prod_api with 1000