解析字符串中的嵌套引用
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
而不是 parseString
。 transformString
将用任何解析操作发出的文本替换任何源文本(或者如果包含在 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
上下文
在 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
而不是 parseString
。 transformString
将用任何解析操作发出的文本替换任何源文本(或者如果包含在 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