Como analisar XML no Bash?

Respostas:

153

Esta é realmente apenas uma explicação da resposta de Yuzem , mas não achei que essa edição devesse ser feita para outra pessoa e os comentários não permitem a formatação, então ...

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

Vamos chamar de "read_dom" em vez de "rdom", espaçá-lo um pouco e usar variáveis ​​mais longas:

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

Ok, então define uma função chamada read_dom. A primeira linha torna o IFS (o separador de campos de entrada) local para essa função e o altera para>. Isso significa que, quando você lê dados, em vez de serem automaticamente divididos no espaço, tabulação ou novas linhas, eles são divididos em '>'. A próxima linha diz para ler a entrada de stdin e, em vez de parar em uma nova linha, pare quando vir um caractere '<' (o sinalizador -d para deliminador). O que é lido é então dividido usando o IFS e atribuído à variável ENTITY e CONTENT. Então, faça o seguinte:

<tag>value</tag>

A primeira chamada para read_domobter uma sequência vazia (já que o '<' é o primeiro caractere). Isso é dividido pelo IFS em apenas '', pois não há um caractere '>'. O Read então atribui uma string vazia às duas variáveis. A segunda chamada obtém a string 'tag> value'. Isso é dividido pelo IFS nos dois campos 'tag' e 'value'. O Read então atribui as variáveis ​​como: ENTITY=tage CONTENT=value. A terceira chamada obtém a string '/ tag>'. Isso é dividido pelo IFS nos dois campos '/ tag' e ''. O Read então atribui as variáveis ​​como: ENTITY=/tage CONTENT=. A quarta chamada retornará um status diferente de zero, porque chegamos ao final do arquivo.

Agora, o loop while se limpou um pouco para coincidir com o acima:

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

A primeira linha diz apenas "enquanto a função read_dom retorna um status zero, faça o seguinte." A segunda linha verifica se a entidade que acabamos de ver é "title". A próxima linha faz eco do conteúdo da tag. A linha quatro sai. Se não fosse a entidade do título, o loop será repetido na sexta linha. Nós redirecionamos "xhtmlfile.xhtml" para a entrada padrão (para a read_domfunção) e redirecionamos a saída padrão para "titleOfXHTMLPage.txt" (o eco do início do loop).

Agora, dê o seguinte (semelhante ao que você obtém ao listar um bucket no S3) para 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>

e o seguinte loop:

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

Voce deveria pegar:

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

Então, se escrevemos um whileloop como o de Yuzem:

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

Obteríamos uma lista de todos os arquivos no bucket S3.

EDIT Se, por algum motivo local IFS=\>, não funcionar para você e você configurá-lo globalmente, você deve redefini-lo no final da função, como:

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

Caso contrário, qualquer linha dividida posteriormente no script será confusa.

EDIT 2 Para dividir os pares de nome / valor de atributo, você pode aumentar da seguinte read_dom()maneira:

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

Em seguida, escreva sua função para analisar e obter os dados desejados assim:

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
}

Então, enquanto você read_domliga para parse_dom:

while read_dom; do
    parse_dom
done

Depois, dê o seguinte exemplo de marcação:

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

Você deve obter esta saída:

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

EDIT 3 outro usuário disse que estava tendo problemas com ele no FreeBSD e sugeriu salvar o status de saída da leitura e devolvê-lo no final do read_dom como:

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

Não vejo nenhuma razão para que isso não funcione

