如何在Bash中解析XML?

128 xml bash shell xhtml xpath

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

cat xhtmlfile.xhtml |
getElementViaXPath --path='/html/head/title' |
sed -e 's%(^<title>|</title>$)%%g' > titleOfXHTMLPage.txt
Run Code Online (Sandbox Code Playgroud)

cha*_*had 144

这实际上只是对Yuzem答案的解释,但我不觉得应该对其他人进行这么多编辑,并且评论不允许格式化,所以...

rdom () { local IFS=\> ; read -d \< E C ;}
Run Code Online (Sandbox Code Playgroud)

让我们称之为"read_dom"而不是"rdom",将其空出一点并使用更长的变量:

read_dom () {
    local IFS=\>
    read -d \< ENTITY CONTENT
}
Run Code Online (Sandbox Code Playgroud)

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

<tag>value</tag>
Run Code Online (Sandbox Code Playgroud)

第一次调用read_dom获取一个空字符串(因为'<'是第一个字符).由于没有'>'字符,因此IFS将其拆分为''.然后读取为两个变量分配一个空字符串.第二个调用获取字符串'tag> value'.然后由IFS分成两个字段'tag'和'value'.然后读取分配变量,如:ENTITY=tagCONTENT=value.第三个调用获取字符串'/ tag>'.由IFS分成两个字段'/ tag'和''.然后读取分配变量,如:ENTITY=/tagCONTENT=.第四个调用将返回非零状态,因为我们已到达文件末尾.

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

while read_dom; do
    if [[ $ENTITY = "title" ]]; then
        echo $CONTENT
        exit
    fi
done < xhtmlfile.xhtml > titleOfXHTMLPage.txt
Run Code Online (Sandbox Code Playgroud)

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

现在给出以下内容(类似于在S3上列出一个桶的内容)input.xml:

<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Name>sth-items</Name>
  <IsTruncated>false</IsTruncated>
  <Contents>
    <Key>item-apple-iso@2x.png</Key>
    <LastModified>2011-07-25T22:23:04.000Z</LastModified>
    <ETag>&quot;0032a28286680abee71aed5d059c6a09&quot;</ETag>
    <Size>1785</Size>
    <StorageClass>STANDARD</StorageClass>
  </Contents>
</ListBucketResult>
Run Code Online (Sandbox Code Playgroud)

和以下循环:

while read_dom; do
    echo "$ENTITY => $CONTENT"
done < input.xml
Run Code Online (Sandbox Code Playgroud)

你应该得到:

 => 
ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/" => 
Name => sth-items
/Name => 
IsTruncated => false
/IsTruncated => 
Contents => 
Key => item-apple-iso@2x.png
/Key => 
LastModified => 2011-07-25T22:23:04.000Z
/LastModified => 
ETag => &quot;0032a28286680abee71aed5d059c6a09&quot;
/ETag => 
Size => 1785
/Size => 
StorageClass => STANDARD
/StorageClass => 
/Contents => 
Run Code Online (Sandbox Code Playgroud)

所以如果我们写一个while像Yuzem的循环:

while read_dom; do
    if [[ $ENTITY = "Key" ]] ; then
        echo $CONTENT
    fi
done < input.xml
Run Code Online (Sandbox Code Playgroud)

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

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

read_dom () {
    ORIGINAL_IFS=$IFS
    IFS=\>
    read -d \< ENTITY CONTENT
    IFS=$ORIGINAL_IFS
}
Run Code Online (Sandbox Code Playgroud)

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

编辑2 要拆分属性名称/值对,您可以增加read_dom()类似的内容,以便:

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

然后编写你的函数来解析并得到你想要的数据,如下所示:

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
}
Run Code Online (Sandbox Code Playgroud)

然后你read_dom打电话parse_dom:

while read_dom; do
    parse_dom
done
Run Code Online (Sandbox Code Playgroud)

然后给出以下示例标记:

<example>
  <bar size="bar_size" type="metal">bars content</bar>
  <foo size="1789" type="unknown">foos content</foo>
</example>
Run Code Online (Sandbox Code Playgroud)

你应该得到这个输出:

$ cat example.xml | ./bash_xml.sh 
bar type is: metal
foo size is: 1789
Run Code Online (Sandbox Code Playgroud)

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

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

