¿Cómo analizar XML en Bash?

Respuestas:

153

Esto es realmente solo una explicación de la respuesta de Yuzem , pero no sentí que esta edición se deba hacer a otra persona, y los comentarios no permiten el formateo, así que ...

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

Llamemos a eso "read_dom" en lugar de "rdom", espacie un poco y use variables más largas:

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

Bien, entonces define una función llamada read_dom. La primera línea hace que IFS (el separador de campo de entrada) sea local a esta función y lo cambia a>. Eso significa que cuando lee datos en lugar de dividirse automáticamente en el espacio, la pestaña o las nuevas líneas, se divide en '>'. La siguiente línea dice que lea la entrada de stdin, y en lugar de detenerse en una nueva línea, deténgase cuando vea un carácter '<' (la -d para el indicador delimitador). Lo que se lee se divide usando el IFS y se asigna a la variable ENTIDAD y CONTENIDO. Entonces toma lo siguiente:

<tag>value</tag>

La primera llamada para read_domobtener una cadena vacía (ya que '<' es el primer carácter). Eso se divide por IFS en solo '', ya que no hay un carácter '>'. Leer luego asigna una cadena vacía a ambas variables. La segunda llamada obtiene la cadena 'etiqueta> valor'. Eso se divide luego por el IFS en los dos campos 'etiqueta' y 'valor'. Leer luego asigna las variables como: ENTITY=tagy CONTENT=value. La tercera llamada obtiene la cadena '/ tag>'. El IFS lo divide en los dos campos '/ etiqueta' y ''. Leer luego asigna las variables como: ENTITY=/tagy CONTENT=. La cuarta llamada devolverá un estado distinto de cero porque hemos llegado al final del archivo.

Ahora su ciclo while se limpió un poco para que coincida con lo anterior:

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

La primera línea dice: "mientras la función read_dom devuelve un estado cero, haga lo siguiente". La segunda línea verifica si la entidad que acabamos de ver es "título". La siguiente línea muestra el contenido de la etiqueta. Las cuatro salidas de línea. Si no era la entidad del título, el bucle se repite en la sexta línea. Redirigimos "xhtmlfile.xhtml" a la entrada estándar (para elread_dom función) y redirigimos la salida estándar a "titleOfXHTMLPage.txt" (el eco de antes en el bucle).

Ahora se da lo siguiente (similar a lo que obtienes al incluir un cubo en 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>

y el siguiente bucle:

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

Deberías obtener:

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

Entonces, si escribimos un whilebucle como el de Yuzem:

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

Obtendríamos una lista de todos los archivos en el bucket de S3.

EDITAR Si por alguna razón local IFS=\>no funciona para usted y lo configura globalmente, debe restablecerlo al final de la función como:

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

De lo contrario, cualquier división de línea que haga más adelante en el script se verá afectada.

EDITAR 2 Para dividir los pares de nombre / valor de atributo, puede aumentar el read_dom()tipo así:

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

Luego escriba su función para analizar y obtener los datos que desea de esta manera:

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
}

Luego, mientras read_domllamas parse_dom:

while read_dom; do
    parse_dom
done

Luego dado el siguiente marcado de ejemplo:

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

Deberías obtener esta salida:

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

EDITAR 3 otro usuario dijo que estaban teniendo problemas con él en FreeBSD y sugirió guardar el estado de salida de lectura y devolverlo al final de read_dom como:

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

No veo ninguna razón por la que eso no debería funcionar

