Xpath 通配符仅返回第一个元素



我正在编写一个 schematron 来验证以下 xml 文件:

<root version="1.0">
<zone map="fields.map" display_name="Fields">
<zone.rectangles>
<rectangle h="2" w="2" x="0" y="0" />
</zone.rectangles>
</zone>
</root>

我想确保如果声明了任何元素的属性,则该元素不能包含与该属性同名的子元素。

例如,如果<zone>具有属性map,则<zone>不能包含元素<zone.map>

因此,以前的 xml 文件有效,但以下文件无效:

无效:

<root version="1.0">
<zone map="fields.map" display_name="Fields">
<zone.map>fields.map</zone.map>
<zone.rectangles>
<rectangle h="2" w="2" x="0" y="0" />
</zone.rectangles>
</zone>
</root>

另一方面,这个是有效的:

有效:

<root version="1.0">
<zone display_name="Fields">
<zone.map>fields.map</zone.map>
<zone.rectangles>
<rectangle h="2" w="2" x="0" y="0" />
</zone.rectangles>
</zone>
</root>

我得到了它与这个架构文件一起工作:

<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern>
<title>Attribute usage</title>
<!-- Every element that has attributes -->
<rule context="*[@*]">
<!-- The name of its children should not be {element}.{attribute} -->
<assert test="name(*) != concat(name(), '.', name(@*))">
The attribute <name />.<value-of select="name(@*)" /> is defined twice.
</assert>
</rule>
</pattern>
</schema>

经过多次不幸的尝试,我花了大约 4 个小时才能使其正常工作,所以我对这个架构非常满意,并开始对其进行更多测试。

我真的很失望地看到它仅适用于每个元素的第一个属性。例如,对于zone元素,仅测试map属性。因此,将<zone.display_name>元素放在<zone map="" display_name="">中不会使架构失败,而像<zone display_name="" map="">这样的反转属性会触发失败。

如果我理解得很好,似乎问题在于通配符@*实际上并没有用作concat(name(), '.', name(@*))中的列表,因为concat() 实际上需要单个字符串,而name()需要单个元素,如本答案中所述。

那么我如何才能实际检查每个属性,子属性中没有等效的元素呢?

这是一个嵌套循环,可以在伪代码中表示为:

for attribute in element.attributes:
for child in element.children:
if child.name == element.name + "." + attribute.name:
raise Error

知道吗?我觉得我离得很近!

我终于通过使用变量让它工作了。

我使用了这个模式:

<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern>
<title>Attribute usage</title>
<!-- Elements that contains a dot in their name -->
<rule context="*[contains(name(), '.')]">
<!-- Take the part after the dot -->
<let name="attr_name" value="substring-after(name(), '.')" />
<!-- Check that there is no parent's attributes with the same name -->
<assert test="count(../@*[name() = $attr_name]) = 0">
The attribute <name /> is defined twice.
</assert>
</rule>
</pattern>
</schema>

Schematron真的很强大,但你必须掌握它的窍门......

这个问题的更通用的答案:

如果你想循环通配符*@*,那么count()就是你的朋友,因为它实际上考虑了元素列表。

如果您发现自己卡住了,请尝试将问题颠倒过来。我遍历属性,然后遍历子元素,而现在我遍历每个元素,然后检查其父元素的属性。

如果要使用父级上下文中的信息,但发现自己陷入[]关闭,请使用变量来获取值。
例如,如果您尝试../@*[name() = name(..)],它不会执行您想要的操作,因为[]内部name(..)引用属性的父级名称,而不是当前上下文元素的名称。
如果将值提取为<let name="element_name" value="name()" />,那么就可以开始了:../@*[name() = $element_name]

当您打开方括号时,您将无法再访问这些括号之外的元素,因此请使用变量将它们放入。

编辑:

可以使用current()函数从括号内获取上下文元素,而无需使用变量。我的最终架构是:

<schema xmlns="http://purl.oclc.org/dsdl/schematron">
<pattern>
<title>Attribute usage</title>
<!-- Elements that contains a dot in their name -->
<rule context="*[contains(name(), '.')]">
<!-- Check that there is no parent's attributes with the same name -->
<assert test="not(../@*[name() = substring-after(name(current()), '.')])">
The attribute <name /> is defined twice.
</assert>
</rule>
</pattern>
</schema>

感谢埃里克尔·乌特伦迪!

最新更新