Cómo analizar XML en Bash?

Idealmente, lo que me gustaría ser capaz de hacer es:

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

Esto es solo una explicación de la respuesta de Yuzem , pero no sentí que esta edición debería hacerse a otra persona, y los comentarios no permiten el formateo, así que …

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

Llamemos a "read_dom" en lugar de "rdom", espaciémoslo un poco y usemos 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 para esta función y la cambie a>. Esto significa que cuando lee datos en lugar de dividirse automáticamente en espacio, tabulación o 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 '<' (el -d para el indicador del delimitador). Lo que se lee se divide utilizando el IFS y se asigna a la variable ENTIDAD y CONTENIDO. Entonces tome lo siguiente:

 value 

La primera llamada a read_dom obtiene una cadena vacía (ya que '<' es el primer caracter). Eso se divide por IFS en solo '', ya que no hay un caracter '>'. Leer luego asigna una cadena vacía a ambas variables. La segunda llamada obtiene la cadena 'tag> value'. Eso se divide luego por el IFS en los dos campos 'etiqueta' y 'valor'. Leer luego asigna las variables como: ENTITY=tag y CONTENT=value . La tercera llamada obtiene la cadena '/ tag>'. Eso se divide por el IFS en los dos campos '/ tag' y ''. Leer luego asigna las variables como: ENTITY=/tag y 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 el anterior:

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

La primera línea simplemente 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 fue la entidad del título, el ciclo se repite en la sexta línea. Redirigimos "xhtmlfile.xhtml" a la entrada estándar (para la función read_dom ) y redirigimos la salida estándar a "titleOfXHTMLPage.txt" (el eco anterior en el ciclo).

Ahora dado lo siguiente (similar a lo que obtienes al listar un cubo en S3) para input.xml :

  sth-items false  item-apple-iso@2x.png 2011-07-25T22:23:04.000Z "0032a28286680abee71aed5d059c6a09" 1785 STANDARD   

y el siguiente ciclo:

 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 => item-apple-iso@2x.png /Key => LastModified => 2011-07-25T22:23:04.000Z /LastModified => ETag => "0032a28286680abee71aed5d059c6a09" /ETag => Size => 1785 /Size => StorageClass => STANDARD /StorageClass => /Contents => 

Entonces, si escribimos un ciclo while como el de Yuzem:

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

Obtendremos una lista de todos los archivos en el cubo S3.

EDITAR Si por alguna razón local IFS=\> no funciona para usted y lo establece 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 hagas más adelante en el script se perderá.

EDITAR 2 Para dividir los pares de nombre / valor de atributo, puede boost read_dom() como read_dom() :

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

Luego escribe tu función para analizar y obtener los datos que desees así:

 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_dom llama a parse_dom :

 while read_dom; do parse_dom done 

Luego, dado el siguiente ejemplo de marcado:

  bars content foos content  

Deberías obtener esta salida:

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

EDIT 3 otro usuario dijo que estaban teniendo problemas con FreeBSD y sugirió guardar el estado de salida de read 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

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

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

Ahora puede usar rdom like read pero para documentos html. Cuando se lo llame, se asignará el elemento a la variable E y el contenido a var 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 

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

  • 4xpath – Contenedor de línea de comandos alrededor del paquete 4Suite de Python
  • XMLStarlet
  • xpath – Contenedor de línea de comandos alrededor de la biblioteca XPath de Perl
  • Xidel – Funciona con URLs 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.

Puedes usar la utilidad xpath. Está instalado con el paquete Perl XML-XPath.

Uso:

 /usr/bin/xpath [filename] query 

o XMLStarlet . Para instalarlo en opensuse use:

 sudo zypper install xmlstarlet 

o pruebe cnf xml en otras plataformas.

Esto es suficiente…

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

Consulte XML2 desde http://www.ofb.net/~egnor/xml2/, que convierte XML a un formato orientado a líneas.

Otra herramienta de línea de comando es mi nuevo Xidel . También es compatible con XPath 2 y XQuery, al contrario 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 interesante para exportar múltiples variables a bash. Por ejemplo

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

establece $title para el título y $imgcount para el número de imágenes en el archivo, que debe ser tan flexible como analizarlo directamente en bash.

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

El objective de esta respuesta es proporcionar funciones ready-2-use, out-of-box bash a cualquier persona que necesite analizar UML sin herramientas complejas usando perl, python o cualquier otra cosa. En cuanto a mí, no puedo instalar módulos cpan ni perl para el sistema operativo de producción anterior en el que estoy trabajando y Python no está disponible.

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

  content... 

EDITAR: funciones actualizadas, con el mango de:

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

Las funciones, primero es el xml_read_dom que es llamado recursivamente por 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 } 

y el segundo:

 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>]  [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 (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; } 

Colorización:

Ah, y necesitará algunas variables dinámicas de color claras que se definirán primero y se exportarán también:

 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 todo eso:

O 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>]  [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  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

No conozco ninguna herramienta de análisis XML de shell puro. Entonces probablemente necesitará una herramienta escrita en otro idioma.

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

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

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

  • Información general sobre XPaths
  • Amara – colección de herramientas Pythonic para XML
  • Desarrollar Python / XML con 4Suite (2 partes)

Si bien existen bastantes utilidades de consola listas para usar que pueden hacer lo que usted desea, probablemente le tomará menos tiempo escribir un par de líneas de código en un lenguaje de progtwigción de propósito general como Python el cual puede extender fácilmente y adaptarse a tus necesidades.

Aquí hay una secuencia de comandos 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 # Eg 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 se 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 nombres predeterminado sin prefijo (por ejemplo, xmlns=http://abc... ), entonces tiene que usar el prefijo p (provisto por el ‘hack’) en sus expresiones, por ejemplo //p:module para obtener los módulos de un archivo pom.xml . En caso de que el prefijo p ya esté mapeado en su XML, deberá modificar la secuencia de comandos para usar otro prefijo.


Ejemplo 2

Una secuencia de comandos única que sirve para el propósito estrecho de extraer nombres de módulos de un archivo apache maven. Observe cómo el nombre de nodo ( module ) tiene como prefijo el espacio de nombre predeterminado {http://maven.apache.org/POM/4.0.0} :

pom.xml :

    cherries bananas pears   

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) 

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

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

se convierte en:

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

Si el análisis no se hace así, nunca se alcanzará 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 ciclo while.

Esto funciona si quieres atributos XML:

 $ cat alfa.xml  $ sed 's.[^ ]*..;s./>..' alfa.xml > alfa.sh $ . ./alfa.sh $ echo "$stream" H264_400.mp4