如何在 Bash 中解析 XML?

How to parse XML in Bash?

提问人: 提问时间:5/21/2009 最后编辑:Zombo 更新时间:12/7/2022 访问量:364495

问:

理想情况下,我希望能够做的是:

cat xhtmlfile.xhtml |
getElementViaXPath --path='/html/head/title' |
sed -e 's%(^<title>|</title>$)%%g' > titleOfXHTMLPage.txt
XML bash xhtml shell xpath

评论

2赞 Ciro Santilli OurBigBook.com 10/7/2015
unix.stackexchange.com/questions/83385/......||superuser.com/questions/369996/......
0赞 jpseng 12/7/2022
echo '<html><head><title>Example</title></body></html>' | yq -p xml '.html.head.title'输出。参见:yq一些例子Example

答:

3赞 alamar 5/21/2009 #1

好吧,您可以使用 xpath 实用程序。我猜perl的XML::Xpath包含它。

4赞 mirod 5/21/2009 #2

我不知道有任何纯shell XML解析工具。因此,您很可能需要用其他语言编写的工具。

我的 XML::Twig Perl 模块带有这样一个工具: ,您可能会在其中编写您想要的内容(该选项将结果作为文本而不是 xml)xml_grepxml_grep -t '/html/head/title' xhtmlfile.xhtml > titleOfXHTMLPage.txt-t

76赞 Nat 5/22/2009 #3

可以从 shell 脚本调用的命令行工具包括:

  • 4xpath - Python 的 4Suite 包的命令行包装器

  • XMLStarlet

  • xpath - Perl XPath 库的命令行包装器

    sudo apt-get install libxml-xpath-perl
    
  • Xidel - 适用于 URL 和文件。也适用于 JSON

我还使用带有小 XSL 转换脚本的 xmllint 和 xsltproc 从命令行或 shell 脚本中执行 XML 处理。

评论

2赞 Opher 4/15/2011
我在哪里可以下载“xpath”或“4xpath”?
4赞 David 11/22/2011
是的,第二次投票/请求 - 在哪里下载这些工具,或者你的意思是必须手动编写包装器?除非必要,否则我宁愿不浪费时间这样做。
4赞 Andrew Wagner 11/23/2012
sudo apt-get 安装 libxml-xpath-perl
0赞 phyatt 9/24/2021
xpath太棒了!用法很简单,然后添加 a 以仅显示输出,以便您可以将其管道传输到其他地方或保存到变量。xpath -e 'xpath/expression/here' $filename-q
0赞 sean 1/10/2022
4xpath 的链接断开。
8赞 simon04 11/7/2009 #4

查看 http://www.ofb.net/~egnor/xml2/XML2,它将 XML 转换为面向行的格式。

评论

