如何在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个回答

2
值得一提的是,nokogiri本身附带了一个命令行工具,应该使用gem install nokogiri进行安装。
您可能会发现这篇博客文章有用。

2

我的Python脚本xgrep.py可以完美实现这个功能。如果想要在文件filename.xml ...中搜索所有元素element的属性attribute,你需要按照以下方式运行它:

命令如下:

xgrep.py "//element/@attribute" filename.xml ...

有许多开关可用于控制输出,例如-c用于计数匹配项,-i用于缩进匹配部分,-l仅输出文件名。

该脚本不作为Debian或Ubuntu软件包提供,但是它的所有依赖项都可以获得。


而且你正在使用Sourcehut进行托管!太棒了! - clacke

2
除了XML::XSHXML::XSH2之外,还有一些类似于grep的实用工具,例如App::xml_grep2XML::Twig(它包括xml_grep而不是xml_grep2)。当处理大量或多个XML文件时,这些工具可以非常有用,用于快速的单行命令或Makefile目标。特别是在使用perl脚本方法进行处理时,XML::Twig非常好用,比起$SHELLxmllint xstlproc提供更多的处理功能。
应用程序名称中的编号方案表明,“2”版本是基本相同工具的更新/后续版本,可能需要其他模块(或perl本身)的较新版本。

xml_grep2 -t //element@attribute filename.xml 运行正常,符合我的预期(xml_grep --root //element@attribute --text_only filename.xml 仍然不行,返回“无法识别的表达式”错误)。太好了! - clacke
那么xml_grep --pretty_print --root '//element[@attribute]' --text_only filename.xml呢?不确定那里发生了什么或者XPath在这种情况下如何处理[],但是用方括号将@attribute包围起来对于xml_grepxml_grep2有效。 - G. Cito
我的意思是 //element/@attribute,而不是 //element@attribute。显然无法编辑它,但我将其保留在那里,而不是删除+替换,以免混淆本讨论的历史记录。 - clacke
//element[@attribute] 选择具有属性 attribute 的类型为 element 的元素。我不需要元素,只需要属性。<element attribute='foo'/> 应该给我 foo,而不是完整的 <element attribute='foo'/> - clacke
在这种情况下,使用--text_only参数会返回空字符串,例如对于没有文本节点的元素<element attribute='foo'/> - clacke
小修正:“Xml”而不是“xml”:sudo cpan App::Xml_grep2 - JJoao

2

安装BaseX数据库,然后使用它的“独立命令行模式”,如下所示:

basex -i - //element@attribute < filename.xml

或者

basex -i filename.xml //element@attribute

查询语言实际上是XQuery(3.0),而不是XPath,但由于XQuery是XPath的超集,因此您可以使用XPath查询而不会注意到。


1
我对Python的HTML XPath查询一行代码不满意,因此我自己编写了一个。假设您已安装python-lxml软件包或运行了pip install --user lxml
function htmlxpath() { python -c 'for x in __import__("lxml.html").html.fromstring(__import__("sys").stdin.read()).xpath(__import__("sys").argv[1]): print(x)' $1 }

一旦你拥有它,你可以像这个例子一样使用它:

> curl -s https://slashdot.org | htmlxpath '//title/text()'
Slashdot: News for nerds, stuff that matters

1

由于这个项目显然是比较新的,请查看https://github.com/jeffbr13/xq,它似乎是lxml的封装器,但这就是你真正需要的(并且在其他答案中也发布了使用lxml的即席解决方案)。


0

抱歉再次加入争论。我尝试了这个帖子中的所有工具,但发现它们都不符合我的需求,所以我写了自己的程序。你可以在这里找到它:https://github.com/charmparticle/xpe

它已经上传到pypi,因此您可以像这样使用pip3轻松安装:

sudo pip3 install xpe

安装完成后,您可以使用它来针对各种输入运行xpath表达式,具有与在selenium或javascript中使用xpaths相同的灵活性水平。是的,您可以使用此工具针对HTML运行xpaths。


0
一个可以在顶部存在命名空间声明时正常工作的解决方案:
大多数答案中提出的命令如果xml在顶部声明了命名空间,则不能直接使用。考虑以下内容:
输入的xml:
<elem1 xmlns="urn:x" xmlns:prefix="urn:y">
    <elem2 attr1="false" attr2="value2">
        elem2 value
    </elem2>
    <elem2 attr1="true" attr2="value2.1">
        elem2.1 value
    </elem2>    
    <prefix:elem3>
        elem3 value
    </prefix:elem3>        
</elem1>

无法工作:

xmlstarlet sel -t -v "/elem1" input.xml
# nothing printed
xmllint -xpath "/elem1" input.xml
# XPath set is empty

解决方案:

# Requires >=java11 to run like below (but the code requires >=java17 for case syntax to be recognized)

# Prints the whole document
java ExtractXpath.java "/" example-inputs/input.xml

# Prints the contents and self of "elem1"
java ExtractXpath.java "/elem1" input.xml

# Prints the contents and self of "elem2" whose attr2 value is: 'value2'
java ExtractXpath.java "//elem2[@attr2='value2']" input.xml

# Prints the value of the attribute 'attr2': "value2", "value2.1"
java ExtractXpath.java "/elem1/elem2/@attr2" input.xml

# Prints the text inside elem3: "elem3 value"
java ExtractXpath.java "/elem1/elem3/text()" input.xml