Chad
fuente
2
Si hace que IFS (el separador de campo de entrada) sea global, debe restablecerlo a su valor original al final, edité la respuesta para tener eso. De lo contrario, cualquier otra división de entrada que haga más adelante en su secuencia de comandos se verá afectada. Sospecho que la razón por la que el local no funciona es porque estás usando bash en un modo de compatibilidad (como si tu shbang es #! / Bin / sh) o es una versión antigua de bash.
chad
30
El hecho de que pueda escribir su propio analizador no significa que deba hacerlo.
Stephen Niedzielski
1
¡@chad ciertamente dice algo sobre el flujo de trabajo / implementación de AWS que estaba buscando una respuesta a "bash xml" para también recordar el contenido de un bucket de S3!
Alastair
2
@Alastair, consulte github.com/chad3814/s3scripts para ver un conjunto de scripts de bash que usamos para manipular objetos S3
chad
55
Asignar IFS en una variable local es frágil y no es necesario. Simplemente haga: IFS=\< read ...que solo establecerá IFS para la llamada de lectura. (Tenga en cuenta que de ninguna manera estoy respaldando la práctica de usar readpara analizar xml, y creo que hacerlo está lleno de peligros y debería evitarse).
William Pursell
64

Puedes hacerlo fácilmente usando solo bash. Solo tiene que agregar esta función:

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

Ahora puede usar rdom como read pero para documentos html. Cuando se llama rdom asignará el elemento a la variable E y el contenido a la variable C.

Por ejemplo, para hacer lo que quería hacer:

while rdom; do
    if [[ $E = title ]]; then
        echo $C
        exit
    fi
done < xhtmlfile.xhtml > titleOfXHTMLPage.txt
Yuzem
fuente
¿Podrías dar más detalles sobre esto? Apuesto a que está perfectamente claro para usted ... y esta podría ser una gran respuesta: si pudiera decir lo que estaba haciendo allí ... ¿podría desglosarlo un poco más, posiblemente generando alguna salida de muestra?
Alex Gray
1
Cred a la original: este one-liner es tan increíblemente elegante y sorprendente.
inconformista
1
gran truco, pero tuve que usar comillas dobles como echo "$ C" para evitar la expansión de shell y la interpretación correcta de las líneas finales (depende de la búsqueda)
user311174
8
Analizar XML con grep y awk no está bien . Puede ser un compromiso aceptable si los XML son lo suficientemente simples y no tiene demasiado tiempo, pero nunca se puede llamar una buena solución.
peterh - Restablece a Monica
59

Las herramientas de línea de comandos que se pueden llamar desde scripts de shell incluyen:

  • 4xpath - envoltorio de línea de comando alrededor de 4Suite de Python paquete
  • XMLStarlet
  • xpath - contenedor de línea de comandos alrededor de la biblioteca XPath de Perl
  • Xidel : funciona con URL y archivos. También funciona con JSON

También uso xmllint y xsltproc con pequeños scripts de transformación XSL para hacer el procesamiento XML desde la línea de comandos o en scripts de shell.

Nat
fuente
2
¿Desde dónde puedo descargar 'xpath' o '4xpath'?
Opher
3
sí, una segunda votación / solicitud: ¿dónde descargar esas herramientas, o quiere decir que uno tiene que escribir manualmente un contenedor? Prefiero no perder el tiempo haciendo eso a menos que sea necesario.
David
44
sudo apt-get install libxml-xpath-perl
Andrew Wagner
22

Puede usar la utilidad xpath. Se instala con el paquete Perl XML-XPath.

Uso:

/usr/bin/xpath [filename] query

o XMLStarlet . Para instalarlo en opensuse use:

sudo zypper install xmlstarlet

o prueba cnf xmlen otras plataformas.

Grisha
fuente
55
Usar XML Starlet es definitivamente una mejor opción que escribir el propio serializador (como se sugiere en las otras respuestas).
Bruno von Paris
En muchos sistemas, el xpathque viene preinstalado no es adecuado para su uso como componente en scripts. Consulte, por ejemplo, stackoverflow.com/questions/15461737/… para obtener más detalles.
tripleee
2
En Ubuntu / Debianapt-get install xmlstarlet
rubo77
12

Esto es suficiente...

xpath xhtmlfile.xhtml '/html/head/title/text()' > titleOfXHTMLPage.txt
teknopaul
fuente
Gracias, rápido e hizo el trabajo por mí
Miguel Mota
1
En debian apt-get install libxml-xpath-perl.
tres.14159
funciona de maravilla
Alexandru-Mihai Manolescu
5

a partir de la respuesta de chad, aquí está la solución de trabajo COMPLETA para analizar UML, con manejo adecuado de comentarios, con solo 2 pequeñas funciones (más de 2 pero puede mezclarlas todas). No digo que el de Chad no funcionó en absoluto, pero tenía demasiados problemas con los archivos XML mal formateados: por lo tanto, debe ser un poco más complicado para manejar los comentarios y espacios mal ubicados / CR / TAB / etc.

El propósito de esta respuesta es proporcionar funciones de bash listas para usar, listas para usar, a cualquiera que necesite analizar UML sin herramientas complejas que usen perl, python o cualquier otra cosa. En cuanto a mí, no puedo instalar cpan, ni módulos perl para el antiguo sistema operativo de producción en el que estoy trabajando, y python no está disponible.

Primero, una definición de las palabras UML utilizadas en esta publicación:

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

EDITAR: funciones actualizadas, con identificador de:

  • Websphere xml (atributos xmi y xmlns)
  • debe tener un terminal compatible con 256 colores
  • 24 tonos de gris
  • compatibilidad añadida para IBM AIX bash 3.2.16 (1)

Las funciones, primero es xml_read_dom, que xml_read llama recursivamente:

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
}