1赞 Joshua Goldberg 11/7/2020
非常有用的工具。链接已损坏(请参阅 web.archive.org/web/20160312110413/https://dan.egnor.name/xml2 ),但 github 上有一个有效的冻结克隆:github.com/clone/xml2
72赞 Yuzem 4/9/2010 #5

你可以很容易地只使用 bash 来做到这一点。 您只需添加此函数:

rdom () { local IFS=\> ; read -d \< E C ;}

现在,您可以像 read 一样使用 rdom,但用于 html 文档。 调用时,rdom 会将元素分配给变量 E,将内容分配给变量 C。

例如,要执行您想执行的操作,请执行以下操作:

while rdom; do
    if [[ $E = title ]]; then
        echo $C
        exit
    fi
done < xhtmlfile.xhtml > titleOfXHTMLPage.txt

评论

0赞 Alex Gray 7/4/2011
你能详细说明一下吗?我敢打赌,你很清楚......这可能是一个很好的答案——如果我能说出你在那里做什么......你能把它分解得更深入一点,可能产生一些样本输出吗?
2赞 maverick 12/6/2013
相信原版——这句单行字是如此的优雅和惊人。
2赞 user311174 1/16/2014
很棒的黑客,但我不得不使用双引号,如 echo “$C” 来防止 shell 扩展和正确解释结束线(取决于 enconding)
16赞 peterh 2/2/2018
使用 grep 和 awk 解析 XML 是不行的。如果 XML 足够简单并且您没有太多时间,这可能是一个可以接受的折衷方案,但它永远不能称为一个好的解决方案。
2赞 user485380 10/24/2010 #6

在对 XML 文件中文件路径的 Linux 和 Windows 格式之间的转换进行了一些研究后,我发现了有趣的教程和解决方案:

184赞 chad 8/14/2011 #7

这真的只是对Yuzem回答的解释,但我不觉得应该对别人做这么多编辑,而且评论不允许格式化,所以......

rdom () { local IFS=\> ; read -d \< E C ;}

我们将其称为“read_dom”而不是“rdom”,将其间隔开一点并使用更长的变量:

read_dom () {
    local IFS=\>
    read -d \< ENTITY CONTENT
}

好的,所以它定义了一个名为 read_dom 的函数。第一行使 IFS(输入字段分隔符)成为此函数的本地部分,并将其更改为 >。这意味着当您读取数据时,它不会自动拆分为空格、制表符或换行符,而是在“>”上拆分。下一行说从 stdin 读取输入,而不是停在换行符处,而是在看到“<”字符(deliminator 标志的 -d)时停止。然后,使用 IFS 对读取的内容进行拆分,并将其分配给变量 ENTITY 和 CONTENT。因此,请采取以下措施:

<tag>value</tag>

获取空字符串的第一个调用(因为“<”是第一个字符)。IFS 将其拆分为“”,因为没有“>”字符。然后,Read 将一个空字符串分配给这两个变量。第二次调用获取字符串“tag>value”。然后,IFS 将其拆分为“标签”和“值”两个字段。Read 然后分配变量,例如: 和 .第三次调用获取字符串“/tag>”。IFS 将其拆分为两个字段“/tag”和“”。Read 然后分配变量,例如: 和 .第四次调用将返回非零状态,因为我们已经到达了文件末尾。read_domENTITY=tagCONTENT=valueENTITY=/tagCONTENT=

现在他的 while 循环清理了一点以匹配上述内容:

while read_dom; do
    if [[ $ENTITY = "title" ]]; then
        echo $CONTENT
        exit
    fi
done < xhtmlfile.xhtml > titleOfXHTMLPage.txt

第一行只是说,“当 read_dom 函数返回零状态时,请执行以下操作。第二行检查我们刚才看到的实体是否为“title”。下一行与标记的内容相呼应。四条线退出。如果它不是标题实体,则循环在第六行重复。我们将“xhtmlfile.xhtml”重定向到标准输入(用于函数),并将标准输出重定向到“titleOfXHTMLPage.txt”(循环中前面的回声)。read_dom

现在给出以下内容(类似于您在 S3 上列出存储桶所获得的结果):input.xml

<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Name>sth-items</Name>
  <IsTruncated>false</IsTruncated>
  <Contents>
    <Key>[email protected]</Key>
    <LastModified>2011-07-25T22:23:04.000Z</LastModified>
    <ETag>&quot;0032a28286680abee71aed5d059c6a09&quot;</ETag>
    <Size>1785</Size>
    <StorageClass>STANDARD</StorageClass>
  </Contents>
</ListBucketResult>

和以下循环:

while read_dom; do
    echo "$ENTITY => $CONTENT"
done < input.xml

您应该获得:

 => 
ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/" => 
Name => sth-items
/Name => 
IsTruncated => false
/IsTruncated => 
Contents => 
Key => [email protected]
/Key => 
LastModified => 2011-07-25T22:23:04.000Z
/LastModified => 
ETag => &quot;0032a28286680abee71aed5d059c6a09&quot;
/ETag => 
Size => 1785
/Size => 
StorageClass => STANDARD
/StorageClass => 
/Contents => 

因此,如果我们编写一个像 Yuzem 这样的循环:while

while read_dom; do
    if [[ $ENTITY = "Key" ]] ; then
        echo $CONTENT
    fi
done < input.xml

我们将获得 S3 存储桶中所有文件的列表。

编辑如果由于某种原因对您不起作用并且您全局设置了它,您应该在函数结束时重置它,例如:local IFS=\>

read_dom () {
    ORIGINAL_IFS=$IFS
    IFS=\>
    read -d \< ENTITY CONTENT
    IFS=$ORIGINAL_IFS
}

否则,您稍后在脚本中所做的任何行拆分都将被搞砸。

编辑 2要拆分属性名称/值对,您可以按如下方式进行扩充:read_dom()

read_dom () {
    local IFS=\>
    read -d \< ENTITY CONTENT
    local ret=$?
    TAG_NAME=${ENTITY%% *}
    ATTRIBUTES=${ENTITY#* }
    return $ret
}

然后编写函数来解析并获取所需的数据,如下所示:

parse_dom () {
    if [[ $TAG_NAME = "foo" ]] ; then
        eval local $ATTRIBUTES
        echo "foo size is: $size"
    elif [[ $TAG_NAME = "bar" ]] ; then
        eval local $ATTRIBUTES
        echo "bar type is: $type"
    fi
}

然后,当您致电时:read_domparse_dom

while read_dom; do
    parse_dom
done

然后给出以下示例标记:

<example>
  <bar size="bar_size" type="metal">bars content</bar>
  <foo size="1789" type="unknown">foos content</foo>
</example>

您应该得到以下输出:

$ cat example.xml | ./bash_xml.sh 
bar type is: metal
foo size is: 1789

编辑 3 另一位用户说他们在 FreeBSD 中遇到了问题,并建议保存退出状态,并在read_dom结束时返回它,例如:

read_dom () {
    local IFS=\>
    read -d \< ENTITY CONTENT
    local RET=$?
    TAG_NAME=${ENTITY%% *}
    ATTRIBUTES=${ENTITY#* }
    return $RET
}

我看不出有什么理由不能这样做

评论

2赞 chad 7/25/2012
如果您将 IFS(输入字段分隔符)设置为全局,您应该在最后将其重置回其原始值,我编辑了答案以获得它。否则,您稍后在脚本中执行的任何其他输入拆分都将被搞砸。我怀疑本地对你不起作用的原因是因为你在兼容模式下使用 bash(比如你的 shbang 是 #!/bin/sh),或者它是 bash 的古老版本。
45赞 Stephen Niedzielski 4/24/2013
仅仅因为你可以编写自己的解析器,并不意味着你应该这样做。
3赞 chad 10/11/2013
@Alastair请参阅 github.com/chad3814/s3scripts,了解我们用于操作 S3 对象的一组 bash 脚本
6赞 William Pursell 11/28/2013
在局部变量中分配 IFS 是脆弱的,不是必需的。Just do: ,这将仅为读取调用设置 IFS。(请注意,我绝不赞成使用解析 xml 的做法,而且我认为这样做充满了危险,应该避免。IFS=\< read ...read
2赞 Stephen Niedzielski 1/19/2023
我很抱歉我的评论是如此粗鲁和消极。我的观点是,一个临时的XML解析器实现不太可能是正确的(也许这个是,但没有一套全面的测试来证明这一点)。在我看来,选择其他答案中提到的能够很好地解决 XML 解析的专用工具之一似乎更实用。
26赞 Grisha 4/24/2012 #8

您可以使用 xpath 实用程序。它与 Perl XML-XPath 包一起安装。

用法:

/usr/bin/xpath [filename] query

XMLStarlet。要在 opensuse 上安装它,请使用:

sudo zypper install xmlstarlet

或在其他平台上尝试。cnf xml

评论

6赞 Bruno von Paris 2/8/2013
使用 xml starlet 绝对是比编写自己的序列化程序更好的选择(如其他答案中所建议的那样)。
1赞 tripleee 7/27/2016
在许多系统上,预装的 不适合用作脚本中的组件。例如,详见 stackoverflow.com/questions/15461737/...xpath
2赞 rubo77 12/24/2016
在 Ubuntu/Debian 上apt-get install xmlstarlet
0赞 Zombo 6/17/2012 #9

如果您需要 XML 属性,这将起作用:

$ cat alfa.xml
<video server="asdf.com" stream="H264_400.mp4" cdn="limelight"/>

$ sed 's.[^ ]*..;s./>..' alfa.xml > alfa.sh

$ . ./alfa.sh

$ echo "$stream"
H264_400.mp4
1赞 michaelmeyer 1/24/2013 #10

Yuzem 的方法可以通过反转函数和变量赋值中 and 符号的顺序来改进,从而:<>rdom

rdom () { local IFS=\> ; read -d \< E C ;}

成为:

rdom () { local IFS=\< ; read -d \> C E ;}

如果不这样做进行分析,则永远不会到达 XML 文件中的最后一个标记。如果您打算在循环结束时输出另一个 XML 文件,这可能会有问题。while

6赞 BeniBela 3/27/2013 #11

另一个命令行工具是我的新 Xidel。它还支持 XPath 2 和 XQuery,这与前面提到的 xpath/xmlstarlet 相反。

标题可以读作:

xidel xhtmlfile.xhtml -e /html/head/title > titleOfXHTMLPage.txt

而且它还有一个很酷的功能,可以将多个变量导出到 bash。例如

eval $(xidel xhtmlfile.xhtml -e 'title := //title, imgcount := count(//img)' --output-format bash )

设置为标题和文件中的图像数,这应该与直接在 bash 中解析它一样灵活。$title$imgcount

5赞 scavenger 1/29/2014 #12

从 chad 的答案开始,这里是解析 UML 的完整工作解决方案,具有注释的 propper 处理,只有 2 个小函数(超过 2 个 bu 你可以将它们全部混合)。我不是说 chad 的那个根本不起作用,但它在格式错误的 XML 文件方面存在太多问题:因此,您必须更棘手地处理注释和放错位置的空格/CR/TAB 等。

这个答案的目的是为任何需要解析 UML 的人提供现成的、开箱即用的 bash 函数,而无需使用 perl、python 或其他任何东西的复杂工具。至于我,我无法为我正在使用的旧生产操作系统安装 cpan 或 perl 模块,而且 python 不可用。

首先,本文中使用的 UML 单词的定义:

<!-- comment... -->
<tag attribute="value">content...</tag>

编辑:更新了函数,句柄为:

  • Websphere xml(xmi 和 xmlns 属性)
  • 必须具有 256 种颜色的兼容终端
  • 24 种灰色阴影
  • 为 IBM AIX bash 3.2.16(1) 添加了兼容性

函数,首先是xml_read_dom,由xml_read递归调用:

xml_read_dom() {
# https://stackoverflow.com/questions/893585/how-to-parse-xml-in-bash
local ENTITY IFS=\>
if $ITSACOMMENT; then
  read -d \< COMMENTS
  COMMENTS="$(rtrim "${COMMENTS}")"
  return 0
else
  read -d \< ENTITY CONTENT
  CR=$?
  [ "x${ENTITY:0:1}x" == "x/x" ] && return 0
  TAG_NAME=${ENTITY%%[[:space:]]*}
  [ "x${TAG_NAME}x" == "x?xmlx" ] && TAG_NAME=xml
  TAG_NAME=${TAG_NAME%%:*}
  ATTRIBUTES=${ENTITY#*[[:space:]]}
  ATTRIBUTES="${ATTRIBUTES//xmi:/}"
  ATTRIBUTES="${ATTRIBUTES//xmlns:/}"
fi

# when comments sticks to !-- :
[ "x${TAG_NAME:0:3}x" == "x!--x" ] && COMMENTS="${TAG_NAME:3} ${ATTRIBUTES}" && ITSACOMMENT=true && return 0

# http://tldp.org/LDP/abs/html/string-manipulation.html
# INFO: oh wait it doesn't work on IBM AIX bash 3.2.16(1):
# [ "x${ATTRIBUTES:(-1):1}x" == "x/x" -o "x${ATTRIBUTES:(-1):1}x" == "x?x" ] && ATTRIBUTES="${ATTRIBUTES:0:(-1)}"
[ "x${ATTRIBUTES:${#ATTRIBUTES} -1:1}x" == "x/x" -o "x${ATTRIBUTES:${#ATTRIBUTES} -1:1}x" == "x?x" ] && ATTRIBUTES="${ATTRIBUTES:0:${#ATTRIBUTES} -1}"
return $CR
}

第二个:

xml_read() {
# https://stackoverflow.com/questions/893585/how-to-parse-xml-in-bash
ITSACOMMENT=false
local MULTIPLE_ATTR LIGHT FORCE_PRINT XAPPLY XCOMMAND XATTRIBUTE GETCONTENT fileXml tag attributes attribute tag2print TAGPRINTED attribute2print XAPPLIED_COLOR PROSTPROCESS USAGE
local TMP LOG LOGG
LIGHT=false
FORCE_PRINT=false
XAPPLY=false
MULTIPLE_ATTR=false
XAPPLIED_COLOR=g
TAGPRINTED=false
GETCONTENT=false
PROSTPROCESS=cat
Debug=${Debug:-false}
TMP=/tmp/xml_read.$RANDOM
USAGE="${C}${FUNCNAME}${c} [-cdlp] [-x command <-a attribute>] <file.xml> [tag | \"any\"] [attributes .. | \"content\"]
${nn[2]}  -c = NOCOLOR${END}
${nn[2]}  -d = Debug${END}
${nn[2]}  -l = LIGHT (no \"attribute=\" printed)${END}
${nn[2]}  -p = FORCE PRINT (when no attributes given)${END}
${nn[2]}  -x = apply a command on an attribute and print the result instead of the former value, in green color${END}
${nn[1]}  (no attribute given will load their values into your shell; use '-p' to print them as well)${END}"

! (($#)) && echo2 "$USAGE" && return 99
(( $# < 2 )) && ERROR nbaram 2 0 && return 99
# getopts:
while getopts :cdlpx:a: _OPT 2>/dev/null
do
{
  case ${_OPT} in
    c) PROSTPROCESS="${DECOLORIZE}" ;;
    d) local Debug=true ;;
    l) LIGHT=true; XAPPLIED_COLOR=END ;;
    p) FORCE_PRINT=true ;;
    x) XAPPLY=true; XCOMMAND="${OPTARG}" ;;
    a) XATTRIBUTE="${OPTARG}" ;;
    *) _NOARGS="${_NOARGS}${_NOARGS+, }-${OPTARG}" ;;
  esac
}
done
shift $((OPTIND - 1))
unset _OPT OPTARG OPTIND
[ "X${_NOARGS}" != "X" ] && ERROR param "${_NOARGS}" 0

fileXml=$1
tag=$2
(( $# > 2 )) && shift 2 && attributes=$*
(( $# > 1 )) && MULTIPLE_ATTR=true

[ -d "${fileXml}" -o ! -s "${fileXml}" ] && ERROR empty "${fileXml}" 0 && return 1
$XAPPLY && $MULTIPLE_ATTR && [ -z "${XATTRIBUTE}" ] && ERROR param "-x command " 0 && return 2
# nb attributes == 1 because $MULTIPLE_ATTR is false
[ "${attributes}" == "content" ] && GETCONTENT=true

while xml_read_dom; do
  # (( CR != 0 )) && break
  (( PIPESTATUS[1] != 0 )) && break

  if $ITSACOMMENT; then
    # oh wait it doesn't work on IBM AIX bash 3.2.16(1):
    # if [ "x${COMMENTS:(-2):2}x" == "x--x" ]; then COMMENTS="${COMMENTS:0:(-2)}" && ITSACOMMENT=false
    # elif [ "x${COMMENTS:(-3):3}x" == "x-->x" ]; then COMMENTS="${COMMENTS:0:(-3)}" && ITSACOMMENT=false
    if [ "x${COMMENTS:${#COMMENTS} - 2:2}x" == "x--x" ]; then COMMENTS="${COMMENTS:0:${#COMMENTS} - 2}" && ITSACOMMENT=false
    elif [ "x${COMMENTS:${#COMMENTS} - 3:3}x" == "x-->x" ]; then COMMENTS="${COMMENTS:0:${#COMMENTS} - 3}" && ITSACOMMENT=false
    fi
    $Debug && echo2 "${N}${COMMENTS}${END}"
  elif test "${TAG_NAME}"; then
    if [ "x${TAG_NAME}x" == "x${tag}x" -o "x${tag}x" == "xanyx" ]; then
      if $GETCONTENT; then
        CONTENT="$(trim "${CONTENT}")"
        test ${CONTENT} && echo "${CONTENT}"
      else
        # eval local $ATTRIBUTES => eval test "\"\$${attribute}\"" will be true for matching attributes
        eval local $ATTRIBUTES
        $Debug && (echo2 "${m}${TAG_NAME}: ${M}$ATTRIBUTES${END}"; test ${CONTENT} && echo2 "${m}CONTENT=${M}$CONTENT${END}")
        if test "${attributes}"; then
          if $MULTIPLE_ATTR; then
            # we don't print "tag: attr=x ..." for a tag passed as argument: it's usefull only for "any" tags so then we print the matching tags found
            ! $LIGHT && [ "x${tag}x" == "xanyx" ] && tag2print="${g6}${TAG_NAME}: "
            for attribute in ${attributes}; do
              ! $LIGHT && attribute2print="${g10}${attribute}${g6}=${g14}"
              if eval test "\"\$${attribute}\""; then
                test "${tag2print}" && ${print} "${tag2print}"
                TAGPRINTED=true; unset tag2print
                if [ "$XAPPLY" == "true" -a "${attribute}" == "${XATTRIBUTE}" ]; then
                  eval ${print} "%s%s\ " "\${attribute2print}" "\${${XAPPLIED_COLOR}}\"\$(\$XCOMMAND \$${attribute})\"\${END}" && eval unset ${attribute}
                else
                  eval ${print} "%s%s\ " "\${attribute2print}" "\"\$${attribute}\"" && eval unset ${attribute}
                fi
              fi
            done
            # this trick prints a CR only if attributes have been printed durint the loop:
            $TAGPRINTED && ${print} "\n" && TAGPRINTED=false
          else
            if eval test "\"\$${attributes}\""; then
              if $XAPPLY; then
                eval echo "\${g}\$(\$XCOMMAND \$${attributes})" && eval unset ${attributes}
              else
                eval echo "\$${attributes}" && eval unset ${attributes}
              fi
            fi
          fi
        else
          echo eval $ATTRIBUTES >>$TMP
        fi
      fi
    fi
  fi
  unset CR TAG_NAME ATTRIBUTES CONTENT COMMENTS
done < "${fileXml}" | ${PROSTPROCESS}
# http://mywiki.wooledge.org/BashFAQ/024
# INFO: I set variables in a "while loop" that's in a pipeline. Why do they disappear? workaround:
if [ -s "$TMP" ]; then
  $FORCE_PRINT && ! $LIGHT && cat $TMP
  # $FORCE_PRINT && $LIGHT && perl -pe 's/[[:space:]].*?=/ /g' $TMP
  $FORCE_PRINT && $LIGHT && sed -r 's/[^\"]*([\"][^\"]*[\"][,]?)[^\"]*/\1 /g' $TMP
  . $TMP
  rm -f $TMP
fi
unset ITSACOMMENT
}

最后,rtrim、trim 和 echo2(到 stderr)函数:

rtrim() {
local var=$@
var="${var%"${var##*[![:space:]]}"}"   # remove trailing whitespace characters
echo -n "$var"
}
trim() {
local var=$@
var="${var#"${var%%[![:space:]]*}"}"   # remove leading whitespace characters
var="${var%"${var##*[![:space:]]}"}"   # remove trailing whitespace characters
echo -n "$var"
}
echo2() { echo -e "$@" 1>&2; }

着色:

哦,首先需要定义一些整齐的着色动态变量,然后导出:

set -a
TERM=xterm-256color
case ${UNAME} in
AIX|SunOS)
  M=$(${print} '\033[1;35m')
  m=$(${print} '\033[0;35m')
  END=$(${print} '\033[0m')
;;
*)
  m=$(tput setaf 5)
  M=$(tput setaf 13)
  # END=$(tput sgr0)          # issue on Linux: it can produces ^[(B instead of ^[[0m, more likely when using screenrc
  END=$(${print} '\033[0m')
;;
esac
# 24 shades of grey:
for i in $(seq 0 23); do eval g$i="$(${print} \"\\033\[38\;5\;$((232 + i))m\")" ; done
# another way of having an array of 5 shades of grey:
declare -a colorNums=(238 240 243 248 254)
for num in 0 1 2 3 4; do nn[$num]=$(${print} "\033[38;5;${colorNums[$num]}m"); NN[$num]=$(${print} "\033[48;5;${colorNums[$num]}m"); done
# piped decolorization:
DECOLORIZE='eval sed "s,${END}\[[0-9;]*[m|K],,g"'

如何加载所有这些东西:

您知道如何创建函数并通过 FPATH (ksh) 或 FPATH (bash) 的仿真加载它们

如果没有,只需在命令行上复制/粘贴所有内容即可。

它是如何工作的:

xml_read [-cdlp] [-x command <-a attribute>] <file.xml> [tag | "any"] [attributes .. | "content"]
  -c = NOCOLOR
  -d = Debug
  -l = LIGHT (no \"attribute=\" printed)
  -p = FORCE PRINT (when no attributes given)
  -x = apply a command on an attribute and print the result instead of the former value, in green color
  (no attribute given will load their values into your shell as $ATTRIBUTE=value; use '-p' to print them as well)

xml_read server.xml title content     # print content between <title></title>
xml_read server.xml Connector port    # print all port values from Connector tags
xml_read server.xml any port          # print all port values from any tags

使用调试模式 (-d) 时,注释和解析的属性将打印到 stderr

评论

0赞 khmarbaise 3/5/2014
我正在尝试使用上述两个函数,它产生以下内容:?./read_xml.sh: line 22: (-1): substring expression < 0
0赞 khmarbaise 3/5/2014
第 22 行:[ "x${ATTRIBUTES:(-1):1}x" == "x?x" ] ...
0赞 scavenger 4/8/2015
对不起 khmarbaise,这些是 bash shell 函数。如果你想把它们改编成shell脚本,你当然要期待一些小的改编!此外,更新的功能会处理您的错误;)
0赞 JPM 8/26/2023
如果这是在像我这样需要它从服务运行的人的 shell 脚本中,那就太好了。
17赞 teknopaul 1/5/2015 #13

这就足够了......

xpath xhtmlfile.xhtml '/html/head/title/text()' > titleOfXHTMLPage.txt

评论

3赞 tres.14159 1/18/2019
在 debian .apt-get install libxml-xpath-perl
2赞 ccpizza 10/25/2017 #14

虽然有相当多的现成控制台实用程序可以做你想做的事,但用通用编程语言(如 Python)编写几行代码可能需要更少的时间,你可以很容易地扩展和适应你的需求。

这是一个使用 lxml 进行解析的 python 脚本——它将文件或 URL 的名称作为第一个参数,将 XPath 表达式作为第二个参数,并打印与给定表达式匹配的字符串/节点。

示例 1

#!/usr/bin/env python
import sys
from lxml import etree

tree = etree.parse(sys.argv[1])
xpath_expression = sys.argv[2]

#  a hack allowing to access the
#  default namespace (if defined) via the 'p:' prefix    
#  E.g. given a default namespaces such as 'xmlns="http://maven.apache.org/POM/4.0.0"'
#  an XPath of '//p:module' will return all the 'module' nodes
ns = tree.getroot().nsmap
if ns.keys() and None in ns:
    ns['p'] = ns.pop(None)
#   end of hack    

for e in tree.xpath(xpath_expression, namespaces=ns):
    if isinstance(e, str):
        print(e)
    else:
        print(e.text and e.text.strip() or etree.tostring(e, pretty_print=True))

lxml可以与 一起安装。在 ubuntu 上,您可以使用 .pip install lxmlsudo apt install python-lxml

用法

python xpath.py myfile.xml "//mynode"

lxml还接受 URL 作为输入:

python xpath.py http://www.feedforall.com/sample.xml "//link"

注意:如果你的XML有一个没有前缀的默认命名空间(例如),那么你必须在表达式中使用前缀(由'hack'提供),例如 从文件中获取模块。如果前缀已映射在 XML 中,则需要修改脚本以使用其他前缀。xmlns=http://abc...p//p:modulepom.xmlp


示例 2

一个一次性脚本,其狭隘目的是从 apache maven 文件中提取模块名称。请注意节点名称 () 如何以默认命名空间为前缀:module{http://maven.apache.org/POM/4.0.0}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modules>
        <module>cherries</module>
        <module>bananas</module>
        <module>pears</module>
    </modules>
</project>

module_extractor.py

from lxml import etree
for _, e in etree.iterparse(open("pom.xml"), tag="{http://maven.apache.org/POM/4.0.0}module"):
    print(e.text)

评论

0赞 E. Moffat 10/31/2018
当您想要避免安装额外的软件包或无法访问时,这真是太棒了。在构建机器上,我可以证明额外的过度或调用是合理的。谢谢!pip installapt-getyum
2赞 Pila 5/10/2020 #15

虽然看起来“从不解析 XML、JSON......在没有适当工具的情况下从 bash “是合理的建议,我不同意。如果这是副业,那么寻找合适的工具是腰部的,然后学习它......Awk 可以在几分钟内完成。我的程序必须处理上述所有数据以及更多种类的数据。见鬼,如果我能在几分钟内解决问题,我不想测试 30 种工具来解析我需要的 5-7-10 种不同格式。我不关心XML,JSON或其他什么!我需要一个单一的解决方案来满足所有这些需求。

举个例子:我的SmartHome程序运行着我们的家。在这样做时,它会以太多我无法控制的不同格式读取大量数据。我从不使用专用的、适当的工具,因为我不想花超过几分钟来读取我需要的数据。通过 FS 和 RS 调整,此 awk 解决方案适用于任何文本格式。但是,当您的主要任务是主要处理该格式的大量数据时,这可能不是正确的答案!

我昨天遇到的从 bash 解析 XML 的问题。以下是我如何针对任何分层数据格式执行此操作。作为奖励 - 我直接将数据分配给 bash 脚本中的变量。

为了使薄片更易于阅读,我将分阶段介绍解决方案。根据OP测试数据,我创建了一个文件:test.xml

在 bash 中解析 XML 并以 90 个字符提取数据:

awk 'BEGIN { FS="<|>"; RS="\n" }; /host|username|password|dbname/ { print $2, $4 }' test.xml

我通常使用更具可读性的版本,因为它在现实生活中更容易修改,因为我经常需要以不同的方式进行测试:

awk 'BEGIN { FS="<|>"; RS="\n" }; { if ($0 ~ /host|username|password|dbname/) print $2,$4}' test.xml

我不在乎格式是如何称呼的。我只寻求最简单的解决方案。在这种特殊情况下,我从数据中可以看出换行符是记录分隔符 (RS),<>分隔字段 (FS)。在我最初的案例中,我对两条记录中的 6 个值进行了复杂的索引,将它们关联起来,查找数据何时存在以及字段(记录)可能存在也可能不存在。花了 4 行 awk 才能完美解决问题。因此,在使用之前,请根据每个需求调整想法!

第二部分只是简单地看一行(RS)中有想要的字符串,如果是这样,则打印出所需的字段(FS)。上面花了我大约 30 秒来复制和适应我以这种方式使用的最后一个命令(时间长 4 倍)。就是这样!以 90 个字符完成。

但是,我总是需要将数据整齐地放入脚本中的变量中。我首先像这样测试结构:

awk 'BEGIN { FS="<|>"; RS="\n" }; { if ($0 ~ /host|username|password|dbname/) print $2"=\""$4"\"" }' test.xml

在某些情况下,我使用 printf 而不是 print。当我看到一切看起来都很好时,我只需完成为变量赋值即可。我知道很多人认为“评估”是“邪恶的”,无需评论:)多年来,Trick 在我的所有四个网络上都能完美运行。但是,如果您不明白为什么这可能是不好的做法,请继续学习!包括 bash 变量赋值和充足的间距,我的解决方案需要 120 个字符才能完成所有操作。

eval $( awk 'BEGIN { FS="<|>"; RS="\n" }; { if ($0 ~ /host|username|password|dbname/) print $2"=\""$4"\"" }' test.xml ); echo "host: $host, username: $username, password: $password dbname: $dbname"

评论

1赞 Charles Duffy 11/11/2020
这种方法存在严重的安全问题。您不希望包含该命令的密码(如果您将注入的引号从双引号更改为单引号,则它们可能会被 )。$(rm -rf ~)eval$(rm -rf ~)'$(rm -rf ~)'
0赞 Charles Duffy 11/11/2020
...因此,如果你想确保安全,你需要同时 (1) 从注入双引号切换到单引号;(2) 将数据中的任何文字单引号替换为类似'"'"'
0赞 Charles Duffy 11/11/2020
此外,不仅仅是.有关后者如何导致错误结果的示例,请尝试 ,然后将 的输出与 的输出进行比较 -- 如果没有引号,则在开始解析之前,your 会被替换为当前目录中的文件列表(这意味着这些文件名本身被评估为代码,为安全问题打开了更多潜在的空间)。eval "$(...)"eval $(...)cmd=$'printf \'%s\\n\' \'first * line\''eval $cmdeval "$cmd"*eval
2赞 Ihe Onwuka 10/6/2021
永远不要在没有适当工具的情况下解析XML或JSON是合理的建议。唯一的例外是,如果您因为输入的大小而需要流式传输输入。
0赞 ElectroBuddha 12/20/2022
很棒的解决方案。对我来说,该值包含在变量(MacOS,z-shell终端)中。$3
4赞 jpseng 11/10/2022 #16

yq 可用于 XML 解析 (以下示例所需的版本:>= 4.30.5)。

它是一个轻量级且可移植的命令行 YAML 处理器,也可以处理 XML。 语法类似于 jq

输入

<root>
  <myel name="Foo" />
  <myel name="Bar">
    <mysubel>stairway to heaven</mysubel>
  </myel>
</root>

使用示例1

yq --input-format xml '.root.myel.0.+@name' $FILE

Foo

使用示例2

yq有一个很好的内置功能,使 XML 易于 grep

yq --input-format xml --output-format props $FILE

root.myel.0.+@name = Foo
root.myel.1.+@name = Bar
root.myel.1.mysubel = stairway to heaven

使用示例3

yq还可以将 XML 输入转换为 JSON 或 YAML

yq --input-format xml --output-format json $FILE

{
  "root": {
    "myel": [
      {
        "+@name": "Foo"
      },
      {
        "+@name": "Bar",
        "mysubel": "stairway to heaven"
      }
    ]
  }
}

yq --input-format xml $FILE (YAML是默认格式)

root:
  myel:
    - +@name: Foo
    - +@name: Bar
      mysubel: stairway to heaven
0赞 pancake 11/22/2022 #17

试试 xpe。它是专门为此目的而建造的。你可以用 python3 pip 安装它:

pip3 install xpe

您可以像这样使用它:

curl example.com | xpe '//title'

上述命令返回:

示例域