Как анализировать XML в Bash?

В идеале, я хотел бы иметь возможность:

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

Ответ 1

Это действительно просто объяснение ответа

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

Хорошо, поэтому он определяет функцию read_dom. Первая строка делает IFS (разделитель полей ввода) локальной для этой функции и изменяет ее нa > . Это означает, что когда вы читаете данные вместо автоматического разделения на пространство, вкладку или новые строки, она делится на " > ". Следующая строка говорит, чтобы прочитать ввод из stdin и вместо остановки в новой строке остановится, когда вы увидите '<' character (-d для флага делителя). То, что читается, затем разделяется с использованием IFS и присваивается переменной ENTITY и CONTENT. Итак, сделайте следующее:

<tag>value</tag>

Первый вызов read_dom получает пустую строку (поскольку "<" является первым символом). Это расщепляется IFS на просто '', так как не существует символа ' > '. Затем чтение присваивает пустую строку обеим переменным. Второй вызов получает строку "tag > value". Это получает разделение, затем IFS на два поля "tag" и "value". Затем считываются переменные типа: ENTITY=tag и CONTENT=value. Третий вызов получает строку '/tag > '. Это разделяется IFS на два поля "/tag" и "". Затем считываются переменные типа: ENTITY=/tag и CONTENT=. Четвертый вызов вернет ненулевой статус, потому что мы достигли конца файла.

Теперь его цикл while очистился немного, чтобы соответствовать выше:

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

В первой строке просто сказано: "Пока функция read_dom возвращает состояние нуля, выполните следующие действия". Вторая строка проверяет, является ли объект, который мы только что видели, "title". Следующая строка отображает содержимое тега. Четыре линии выходят. Если это не был заголовок, тогда цикл повторяется на шестой строке. Мы перенаправляем "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>[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 => 

Итак, если мы написали цикл while, такой как Yuzem's:

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

Мы получим список всех файлов в ведре S3.

ИЗМЕНИТЬ Если по какой-либо причине local IFS=\> не работает для вас, и вы установите его глобально, вы должны reset в конце функции, например:

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

В противном случае любое разделение строк, которое вы сделаете позже в script, будет испорчено.

РЕДАКТИРОВАТЬ 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_dom вызовите parse_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

EDIT 3 другой сказал, что у них возникли проблемы с ним в FreeBSD, и предложил сохранить статус выхода от чтения и возврата в конце read_dom like:

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

Я не вижу причин, почему это не должно работать

Ответ 2

Вы можете сделать это очень легко, используя только bash. Вам нужно только добавить эту функцию:

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

Теперь вы можете использовать rdom, например read, но для html-документов. Когда вызываемый rdom присваивает элемент переменной E и содержимому var C.

Например, чтобы сделать то, что вы хотели сделать:

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

Ответ 3

Средства командной строки, которые могут быть вызваны из сценариев оболочки, включают в себя:

  • 4xpath - оболочка командной строки вокруг Python 4Suite
  • XMLStarlet
  • xpath - оболочка командной строки вокруг библиотеки Perl XPath
  • Xidel - Работает с URL-адресами, а также с файлами. Также работает с JSON

Я также использую xmllint и xsltproc с небольшими сценариями преобразования XSL для обработки XML из командной строки или в сценариях оболочки.

Ответ 4

Вы можете использовать утилиту xpath. Он установлен с пакетом Perl XML-XPath.

Использование:

/usr/bin/xpath [filename] query

или XMLStarlet. Чтобы установить его на openuse, используйте:

sudo zypper install xmlstarlet

или попробуйте cnf xml на других платформах.

Ответ 5

Отметьте XML2 из http://www.ofb.net/~egnor/xml2/, который преобразует XML в линейно-ориентированный формат.

Ответ 6

Другим инструментом командной строки является мой новый 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 )

устанавливает $title в заголовок и $imgcount количество изображений в файле, которое должно быть таким же гибким, как разбор его непосредственно в bash.

Ответ 7

Этого достаточно...

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

Ответ 8

