如何在Shell中执行XPath一行命令?

231

有没有适用于Ubuntu和/或CentOS的软件包,其中包含一个命令行工具,可以执行XPath one-liner,例如foo //element@attribute filename.xmlfoo //element@attribute < filename.xml 并逐行返回结果?

我正在寻找一些东西,它可以让我只需通过apt-get install fooyum install foo安装,然后直接使用,无需任何包装器或其他适配器。

以下是一些相似的示例:

Nokogiri。如果我编写此包装器,我可以按上述方式调用包装器:

#!/usr/bin/ruby

require 'nokogiri'

Nokogiri::XML(STDIN).xpath(ARGV[0]).each do |row|
  puts row
end

XML::XPath。将与此包装器一起使用:

#!/usr/bin/perl

use strict;
use warnings;
use XML::XPath;

my $root = XML::XPath->new(ioref => 'STDIN');
for my $node ($root->find($ARGV[0])->get_nodelist) {
  print($node->getData, "\n");
}

xpath函数在XML::XPath中返回太多的噪音,包括-- NODE --attribute = "value"

xml_grep函数在XML::Twig中不能处理不返回元素的表达式,因此无法仅通过进一步处理提取属性值。

编辑:

echo cat //element/@attribute | xmllint --shell filename.xml返回类似于xpath的噪音。

xmllint --xpath //element/@attribute filename.xml返回attribute = "value"

xmllint --xpath 'string(//element/@attribute)' filename.xml可以返回我想要的内容,但仅适用于第一个匹配项。

另一个几乎满足问题要求的解决方案是使用XSLT来评估任意XPath表达式(需要XSLT处理器中的dyn:evaluate支持):

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
    xmlns:dyn="http://exslt.org/dynamic" extension-element-prefixes="dyn">
  <xsl:output omit-xml-declaration="yes" indent="no" method="text"/>
  <xsl:template match="/">
    <xsl:for-each select="dyn:evaluate($pattern)">
      <xsl:value-of select="dyn:evaluate($value)"/>
      <xsl:value-of select="'&#10;'"/>
    </xsl:for-each> 
  </xsl:template>
</xsl:stylesheet>

使用 xsltproc --stringparam pattern //element/@attribute --stringparam value . arbitrary-xpath.xslt filename.xml 命令运行。


对于这个好问题以及关于找到一种简单可靠的方法在每个新行上打印多个结果的头脑风暴,我给予加1。 - Gilles Quénot
1
请注意,xpath产生的“噪音”在标准错误输出(STDERR)而不是标准输出(STDOUT)。 - miken32
@miken32 不,我只想要输出的值。https://hastebin.com/ekarexumeg.bash - clacke
18个回答

333
你应该尝试这些工具:
  • xidel (xidel):xpath3
  • xmlstarlet (xmlstarlet page):可编辑、选择、转换……默认未安装,xpath1
  • xmllint (man xmllint):通常与libxml2-utils一起默认安装,xpath1(检查我的包装器以在非常旧的版本和换行符分隔的输出(v < 2.9.9)上使用--xpath开关。可以使用--shell开关作为交互式 shell。
  • xpath:通过perl模块XML::Xpath安装,xpath1
  • xml_grep:通过perl模块XML::Twig安装,xpath1(有限的xpath使用)
  • saxon-lint (saxon-lint):我的项目,是@Michael Kay的Saxon-HE Java库的包装器,xpath3:使用SaxonHE 9.6XPath 3.x(具有向后兼容性)

示例:

xmllint --xpath '//element/@attribute' file.xml
xmlstarlet sel -t -v "//element/@attribute" file.xml
xpath -q -e '//element/@attribute' file.xml
xidel -se '//element/@attribute' file.xml
saxon-lint --xpath '//element/@attribute' file.xml

8
太好了!xmlstarlet sel -T -t -m '//element/@attribute' -v '.' -n filename.xml正是我想要的! - clacke
2
注意:xmlstarlet曾被传言已经被放弃,但现在又重新开始积极开发了。 - clacke
6
注意:一些旧版本的xmllint不支持命令行参数--xpath,但大多数似乎支持--shell。虽然输出不太干净,但在紧急情况下仍然有用。 - kevinarpe
在我的Linux Mint机器上(一个Ubuntu/Debian的衍生版本),xmllint不是与libxml2一起提供的,而是与libxml2-utils一起提供的。 - toon81
如果您希望在具有命名空间的文档上使用xmllint,请同时查看https://dev59.com/I1gR5IYBdhLWcg3wnOVw#41115011。 - Hubbitus
显示剩余4条评论

29
您也可以尝试使用我的Xidel。它不在存储库的软件包中,但您可以从网页上下载它(它没有依赖项)。
它有适用于此任务的简单语法:
xidel filename.xml -e '//element/@attribute' 

它是少数支持XPath 2的工具之一。


5
Xidel 看起来很酷,但你应该提一下你也是这个推荐工具的作者。 - FrustratedWithFormsDesigner
1
Saxon和saxon-lint使用xpath3 ;) - Gilles Quénot
Xidel(0..8.win32.zip)在Virustotal上显示为带有恶意软件。因此,使用时请自担风险。https://www.virustotal.com/#/file/96854c2be1e3755f56fabb8f00d1fe567108461b9fab139039219a1b7c17e382/detection - JGFMK
很好 - 我打算将 xidel 添加到我的个人工具箱中。 - maoizm
不错!我必须运行一个递归搜索,查找与给定xpath查询匹配的节点的XML文件。使用xidel和find命令进行查找,如下所示:find . -name "*.xml" -printf '%p : ' -exec xidel {} -s -e 'expr' \; - Vasan
@Vasan,对于大量的XML文件,为每个XML文件运行“xidel”非常低效!使用EXPath File Modulexidel可以更快地完成这项工作:xidel -se 'file:list(.,true(),"*.xml") ! concat(.," : ",doc(.)/{expr})' - Reino