y el 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
}

y, por último, las funciones rtrim, trim y 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; }

Coloración:

ah, y necesitará algunas variables dinámicas de colorización definidas al principio, y también exportadas:

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

Cómo cargar todas esas cosas:

Ya sabes cómo crear funciones y cargarlas a través de FPATH (ksh) o una emulación de FPATH (bash)

Si no, simplemente copie / pegue todo en la línea 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

Con el modo de depuración (-d), los comentarios y los atributos analizados se imprimen en stderr

carroñero
fuente
Estoy tratando de utilizar las dos anteriores funciones que produce los siguientes: ./read_xml.sh: line 22: (-1): substring expression < 0?
khmarbaise
Línea 22:[ "x${ATTRIBUTES:(-1):1}x" == "x?x" ] ...
khmarbaise
lo siento khmarbaise, estas son funciones bash shell. Si desea adaptarlos como scripts de shell, ¡ciertamente debe esperar algunas adaptaciones menores! También las funciones actualizadas manejan sus errores;)
scavenger
4

No conozco ninguna herramienta de análisis XML de shell puro. Por lo tanto, lo más probable es que necesite una herramienta escrita en otro idioma.

Mi módulo XML :: Twig Perl viene con una herramienta de este tipo: xml_grepdonde probablemente escribirías lo que quieras xml_grep -t '/html/head/title' xhtmlfile.xhtml > titleOfXHTMLPage.txt(la -topción te da el resultado como texto en lugar de xml)

mirod
fuente
4

Otra herramienta de línea de comando es mi nuevo Xidel . También es compatible con XPath 2 y XQuery, a diferencia del ya mencionado xpath / xmlstarlet.

El título se puede leer como:

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

Y también tiene una característica genial para exportar múltiples variables a bash. Por ejemplo

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

establece $titleel título y $imgcountla cantidad de imágenes en el archivo, que debería ser tan flexible como analizarlo directamente en bash.

BeniBela
fuente
¡Esto es exactamente lo que necesitaba! :)
Thomas Daugaard
2

Bueno, puedes usar la utilidad xpath. Supongo que XML :: Xpath de perl lo contiene.

alamar
fuente
2

Después de algunas investigaciones para la traducción entre los formatos Linux y Windows de las rutas de archivos en archivos XML, encontré tutoriales y soluciones interesantes sobre:

usuario485380
fuente
2

Si bien hay bastantes utilidades de consola listas para usar que pueden hacer lo que desea, probablemente tomará menos tiempo escribir un par de líneas de código en un lenguaje de programación de propósito general como Python, que puede ampliar y adaptar fácilmente tus necesidades.

Aquí hay un script de Python que usa lxml para analizar: toma el nombre de un archivo o una URL como primer parámetro, una expresión XPath como segundo parámetro e imprime las cadenas / nodos que coinciden con la expresión dada.

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

lxmlSe puede instalar con pip install lxml. En ubuntu puedes usar sudo apt install python-lxml.

Uso

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

lxml También acepta una URL como entrada:

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