Chade
fonte
2
Se você tornar o IFS (o separador de campos de entrada) global, deve redefinir seu valor original no final, editei a resposta para obtê-lo. Caso contrário, qualquer outra entrada que você faça posteriormente no seu script será confusa. Eu suspeito que o motivo pelo qual o local não funcione para você é porque você está usando o bash em um modo de compatibilidade (como o seu shbang é #! / Bin / sh) ou é uma versão antiga do bash.
chad
30
Só porque você pode escrever seu próprio analisador, não significa que deveria.
Stephen Niedzielski
1
@chad certamente diz algo sobre AWS' fluxo de trabalho / implementação que eu estava procurando uma resposta para 'xml bash' também wget o conteúdo de um balde S3!
precisa
2
@Alastair ver github.com/chad3814/s3scripts para um conjunto de scripts bash que usamos para manipular S3 objetos
chad
5
A atribuição do IFS em uma variável local é frágil e não é necessária. Basta fazer:, IFS=\< read ...que definirá apenas o IFS para a chamada de leitura. (Note que eu estou em nenhuma maneira endossando a prática do uso readde xml de análise, e eu acredito que isso é muito perigoso e deve ser evitado.)
William Pursell
64

Você pode fazer isso muito facilmente usando apenas o bash. Você só precisa adicionar esta função:

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

Agora você pode usar o rdom como read, mas para documentos html. Quando chamado, rdom atribuirá o elemento à variável E e o conteúdo à var C.

Por exemplo, para fazer o que você queria:

while rdom; do
    if [[ $E = title ]]; then
        echo $C
        exit
    fi
done < xhtmlfile.xhtml > titleOfXHTMLPage.txt
Yuzem
fonte
você poderia elaborar isso? Eu aposto que é perfeitamente claro para você .. e isso poderia ser uma ótima resposta - se eu pudesse dizer o que você estava fazendo lá ... você pode dividir um pouco mais, possivelmente gerando alguma amostra de saída?
Alex Gray
1
Credite para o original - este verso é tão elegante e incrível.
Maverick
1
grande corte, mas eu tinha de usar as aspas como echo "$ C" para evitar a expansão shell e interpretação correta das linhas finais (depende do enconding)
user311174
8
A análise de XML com grep e awk não está bem . Pode ser um compromisso aceitável se os XMLs forem bastante simples e você não tiver muito tempo, mas nunca poderá ser considerada uma boa solução.
peterh - Restabelece Monica
59

As ferramentas de linha de comando que podem ser chamadas a partir de scripts de shell incluem:

  • 4xpath - wrapper de linha de comando em torno do pacote 4Suite do Python
  • XMLStarlet
  • xpath - wrapper de linha de comando da biblioteca XPath do Perl
  • Xidel - Funciona com URLs e arquivos. Também funciona com JSON

Também uso xmllint e xsltproc com pequenos scripts de transformação XSL para processar XML a partir da linha de comando ou em scripts de shell.

Nat
fonte
2
De onde posso baixar 'xpath' ou '4xpath'?
Opher
3
sim, uma segunda votação / solicitação - onde baixar essas ferramentas, ou você quer dizer que é necessário escrever manualmente um wrapper? Prefiro não perder tempo fazendo isso, a menos que seja necessário.
David
4
sudo apt-get install libxml-xpath-perl
Andrew Wagner
22

Você pode usar o utilitário xpath. Está instalado com o pacote Perl XML-XPath.

Uso:

/usr/bin/xpath [filename] query

ou XMLStarlet . Para instalá-lo no opensuse, use:

sudo zypper install xmlstarlet

ou tente cnf xmlem outras plataformas.

Grisha
fonte
5
O uso de xml starlet é definitivamente uma opção melhor do que escrever o próprio serializador (como sugerido nas outras respostas).
Bruno von Paris
Em muitos sistemas, o xpathque vem pré-instalado não é adequado para uso como componente em scripts. Veja, por exemplo, stackoverflow.com/questions/15461737/… para uma elaboração.
Tripleee
2
No Ubuntu / Debianapt-get install xmlstarlet
rubo77
12

Isso é suficiente ...

xpath xhtmlfile.xhtml '/html/head/title/text()' > titleOfXHTMLPage.txt
teknopaul
fonte
Obrigado, rápido e fez o trabalho por mim
Miguel Mota
1
No debian apt-get install libxml-xpath-perl.
precisa saber é o seguinte
funciona como charme
Alexandru-Mihai Manolescu 09/07
5

a partir da resposta do chad, aqui está a solução de trabalho COMPLETA para analisar UML, com manipulação apropriada de comentários, com apenas duas pequenas funções (mais de 2 bu, você pode misturar todas). Não digo que o chad não funcionou, mas houve muitos problemas com arquivos XML mal formatados: então você precisa ser um pouco mais complicado para lidar com comentários e espaços extraviados / CR / TAB / etc.

O objetivo desta resposta é fornecer funções bash prontas para o uso, prontas para uso, para qualquer pessoa que precise analisar a UML sem ferramentas complexas usando perl, python ou qualquer outra coisa. Quanto a mim, não consigo instalar o cpan, nem os módulos perl para o antigo sistema operacional de produção em que estou trabalhando, e o python não está disponível.

Primeiro, uma definição das palavras UML usadas neste post:

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

EDIT: funções atualizadas, com alça de:

  • XML da Websphere (atributos xmi e xmlns)
  • deve ter um terminal compatível com 256 cores
  • 24 tons de cinza
  • compatibilidade adicionada para o IBM AIX bash 3.2.16 (1)

As funções, primeiro, são o xml_read_dom chamado recursivamente por xml_read:

xml_read_dom() {
# /programming/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
}

e o segundo:

xml_read() {
# /programming/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
}

e, finalmente, as funções rtrim, trim e 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; }