19

很有可能已经安装在系统上的一个软件包是python-lxml。如果是这样的话,可以不需要安装任何额外的软件包就能实现以下操作:

python -c "from lxml.etree import parse; from sys import stdin; print('\n'.join(parse(stdin).xpath('//element/@attribute')))"

1
如何传递文件名? - Ramakrishnan Kannan
6
这适用于stdin。这样可以避免在已经相当冗长的一行代码中包含open()close()。要解析文件,只需运行python -c "from lxml.etree import parse; from sys import stdin; print '\n'.join(parse(stdin).xpath('//element/@attribute'))" < my_file.xml,让你的shell处理文件查找、打开和关闭。 - clacke

11

我在查询maven pom.xml文件的过程中遇到了这个问题。然而,我有以下限制:

  • 必须跨平台运行。
  • 必须在所有主要Linux发行版上存在,无需安装任何额外的模块。
  • 必须处理复杂的xml文件,如maven pom.xml文件。
  • 语法简单。

我尝试了很多方法都没有成功:

  • Python的lxml.etree不是标准Python发行版的一部分。
  • xml.etree是标准Python发行版的一部分,但无法很好地处理复杂的maven pom.xml文件,我还没有深入挖掘。
  • Python的xml.etree因未知原因无法处理maven pom.xml文件。
  • xmllint也不起作用,在Ubuntu 12.04上经常出现内核转储“xmllint:使用libxml版本20708”。

我找到的解决方案稳定、简短、可在许多平台上工作且成熟,那就是Ruby内置的rexml库:

ruby -r rexml/document -e 'include REXML; 
     puts XPath.first(Document.new($stdin), "/project/version/text()")' < pom.xml

我发现这个问题的灵感来自以下文章:


1
这甚至比问题的标准还要严格,所以它绝对适合作为一个答案。我相信许多遇到类似情况的人会受益于你的研究。我将保留“xmlstarlet”作为被采纳的答案,因为它符合我的更广泛的标准并且非常好用。但我以后也可能会不时使用你的解决方案。 - clacke
2
我想补充一点,为了避免在结果周围加引号,在Ruby命令中使用puts而不是p - tooomg

10

Saxon不仅支持XPath 2.0,还支持XQuery 1.0和(商业版本中的)3.0。它不作为Linux软件包提供,而是以jar文件形式提供。语法(您可以轻松地将其包装在简单脚本中)为

java net.sf.saxon.Query -s:source.xml -qs://element/attribute

2020年更新

Saxon 10.0 包含 Gizmo 工具,可以交互式地使用或从命令行批处理中使用。例如:

java net.sf.saxon.Gizmo -s:source.xml
/>show //element/@attribute
/>quit