Nota : Si su XML tiene un espacio de nombre predeterminado sin prefijo (p xmlns=http://abc.... Ej. ), Entonces debe usar el pprefijo (proporcionado por el 'pirateo') en sus expresiones, p. Ej. //p:modulePara obtener los módulos de un pom.xmlarchivo. En caso de que el pprefijo ya esté asignado en su XML, deberá modificar el script para usar otro prefijo.


Ejemplo 2

Una secuencia de comandos única que tiene el propósito limitado de extraer nombres de módulos de un archivo apache maven. Observe cómo el nombre del nodo ( module) tiene el prefijo con el espacio de nombres predeterminado{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
fuente
Esto es increíble cuando quieres evitar la instalación de paquetes adicionales o no tienes acceso. En una máquina de construcción, que pueda justificar un extra pip installmás apt-geto yumllamada. ¡Gracias!
E. Moffat
0

El método de Yuzem se puede mejorar invirtiendo el orden de los signos <y >en la rdomfunción y las asignaciones de variables, de modo que:

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

se convierte en:

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

Si el análisis no se realiza así, nunca se alcanza la última etiqueta en el archivo XML. Esto puede ser problemático si tiene la intención de generar otro archivo XML al final del whileciclo.

michaelmeyer
fuente
0

Esto funciona si desea 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
fuente
-1

Si bien parece que "nunca analizar XML, JSON ... desde bash sin una herramienta adecuada" es un buen consejo, no estoy de acuerdo. Si este es un trabajo secundario, es fácil buscar la herramienta adecuada y luego aprenderla ... Awk puede hacerlo en minutos. Mis programas tienen que trabajar con todos los datos mencionados anteriormente y más tipos de datos. Demonios, no quiero probar 30 herramientas para analizar 5-7-10 formatos diferentes que necesito si puedo resolver el problema en minutos. ¡No me importa XML, JSON o lo que sea! Necesito una solución única para todos ellos.

Como ejemplo: mi programa SmartHome ejecuta nuestros hogares. Mientras lo hace, lee una gran cantidad de datos en demasiados formatos diferentes que no puedo controlar. Nunca uso herramientas adecuadas y dedicadas, ya que no quiero pasar más de minutos leyendo los datos que necesito. Con ajustes FS y RS, esta solución awk funciona perfectamente para cualquier formato de texto. ¡Pero puede que no sea la respuesta adecuada cuando su tarea principal es trabajar principalmente con un montón de datos en ese formato!

El problema de analizar XML desde bash me enfrenté ayer. Así es como lo hago para cualquier formato de datos jerárquico. Como beneficio adicional: asigno datos directamente a las variables en un script bash.

Para facilitar la lectura, presentaré la solución por etapas. A partir de los datos de prueba de OP, creé un archivo: test.xml

Analizar dicho XML en bash y extraer los datos en 90 caracteres:

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

Normalmente uso una versión más legible ya que es más fácil de modificar en la vida real, ya que a menudo necesito probar de manera diferente:

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

No me importa cómo se llama el formato. Busco solo la solución más simple. En este caso particular, puedo ver a partir de los datos que newline es el separador de registros (RS) y <> delimit fields (FS). En mi caso original, tuve una indexación complicada de 6 valores dentro de dos registros, relacionándolos, encontrar cuándo existen los datos más campos (registros) pueden o no existir. Se necesitaron 4 líneas de awk para resolver el problema perfectamente. ¡Adapte la idea a cada necesidad antes de usarla!

La segunda parte simplemente ve que hay una cadena deseada en una línea (RS) y, de ser así, imprime los campos necesarios (FS). Lo anterior me tomó unos 30 segundos para copiar y adaptar el último comando que usé de esta manera (4 veces más). ¡Y eso es todo! Hecho en 90 caracteres.

Pero, siempre necesito obtener los datos perfectamente en variables en mi script. Primero pruebo las construcciones así:

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

En algunos casos, uso printf en lugar de print. Cuando veo que todo se ve bien, simplemente termino de asignar valores a las variables. Sé que muchos piensan que "eval" es "malvado", no es necesario comentar :) El truco funciona perfectamente en mis cuatro redes durante años. ¡Pero sigue aprendiendo si no entiendes por qué esto puede ser una mala práctica! Incluyendo asignaciones de variables bash y un amplio espacio, mi solución necesita 120 caracteres para hacer todo.

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
fuente