Мне не известно о любом чистом XML-синтаксическом инструменте. Поэтому вам, скорее всего, понадобится инструмент, написанный на другом языке.

Мой модуль XML:: Twig Perl поставляется с таким инструментом: xml_grep, где вы, вероятно, напишите, что хотите, как xml_grep -t '/html/head/title' xhtmlfile.xhtml > titleOfXHTMLPage.txt (опция -t дает результат как текст вместо xml)

Ответ 9

Ну, вы можете использовать утилиту xpath. Я предполагаю, что perl XML:: Xpath содержит его.

Ответ 10

После некоторых исследований для перевода между форматами файлов Linux и Windows в файлах XML я нашел интересные руководства и решения на:

Ответ 11

Это работает, если вам нужны атрибуты 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

Ответ 12

Метод Yuzem может быть улучшен путем обращения порядка знаков < и > в функции rdom и назначения переменных, так что:

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

становится:

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

Если разбор не выполняется так, последний тег в XML файле никогда не будет достигнут. Это может быть проблематично, если вы собираетесь вывести другой XML файл в конце цикла while.

Ответ 13

начиная с ответа chad, вот работающее решение COMPLETE для анализа UML, с обработкой комментариями с помощью propper, всего лишь с двумя небольшими функциями (более 2 бу, вы можете их смешивать). Я не говорю, что chad не работал вообще, но у него было слишком много проблем с плохо сформированными файлами XML: так что вам нужно быть более сложным для обработки комментариев и неулокальных мест/CR/TAB/и т.д.

Цель этого ответа - предоставить готовые функции "2-го использования" из bash для всех, нуждающихся в разборе UML без сложных инструментов, используя perl, python или что-то еще. Что касается меня, я не могу установить cpan и perl-модули для старой операционной системы, над которой я работаю, и python недоступен.

Во-первых, определение слов UML, используемых в этом сообщении:

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

EDIT: обновленные функции, с дескриптором:

  • Атрибуты 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 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 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 (to stderr):

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

Раскраска:

oh, и вам понадобятся некоторые опрятные раскрашивающие динамические переменные, которые будут сначала определены и экспортированы:

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 > ] [tag | "any" ] [attributes.. | "Содержание" ]

-c = NOCOLOR

-d = Отладка

-l = LIGHT (no\ "attribute = \" напечатан)

-p = FORCE PRINT (если атрибуты не указаны)

-x = применить команду к атрибуту и ​​напечатать результат вместо прежнего значения в зеленом цвете

(никакие атрибуты не будут загружать их значения в вашу оболочку как $ATTRIBUTE = значение; используйте '-p' для их печати)

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

Ответ 14

Хотя существует немало готовых консольных утилит, которые могут делать то, что вам нужно, по моему опыту требуется значительно меньше времени для написания пары строк кода на языке программирования общего назначения, таком как Python, который вы можете легко расширяться и адаптироваться к вашим потребностям.

Вот пример python script с lxml - он принимает имя файла в качестве первого параметр, выражение XPath как второй параметр и печатает строки/узлы, соответствующие данному выражению XPath.

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

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

for e in tree.xpath(xpath_expression):

    if isinstance(e, str):
        print(e)
    else:
        print(e.text or etree.tostring(e))

lxml можно установить с помощью pip install lxml. На ubuntu вы можете использовать sudo apt install python-lxml.

Использование

python xmlcat.py file.xml "//mynode"

lxml также может принимать URL-адрес как вход:

python xmlcat.py http://example.com/file.xml "//mynode" 

Извлеките атрибут url в enclosure node (т.е. <enclosure url="http:...""..>):

python xmlcat.py xmlcat.py file.xml "//enclosure/@url"

Примечание: lxml отлично работает с точки зрения скорости, так как это хорошо оптимизированный собственный модуль, написанный на C.


2 ¢: Не тратьте время на изучение неясного синтаксиса некоторой наполовину поддерживаемой утилиты. Жизнь слишком коротка для этого. Вместо этого используйте доступные ресурсы, чтобы расширить свои навыки программирования с помощью широко используемых API.