SaxonB在Ubuntu中,包名为libsaxonb-java,但如果我运行saxonb-xquery -qs://element/@attribute -s:filename.xml,我会得到SENR0001:无法序列化独立的属性节点,这与xml_grep等工具存在相同的问题。 - clacke
3
如果想要查看此查询选择的属性节点的全部细节,请在命令行上使用-wrap选项。如果只需要该属性的字符串值,请在查询末尾添加/string()。 - Michael Kay
谢谢。添加 /string() 可以更接近目标,但它会输出一个 XML 头,并将所有结果放在一行上,所以还是不行。 - clacke
2
如果您不想要XML头,请添加选项!method=text。 - Michael Kay
要使用命名空间,请将其添加到 -qs 中,例如:'-qs:declare namespace mets="http://www.loc.gov/METS/";/mets:mets/mets:dmdSec' - igo
你可能需要指定classpath,像这样:java -classpath /usr/share/java/saxonb.jar net.sf.saxon.Query -s:source.xml -qs://element/@attribute - Thomas W

6

clacke的回答很好,但我认为只适用于源代码是格式良好的XML,而不是普通HTML。

因此,要在普通Web内容(即不一定是格式良好的XML的HTML文档)中执行相同的操作:

echo "<p>foo<div>bar</div><p>baz" | python -c "from sys import stdin; \
from lxml import html; \
print '\n'.join(html.tostring(node) for node in html.parse(stdin).xpath('//p'))"

相反,应该使用 html5lib(以确保您获得与 Web 浏览器相同的解析行为,因为像浏览器解析器一样,html5lib 符合 HTML 规范中的解析要求)。

echo "<p>foo<div>bar</div><p>baz" | python -c "from sys import stdin; \
import html5lib; from lxml import html; \
doc = html5lib.parse(stdin, treebuilder='lxml', namespaceHTMLElements=False); \
print '\n'.join(html.tostring(node) for node in doc.xpath('//p'))

是的,我在问题中犯了自己的假设,即XPath意味着XML。这个答案是其他答案的很好补充,感谢让我知道html5lib! - clacke

5

您可能还对xsh感兴趣。它具有交互模式,您可以随意处理文档:

open 1.xml ;
ls //element/@id ;
for //p[@class="first"] echo text() ;

2
@clacke:它不是,但可以通过cpan XML::XSH2从CPAN安装。 - choroba
@choroba,我在OS X上尝试过了,但是安装失败了,出现了某种makefile错误。 - cnst
@cnst: 你是否已经安装了XML::LibXML? - choroba
@choroba,我不知道;但我的观点是,cpan XML::XSH2无法安装任何东西。 - cnst
@cnst:好的,它也应该告诉你为什么。我只是试图找出原因。 - choroba
显示剩余2条评论

4
与Mike和clacke的回答类似,这里提供一个Python一行代码(使用Python >= 2.5),用于从pom.xml文件获取构建版本。此方法绕过了pom.xml文件通常没有dtd或默认命名空间,因此在libxml中不呈现为格式良好的事实。
python -c "import xml.etree.ElementTree as ET; \
  print(ET.parse(open('pom.xml')).getroot().find('\
  {http://maven.apache.org/POM/4.0.0}version').text)"

已在Mac和Linux上测试,无需安装任何额外的软件包。


2
我今天使用了这个!我们的构建服务器上没有 lxmlxmllint,甚至没有 Ruby。按照 我的答案 的格式,我在 bash 中编写了以下代码:python3 -c "from xml.etree.ElementTree import parse; from sys import stdin; print(parse(stdin).find('.//element[subelement=\"value\"]/othersubelement').text)" <<< "$variable_containing_xml"。似乎不需要使用 .getroot() - clacke

3
一个对Python的lxml模块进行最小封装的包装器,可以按名称打印所有匹配的节点(在任何级别),例如mysubnode或XPath子集,例如//intermediarynode/subnode。如果表达式评估为文本,则将打印文本;如果评估为元素,则将整个原始元素呈现为文本。它还尝试以一种允许使用本地标签名而无需添加前缀的方式处理XML命名空间。通过-x标志启用扩展XPath模式时,需要使用p:前缀引用默认命名空间,例如//p:tagname/p:subtag
#!/usr/bin/env python3
import argparse
import os
import sys

from lxml import etree

DEFAULT_NAMESPACE_KEY = 'p'

def print_element(elem):
    if isinstance(elem, str):
        print(elem)
    elif isinstance(elem, bytes):
        print(elem.decode('utf-8'))
    else:
        print(elem.text and elem.text.strip() or etree.tostring(elem, encoding='unicode', pretty_print=True))