Colorização:

ah, e você precisará de algumas variáveis ​​dinâmicas de cores limpas para serem definidas primeiro e exportadas também:

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"'

Como carregar tudo isso:

Você sabe como criar funções e carregá-las via FPATH (ksh) ou uma emulação de FPATH (bash)

Caso contrário, basta copiar / colar tudo na linha de comando.

Como funciona:

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

Com o modo Debug (-d), comentários e atributos analisados ​​são impressos no stderr

Carniceiro
fonte
Estou tentando usar as duas funções acima, que produz o seguinte ./read_xml.sh: line 22: (-1): substring expression < 0:?
precisa saber é o seguinte
Linha 22:[ "x${ATTRIBUTES:(-1):1}x" == "x?x" ] ...
khmarbaise
desculpe khmarbaise, estas são funções do shell bash. Se você quiser adaptá-los como scripts de shell, certamente precisará esperar algumas pequenas adaptações! Também as funções atualizados lidar com seus erros;)
limpador
4

Não conheço nenhuma ferramenta de análise XML de shell puro. Então você provavelmente precisará de uma ferramenta escrita em outro idioma.

Meu módulo XML :: Twig Perl vem com uma ferramenta: xml_greponde você provavelmente escreveria o que deseja xml_grep -t '/html/head/title' xhtmlfile.xhtml > titleOfXHTMLPage.txt(a -topção fornece o resultado como texto em vez de xml)

mirod
fonte
4

Outra ferramenta de linha de comando é o meu novo Xidel . Ele também suporta XPath 2 e XQuery, ao contrário do xpath / xmlstarlet já mencionado.

O título pode ser lido como:

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

E também possui um recurso interessante para exportar várias variáveis ​​para o bash. Por exemplo

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

define $titleo título e $imgcounto número de imagens no arquivo, que deve ser tão flexível quanto analisá-lo diretamente no bash.

BeniBela
fonte
Isso é exatamente o que eu precisava! :)
Thomas Daugaard
2

Bem, você pode usar o utilitário xpath. Eu acho que o XML :: Xpath do perl o contém.

alamar
fonte
2

Após alguma pesquisa para tradução entre os formatos Linux e Windows dos caminhos dos arquivos XML, encontrei tutoriais e soluções interessantes sobre:

user485380
fonte
2

Embora existam alguns utilitários de console prontos que podem fazer o que você deseja, provavelmente levará menos tempo para escrever algumas linhas de código em uma linguagem de programação de uso geral, como o Python, na qual você pode estender e adaptar facilmente suas necessidades.

Aqui está um script python usado lxmlpara análise - ele pega o nome de um arquivo ou uma URL como o primeiro parâmetro, uma expressão XPath como o segundo parâmetro e imprime as strings / nós correspondentes à expressão especificada.

Exemplo 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))

lxmlpode ser instalado com pip install lxml. No ubuntu você pode usar sudo apt install python-lxml.

Uso

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

lxml também aceita um URL como entrada:

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

