Как разобрать XML в Bash? - PullRequest
124 голосов
/ 21 мая 2009

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

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

Ответы [ 15 ]

138 голосов
/ 13 августа 2011

Это действительно просто объяснение ответа Юзема , но я не чувствовал, что такое большое редактирование следует делать кому-то другому, а комментарии не позволяют форматировать, так что ...

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

Давайте назовем это «read_dom» вместо «rdom», поместим его немного в пространство и используем более длинные переменные:

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

Хорошо, поэтому он определяет функцию с именем read_dom. Первая строка делает IFS (разделитель поля ввода) локальным для этой функции и изменяет его на>. Это означает, что когда вы читаете данные вместо того, чтобы автоматически разделяться на пробелы, символы табуляции или перевода строки, они разделяются на «>». В следующей строке написано, что нужно читать ввод из stdin, и вместо того, чтобы останавливаться на новой строке, останавливаться, когда вы видите символ «<» (флаг -d для deliminator). То, что читается, затем разделяется с использованием IFS и присваивается переменной ENTITY и CONTENT. Поэтому возьмите следующее: </p>

<tag>value</tag>

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

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

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

В первой строке просто сказано: «пока функция 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>

и следующий цикл:

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 => 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 => 

Итак, если мы написали цикл while, как у Юзема:

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

Мы получили бы список всех файлов в корзине S3.

EDIT Если по какой-то причине 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_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

РЕДАКТИРОВАТЬ 3 другой пользователь сказал, что у него проблемы с ним во FreeBSD, и предложил сохранить состояние выхода из чтения и вернуть его в конце read_dom, например:

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

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

59 голосов
/ 09 апреля 2010

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

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

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

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

while rdom; do
    if [[ $E = title ]]; then
        echo $C
        exit
    fi
done < xhtmlfile.xhtml > titleOfXHTMLPage.txt
53 голосов
/ 21 мая 2009

Инструменты командной строки, которые можно вызывать из сценариев оболочки:

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

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

21 голосов
/ 24 апреля 2012

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

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

/usr/bin/xpath [filename] query

или XMLStarlet . Для установки на opensuse используйте:

sudo zypper install xmlstarlet

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

8 голосов
/ 05 января 2015

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

xpath xhtmlfile.xhtml '/html/head/title/text()' > titleOfXHTMLPage.txt
5 голосов
/ 29 января 2014

начиная с ответа Чада, вот ПОЛНОЕ рабочее решение для анализа UML, с более точной обработкой комментариев, всего с двумя небольшими функциями (более 2-х, но вы можете смешать их все). Я не говорю, что chad's один не работал вообще, но у него было слишком много проблем с плохо отформатированными XML-файлами: так что вам нужно быть немного хитрее, чтобы обрабатывать комментарии и неуместные пробелы / CR / TAB / и т.д.

Цель этого ответа - предоставить готовые 2 готовые функции bash для тех, кто нуждается в разборе UML без сложных инструментов, использующих 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() {
# /778769/kak-razobrat-xml-v-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() {
# /778769/kak-razobrat-xml-v-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 (to 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

5 голосов
/ 07 ноября 2009

Извлечение XML2 из http://www.ofb.net/~egnor/xml2/, которое преобразует XML в формат строки.

4 голосов
/ 27 марта 2013

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

4 голосов
/ 21 мая 2009

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

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

2 голосов
/ 25 октября 2017

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

Вот скрипт Python, который использует lxml для анализа - он принимает имя файла или 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 может быть установлен с pip install lxml. В Ubuntu вы можете использовать sudo apt install python-lxml.

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

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

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

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

Примечание : Если у вашего XML есть пространство имен по умолчанию без префикса (например, xmlns=http://abc...), тогда вы должны использовать префикс p (предоставляется 'hack') в ваших выражениях, например, //p:module, чтобы получить модули из файла pom.xml. В случае, если префикс p уже сопоставлен в вашем XML, вам необходимо изменить скрипт, чтобы использовать другой префикс.


Пример 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)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...