if __name__ == '__main__':

    parser = argparse.ArgumentParser(description='XPATH lxml wrapper',
                                     usage="""
    Print all nodes by name in XML file:                                     
    \t{0} myfile.xml somename

    Print all nodes by XPath selector (findall: reduced subset):                                     
    \t{0} myfile.xml //itermediarynode/childnode

    Print attribute values by XPath selector 'p' maps to default namespace (xpath 1.0: extended subset):                                     
    \t{0} myfile.xml //p:itermediarynode/p:childnode/@src -x
                          
     """.format(os.path.basename(sys.argv[0])))
    parser.add_argument('xpath_file',
                        help='XPath file path')
    parser.add_argument('xpath_expression',
                        help='tag name or xpath expression')
    parser.add_argument('--force_xpath', '-x',
                        action='store_true',
                        default=False,
                        help='Use lxml.xpath (rather than findall)'
    )

    args = parser.parse_args(sys.argv[1:])
    xpath_expression = args.xpath_expression

    tree = etree.parse(args.xpath_file)

    ns = tree.getroot().nsmap

    if args.force_xpath:
        if ns.keys() and None in ns:
            ns[DEFAULT_NAMESPACE_KEY] = ns.pop(None)
        for node in tree.xpath(xpath_expression, namespaces=ns):
            print_element(node)

    elif xpath_expression.isalpha():
        for node in tree.xpath(f"//*[local-name() = '{xpath_expression}']"):
            print_element(node)
    else:
        for el in tree.findall(xpath_expression, namespaces=ns):
            print_element(el)



它使用了一个快速的C语言编写的XML解析器——lxml,该解析器不包含在标准的Python库中。可以通过pip install lxml来安装它。在Linux/OSX上可能需要在前面加上sudo
用法:
python3 xmlcat.py file.xml "//mynode"
lxml也可以接受URL作为输入:
python3 xmlcat.py http://example.com/file.xml "//mynode" 

提取封闭节点下的url属性,即<enclosure url="http:...""..>-x强制使用扩展的XPath 1.0子集):
python3 xmlcat.py xmlcat.py file.xml "//enclosure/@url" -x

在Google Chrome中使用XPath

作为一个无关的附注:如果你碰巧想对网页的标记运行XPath表达式,你可以直接从Chrome开发工具中进行操作:右键点击Chrome中的页面>选择检查,然后在开发工具控制台中粘贴你的XPath表达式,如$x("//spam/eggs")

示例:获取此页面上的所有作者:

$x("//*[@class='user-details']/a/text()")

这不是一行代码,而且在你之前的两个答案中已经提到了lxml。这些答案分别是这里这里,而且都是几年前的回答。 - clacke

2
以下是提取嵌套元素elem1、elem2数据并将其转换为一行文本的xmlstarlet用例(同时展示如何处理命名空间):

这是一个xmlstarlet用例,可以从此类型的XML中提取嵌套元素elem1、elem2的数据并将其转换为一行文本(同时展示如何处理命名空间):

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<mydoctype xmlns="http://xml-namespace-uri" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xml-namespace-uri http://xsd-uri" format="20171221A" date="2018-05-15">

  <elem1 time="0.586" length="10.586">
      <elem2 value="cue-in" type="outro" />
  </elem1>

</mydoctype>

输出结果将会是:
0.586 10.586 cue-in outro

在这个片段中,-m匹配嵌套的elem2,-v输出属性值(包括表达式和相对寻址),-o为文本添加字面量,-n添加换行符。
xml sel -N ns="http://xml-namespace-uri" -t -m '//ns:elem1/ns:elem2' \
 -v ../@time -o " " -v '../@time + ../@length' -o " " -v @value -o " " -v @type -n file.xml

如果需要从elem1获取更多属性,可以像这样操作(同时展示concat()函数):
xml sel -N ns="http://xml-namespace-uri" -t -m '//ns:elem1/ns:elem2/..' \
 -v 'concat(@time, " ", @time + @length, " ", ns:elem2/@value, " ", ns:elem2/@type)' -n file.xml

请注意(我认为是不必要的)与命名空间(通过-N声明的ns)相关的复杂性,这让我几乎放弃使用xpath和xmlstarlet,并编写了一个快速的特定处理程序。

xmlstarlet非常好用,但是已经有一个被接受和主要排名的答案提到了它。关于如何处理命名空间的信息可能只有作为评论才会相关,如果有的话。任何遇到命名空间和xmlstarlet问题的人都可以在文档中找到一篇优秀的讨论 - clacke
2
当然,@clacke,xmlstarlet已经被提到过多次,但也被认为难以掌握和文档不全。我猜测了一个小时如何从嵌套元素中获取信息。我希望我有那个例子,这就是我在这里发布它的原因,以避免其他人浪费时间(而且这个例子太长了,无法作为评论)。 - diemo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接