我认为没有任何理由不应该起作用

  • 仅仅因为你可以编写自己的解析器,并不意味着你应该. (24认同)
  • 在局部变量中分配IFS是脆弱的,不是必需的.只需:`IFS =\<read ...`,它只会为读取调用设置IFS.(请注意,我绝不赞同使用`read`来解析xml的做法,我相信这样做充满了危险,应该避免.) (5认同)
  • 如果你使IFS(输入字段分隔符)全局,你应该在最后将它重置回原始值,我编辑了答案.否则,稍后在脚本中执行的任何其他输入分割将被搞砸.我怀疑本地不适合你的原因是因为你在兼容模式下使用bash(比如你的shbang是#!/ bin/sh)或者它是bash的古老版本. (2认同)
  • @Alastair请参阅https://github.com/chad3814/s3scripts,了解我们用来操作S3对象的一组bash脚本 (2认同)
  • 我很抱歉我的评论如此粗鲁和消极。我的观点是,临时 XML 解析器实现不太可能是正确的(也许这个是正确的,但没有一套全面的测试可以证明这一点)。选择其他答案中提到的能够很好地解决 XML 解析的专用工具对我来说似乎更实用。 (2认同)

小智 60

只使用bash就可以轻松完成.您只需添加此功能:

rdom () { local IFS=\> ; read -d \< E C ;}
Run Code Online (Sandbox Code Playgroud)

现在你可以使用rdom之类的读取,但是对于html文档.当调用rdom时,将元素赋值给变量E,将内容赋值给var C.

例如,要做你想做的事:

while rdom; do
    if [[ $E = title ]]; then
        echo $C
        exit
    fi
done < xhtmlfile.xhtml > titleOfXHTMLPage.txt
Run Code Online (Sandbox Code Playgroud)

  • **用grep和awk解析XML是不行的**.如果XML足够简单并且您没有太多时间,那么它可能是一个可接受的折衷方案,但它不能被称为一个好的解决方案. (7认同)
  • 相信原版 - 这款单衬纸非常优雅和令人惊叹。 (2认同)
  • 伟大的黑客,但我不得不使用像 echo "$C" 这样的双引号来防止外壳扩展和正确解释结束行(取决于编码) (2认同)

Nat*_*Nat 54

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

  • 4xpath - 围绕Python的4Suite包的命令行包装器
  • XMLStarlet
  • xpath - Perl的XPath库周围的命令行包装器
  • Xidel - 使用URL和文件.也适用于JSON

我还使用xmllint和xsltproc以及少量XSL转换脚本从命令行或shell脚本中进行XML处理.

  • 是的,第二次投票/请求 - 在哪里下载这些工具,或者你是否意味着必须手动编写包装器?除非必要,否则我宁愿不浪费时间. (3认同)
  • sudo apt-get install libxml-xpath-perl (3认同)
  • 我在哪里可以下载'xpath'或'4xpath'? (2认同)

小智 22

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

用法:

/usr/bin/xpath [filename] query
Run Code Online (Sandbox Code Playgroud)

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

sudo zypper install xmlstarlet
Run Code Online (Sandbox Code Playgroud)

或尝试cnf xml其他平台.

  • 使用xml starlet绝对是比编写自己的序列化程序更好的选择(如其他答案所示). (5认同)
  • 在Ubuntu/Debian`apt-get install xmlstarlet`上 (2认同)

tek*_*aul 8

这够了......

xpath xhtmlfile.xhtml '/html/head/title/text()' > titleOfXHTMLPage.txt
Run Code Online (Sandbox Code Playgroud)

  • 在 debian 中 `apt-get install libxml-xpath-perl` 。 (3认同)

sim*_*n04 5

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


Ben*_*ela 5

另一个命令行工具是我的新Xidel。它还支持 XPath 2 和 XQuery,这与已经提到的 xpath/xmlstarlet 不同。

标题可以这样读:

xidel xhtmlfile.xhtml -e /html/head/title > titleOfXHTMLPage.txt
Run Code Online (Sandbox Code Playgroud)

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

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

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


sca*_*ger 5

从chad的答案开始,这里是用于解析UML的COMPLETE工作解决方案,只需2个小函数就可以处理注释,超过2个你可以将它们全部混合起来.我不是说chad的一个根本没用,但它对于格式错误的XML文件有太多的问题:所以你必须更难处理注释和错放的空格/ CR/TAB /等.

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

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

<!-- comment... -->
<tag attribute="value">content...</tag>
Run Code Online (Sandbox Code Playgroud)

编辑:更新的功能,具有以下句柄:

  • 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
}
Run Code Online (Sandbox Code Playgroud)

第二个:

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
}
Run Code Online (Sandbox Code Playgroud)

最后,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; }
Run Code Online (Sandbox Code Playgroud)

彩色化:

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

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"'
Run Code Online (Sandbox Code Playgroud)

如何加载所有东西:

要么你知道如何创建函数并通过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
Run Code Online (Sandbox Code Playgroud)

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


ala*_*mar 3

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