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

时间:2017-04-11 23:27:43

标签: xml xpath schematron

我正在编写一个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>

我使用了这个schematron文件:

<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()单个元素,如this answer中所述。

那么我怎样才能真正检查每个属性,孩子们中没有等效元素?

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

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

有什么想法吗?我觉得我差点儿!

1 个答案:

答案 0 :(得分:1)

我终于通过使用变量来实现它。

我使用了这个schematron:

<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>

感谢EiríkrÚtlendi!