我使用 xmlstarlet 来验证 XML 文件。
当失败时,我将获得错误的数字位置。但我想要 XPath。有没有一个工具可以将位置转换为 XPath?
对于格式良好的 XML 文件,应该始终有一个与该位置相对应的 XPath。
问题:在 Linux 命令行上:给定 XML 文件内的行+字符位置,如何以编程方式获取相应的 XPath?
示例:我的模式表示苹果必须是红色或绿色。但我有一个棕色的苹果。所以我在位置“2.27”处收到错误。
$ cat schema.xsd
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="fruit">
<xs:complexType>
<xs:sequence>
<xs:element name="apples">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="green"/>
<xs:enumeration value="red"/>
</xs:restriction>
</xs:simpleType>
</xs:element>
<xs:element name="bananas"/>
<xs:element name="cherries"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
✓
$ cat badfruit.xml
<fruit>
<apples>brown</apples>
<bananas>yellow</bananas>
<cherries>red</cherries>
</fruit>
✓
$ xmlstarlet validate --well-formed badfruit.xml
badfruit.xml - valid
✓
$ xmlstarlet validate --err --xsd schema.xsd badfruit.xml
badfruit.xml:2.27: Element 'apples': [facet 'enumeration'] The value 'brown' is not an element of the set {'green', 'red'}.
badfruit.xml - invalid
$ xmlstarlet elements badfruit.xml
fruit
fruit/apples
fruit/bananas
fruit/cherries
✓
想要:执行此操作的命令:
$ magiccommand "badfruit.xml:2.27"
/fruit/apples
有这样的工具吗?
背景信息:我现实生活中的例子有点复杂。我正在处理一个 10k 行的文件,其中 XML 元素名称已在多个不同的层次结构级别上重复使用。因此,如果验证因“Element 'x' invalid”而失败,那么这对我来说意义不大,因为“x”可能会出现在几个不同的级别。而这些都是由不同的Java代码生成的。因此,准确了解 XPath 受到的影响对我来说非常有帮助。目前,我只是在图形 XML 编辑器(带有 XML 工具插件的 Notepad++)中重新打开该文件并在其中重新验证。然后就会直接跳到错误位置。光标下的当前 XPath 将位于状态栏的左下角。但我想避免这个额外的步骤并留在命令行上。
这是两张截图。
带有XML 工具 插件的 Notepad++ 在屏幕左下角的光标处显示 XPath。所以我基本上想要这个功能,但不是在 Notepad++ 内,而是在命令行上。
2.27 处的光标显示 XPath
/fruit
因此 xmlstarlet 输出
2.27
作为错误位置,但在 Notepad++ 中,您实际上必须位于 2.26
位置才能获得我想要的 XPath。
2.27 处的光标显示 XPath
/fruit/apples
从错误消息中构建错误元素 xpath,然后使用 xml2xpath 获取 xpath 可能是一个选项
echo "badfruit.xml:2.27: Element 'apples': [facet 'enumeration'] The value 'brown' is not an element of the set {'green', 'red'}." | sed -rne "s@.* Element '([^']+)': .*value '([^']+)' .*@//\1[.='\2']@p"
结果
//apples[.='brown']
使用 xml2xpath
获取其 XPathxml2xpath.sh -a -s "//apples[.='brown']" -x tmp.xml
结果(已编辑)
...
XPath expressions found: 1 (absolute, unique elements, use -r to override)
================================================================================ (2022-12-28 12:51:34 -03)
/fruit/apples[1]
鉴于此 XML
<fruit>
<apples>brown</apples>
<bananas>yellow</bananas>
<cherries>red</cherries>
<apples>brown</apples>
</fruit>
它会回来
/fruit/apples[1]
/fruit/apples[2]
注意:
xml2xpath
需要xmllint
,但它可以在大多数Linux发行版上访问。
使用列号可能会很棘手,因为可能会出现错误和警告消息 报告 (1) 开始标记、(2) 结束标记的开始/结束列 标签、(3) 节点名称或 (4) 节点值。 但由于您想要格式化 XML 文件的 XPath 表达式,可以使 事情变得更简单。
通过打印 max 的格式化程序运行 XML 文件。一个元素标签 每行属性位于同一行,然后将 XSLT 与 XML 结合使用 维护行号的解析器。 使用撒克逊语
lineNumber
扩展 可以在给定行号的情况下提取 XPath 表达式。
它由 Saxon 6.5.5 XSLT 1.0 处理器支持,并且通过
libexslt
、xsltproc 和 xmlstarlet
(可能还有其他 XSLT 处理器)。
更新的 Saxon-PE 和 Saxon-EE 处理器(自版本 9.9.1 起)
支持
行号和列号。也是如此
SAX2
API。
一些快速测试表明 Saxon 6.5.5 处理了 106MB 电子表格 XML 文件包含预期的 200 万行。
libexslt
的
saxon:line-number()
,
但是,从未返回超过 65535 的值,从而限制了范围
可用行号。该限制可能与内存相关,但事实并非如此
可通过 xsltproc
或 xmlstarlet
命令行选项进行配置
(两者都是基于 libxml2)。
libxml2
有一个 XML_PARSE_BIG_LINES
解析器选项,允许行号
大于 65535 才能在错误消息和中正确报告
saxon:line-number
扩展的输出。
我做了一个补丁
对于 xmlstarlet
1.6.1 添加了 --big-lines
命令行选项
申请 XML_PARSE_BIG_LINES
(和 --huge
申请 XML_PARSE_HUGE
允许文本节点大于 10MB)。
(更新结束)
这是 Saxon 6.5.5 的 XSLT 转换。
文件:
ln2xpath.xsl
<?xml version="1.0"?>
<xsl:transform version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:saxon="http://icl.com/saxon"
extension-element-prefixes="saxon"
>
<xsl:output method="text"/>
<xsl:param name="pathname"/>
<xsl:param name="lines"/><!-- list of line numbers -->
<xsl:param name="delim" select="'	'"/>
<xsl:param name="newline" select="' '"/>
<xsl:template match="/">
<xsl:variable name="root" select="/"/>
<xsl:value-of select="concat('# File: ',$pathname,$newline)"/>
<xsl:for-each select="saxon:tokenize($lines,' ')">
<xsl:value-of select="concat('# Line: ',.,$newline)"/>
<xsl:for-each select="$root//*[saxon:line-number() = current()]">
<xsl:for-each select="ancestor-or-self::*">
<xsl:value-of select="concat('/',name(),'['
,1 + count(preceding-sibling::*[name() = name(current())])
,']')"/>
</xsl:for-each>
<xsl:value-of select="$newline"/>
<xsl:for-each select="attribute::*">
<xsl:value-of select="concat(
'@',name(),$delim,normalize-space(),$newline
)"/>
</xsl:for-each>
<xsl:value-of select="$newline"/>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
</xsl:transform>
地点:
xsl:for-each
迭代传递的行号
作为参数使用
saxon:tokenize()
扩展功能;
它还会更改 当前节点 所以 $root
已保存以供稍后使用xsl:for-each
处理元素节点,其
saxon:line-number()
与 $lines
中的current
xsl:for-each
从 上的元素构建 XPath 表达式
ancestor-or-self
轴xsl:for-each
列出属性,每行一个,值在
标准化形式示例:
ln2xpath() {
# Arguments:
# $1 pathname of file output by `xmlstarlet format file.xml`
# $2 list of line numbers (min. 1)
java -jar saxon655.jar -l "${1}" ln2xpath.xsl pathname="${1}" lines="${2:?line numbers?}"
}
xmlstarlet format /usr/share/*/xslt/docbook/common/db-common.xsl |
ln2xpath /dev/stdin '17 53 132 54321'
-l
选项启用行编号${2:?…}
扩展第二个位置
参数,或者如果丢失会导致 shell 发出嗡嗡声并失败输出:
# File: /dev/stdin
# Line: 17
/xsl:stylesheet[1]
@exclude-result-prefixes db str msg
@version 1.0
# Line: 53
/xsl:stylesheet[1]/xsl:key[3]
@name db.biblio.label.key
@match biblioentry[@id and @xreflabel] | bibliomixed[@id and @xreflabel] | db:biblioentry[@xml:id and @xreflabel] | db:bibliomixed[@xml:id and @xreflabel]
@use string(@xreflabel)
# Line: 132
/xsl:stylesheet[1]/xsl:template[4]/xsl:choose[1]/xsl:when[2]/xsl:variable[1]
@name prev
@select $node/preceding::*[name(.) = name($node)][1]
# Line: 54321
这是等效的
xmlstarlet
版本(最多 65535 行输入)。
# shellcheck shell=sh disable=SC2016
ln2xpath() {
xmlstarlet select --text -t \
--var lines -o "${2:?wot, no line numbers?}" -b \
--var delim -o "$(printf '\t')" -b \
--var root='/' \
-o '# File: ' -f -n \
-m 'str:tokenize($lines)' \
-o '# Line: ' -v '.' -n \
-m '$root//*[saxon:line-number() = current()]' \
-m 'ancestor-or-self::*' \
--var pos='1 + count(preceding-sibling::*[name() = name(current())])' \
-v 'concat("/",name(),"[",$pos,"]")' \
-b \
-n \
-m 'attribute::*' \
-v 'concat("@",name(),$delim,normalize-space())' \
-n \
-b \
-n \
"${1}"
}