Nota : Se o seu XML tiver um espaço para nome padrão sem prefixo (por exemplo xmlns=http://abc...), você deverá usar o pprefixo (fornecido pelo 'hack') em suas expressões, por exemplo, //p:modulepara obter os módulos de um pom.xmlarquivo. Caso o pprefixo já esteja mapeado em seu XML, será necessário modificar o script para usar outro prefixo.


Exemplo 2

Um script único que serve ao propósito restrito de extrair nomes de módulos de um arquivo apache maven. Observe como o nome do nó ( module) é prefixado com o espaço para nome padrão {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)
ccpizza
fonte
Isso é incrível quando você quer evitar a instalação de pacotes extras ou não tem acesso. Em uma máquina de compilação, posso justificar uma chamada ou pip installexcesso extra . Obrigado! apt-getyum
E. Moffat
0

O método de Yuzem pode ser aprimorado invertendo a ordem dos sinais <e >na rdomfunção e nas atribuições de variáveis, de modo que:

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

torna-se:

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

Se a análise não for feita assim, a última tag no arquivo XML nunca será alcançada. Isso pode ser problemático se você pretende gerar outro arquivo XML no final do whileloop.

michaelmeyer
fonte
0

Isso funciona se você estiver querendo atributos 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
Steven Penny
fonte
-1

Embora pareça que "nunca analise XML, JSON ... do bash sem uma ferramenta adequada" é um bom conselho, discordo. Se esse é um trabalho paralelo, é essencial procurar a ferramenta adequada e aprender ... O Awk pode fazê-lo em minutos. Meus programas precisam trabalhar com todos os tipos de dados mencionados acima e mais. Inferno, eu não quero testar 30 ferramentas para analisar 5-7-10 formatos diferentes que eu preciso se puder resolver o problema em questão de minutos. Eu não ligo para XML, JSON ou o que quer! Eu preciso de uma solução única para todos eles.

Como exemplo: meu programa SmartHome administra nossas casas. Ao fazê-lo, ele lê uma infinidade de dados em muitos formatos diferentes que não consigo controlar. Eu nunca uso ferramentas dedicadas e adequadas, pois não quero gastar mais do que minutos lendo os dados necessários. Com os ajustes FS e RS, essa solução awk funciona perfeitamente para qualquer formato de texto. Mas, pode não ser a resposta correta quando sua tarefa principal é trabalhar principalmente com cargas de dados nesse formato!

O problema de analisar XML do bash que eu enfrentei ontem. Aqui está como eu faço isso para qualquer formato de dados hierárquico. Como um bônus - eu atribuo dados diretamente às variáveis ​​em um script bash.

Para facilitar a leitura, apresentarei a solução em etapas. A partir dos dados de teste do OP, criei um arquivo: test.xml

Analisando o referido XML no bash e extraindo os dados em 90 caracteres:

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

Normalmente, uso uma versão mais legível, pois é mais fácil modificar na vida real, pois geralmente preciso testar de forma diferente:

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

Eu não me importo como é chamado o formato. Eu busco apenas a solução mais simples. Nesse caso em particular, posso ver pelos dados que newline é o separador de registros (RS) e <> delimit fields (FS). No meu caso original, eu tinha uma indexação complicada de 6 valores em dois registros, relacionando-os, descobrindo quando os dados existem mais campos (registros) podem ou não existir. Foram necessárias 4 linhas de awk para resolver o problema perfeitamente. Então, adapte a idéia a cada necessidade antes de usá-la!

A segunda parte simplesmente parece que há uma sequência desejada em uma linha (RS) e, se houver, imprime os campos necessários (FS). O processo acima levou cerca de 30 segundos para copiar e adaptar do último comando que usei dessa maneira (quatro vezes mais). E é isso aí! Feito em 90 caracteres.

Mas sempre preciso inserir os dados ordenadamente em variáveis ​​no meu script. Primeiro testei as construções da seguinte forma:

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

Em alguns casos, uso printf em vez de print. Quando vejo que tudo parece bem, simplesmente termino de atribuir valores a variáveis. Eu sei que muitos pensam que "eval" é "mau", não há necessidade de comentar :) Trick funciona perfeitamente em todas as minhas quatro redes há anos. Mas continue aprendendo se você não entende por que isso pode ser uma prática ruim! Incluindo atribuições de variáveis ​​bash e amplo espaçamento, minha solução precisa de 120 caracteres para fazer tudo.

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"
Pila
fonte