# Prints the name of the matched element: "prefix:elem3"
java ExtractXpath.java "name(/elem1/elem3)" input.xml
# Same as above: "prefix:elem3"
java ExtractXpath.java "name(*/elem3)" input.xml

# Prints the count of the matched elements: 2.0
java ExtractXpath.java "count(/elem2)" input.xml


# known issue: while "//elem2" works. "//elem3" does not (it works only with: '*/elem3' )


ExtractXpath.java:


import java.io.File;
import java.io.FileInputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;

import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathEvaluationResult;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class ExtractXpath {

    public static void main(String[] args) throws Exception {
        assertThat(args.length==2, "Wrong number of args");
        String xpath = args[0];
        File file = new File(args[1]);
             
        assertThat(file.isFile(), file.getAbsolutePath()+" is not a file.");
        FileInputStream fileIS = new FileInputStream(file);
        DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = builderFactory.newDocumentBuilder();
        Document xmlDocument = builder.parse(fileIS);
        XPath xPath = XPathFactory.newInstance().newXPath();
        String expression = xpath;
        XPathExpression xpathExpression =  xPath.compile(expression);
        
        XPathEvaluationResult xpathEvalResult =  xpathExpression.evaluateExpression(xmlDocument);
        System.out.println(applyXpathExpression(xmlDocument, xpathExpression, xpathEvalResult.type().name()));
    }

    private static String applyXpathExpression(Document xmlDocument, XPathExpression expr, String xpathTypeName) throws TransformerConfigurationException, TransformerException, XPathExpressionException {

        // see: https://www.w3.org/TR/1999/REC-xpath-19991116/#corelib
        List<String> retVal = new ArrayList();
        if(xpathTypeName.equals(XPathConstants.NODESET.getLocalPart())){ //e.g. xpath: /elem1/*
            NodeList nodeList = (NodeList)expr.evaluate(xmlDocument, XPathConstants.NODESET);
            for (int i = 0; i < nodeList.getLength(); i++) {
                retVal.add(convertNodeToString(nodeList.item(i)));
            }
        }else if(xpathTypeName.equals(XPathConstants.STRING.getLocalPart())){ //e.g. xpath: name(/elem1/*)
            retVal.add((String)expr.evaluate(xmlDocument, XPathConstants.STRING));
        }else if(xpathTypeName.equals(XPathConstants.NUMBER.getLocalPart())){ //e.g. xpath: count(/elem1/*)
            retVal.add(((Number)expr.evaluate(xmlDocument, XPathConstants.NUMBER)).toString());
        }else if(xpathTypeName.equals(XPathConstants.BOOLEAN.getLocalPart())){ //e.g. xpath: contains(elem1, 'sth')
            retVal.add(((Boolean)expr.evaluate(xmlDocument, XPathConstants.BOOLEAN)).toString());
        }else if(xpathTypeName.equals(XPathConstants.NODE.getLocalPart())){ //e.g. xpath: fixme: find one
            System.err.println("WARNING found xpathTypeName=NODE");
            retVal.add(convertNodeToString((Node)expr.evaluate(xmlDocument, XPathConstants.NODE)));
        }else{
            throw new RuntimeException("Unexpected xpath type name: "+xpathTypeName+". This should normally not happen");
        }
        return retVal.stream().map(str->"==MATCH_START==\n"+str+"\n==MATCH_END==").collect(Collectors.joining ("\n"));
        
    }
    
    private static String convertNodeToString(Node node) throws TransformerConfigurationException, TransformerException {
            short nType = node.getNodeType();
        switch (nType) {
            case Node.ATTRIBUTE_NODE , Node.TEXT_NODE -> {
                return node.getNodeValue();
            }
            case Node.ELEMENT_NODE, Node.DOCUMENT_NODE -> {
                StringWriter writer = new StringWriter();
                Transformer trans = TransformerFactory.newInstance().newTransformer();
                trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
                trans.setOutputProperty(OutputKeys.INDENT, "yes");
                trans.transform(new DOMSource(node), new StreamResult(writer));
                return writer.toString();
            }
            default -> {
                System.err.println("WARNING: FIXME: Node type:"+nType+" could possibly be handled in a better way.");
                return node.getNodeValue();
            }
                
        }
    }

    
    private static void assertThat(boolean b, String msg) {
        if(!b){
            System.err.println(msg+"\n\nUSAGE: program xpath xmlFile");
            System.exit(-1);
        }
    }
}

@SuppressWarnings("unchecked")
class NamespaceResolver implements NamespaceContext {
    //Store the source document to search the namespaces
    private final Document sourceDocument;
    public NamespaceResolver(Document document) {
        sourceDocument = document;
    }

    //The lookup for the namespace uris is delegated to the stored document.
    @Override
    public String getNamespaceURI(String prefix) {
        if (prefix.equals(XMLConstants.DEFAULT_NS_PREFIX)) {
            return sourceDocument.lookupNamespaceURI(null);
        } else {
            return sourceDocument.lookupNamespaceURI(prefix);
        }
    }

    @Override
    public String getPrefix(String namespaceURI) {
        return sourceDocument.lookupPrefix(namespaceURI);
    }

    @SuppressWarnings("rawtypes")
    @Override
    public Iterator getPrefixes(String namespaceURI) {
        return null;
    }
}

为了简单起见:

xpath-extract 命令:

#!/bin/bash
java ExtractXpath.java "$1" "$2"


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