Parsear XML en Bash
En este artículo quiero compartir una de las tantas soluciones que pueden existir para poder obtener una lista de elementos publicados en XML y tratarlos en un script BASH.
En esta ocasión tenía la necesidad de poder leer las últimas noticias publicadas en otro sitio de noticias que administro, www.radiodogo.com, para luego poder incluirlos como un texto desplazado en la transmisión en vivo en el pié de la pantalla.
Como ya saben yo trato de utilizar todo en el Sistema Operativo Linux por varias razones. Si bien lo que está predominando como lenguaje multiplataforma es Python, en mi caso elegí bash por su gran sencillez y en este modesto proyecto en particular no necesitaba hacer mucho más que leer los titulares y presentarlos en un archivo de texto plano.
Pero sin más vueltas le paso a detallar los detalles de cómo hacerlo en modo genérico.
Índice de artículo
Tratar con xml en Bash
Inicialmente elegí xmllint
para tratar con xml en Bash, sin embargo, no ha sido la única solución como se mostrará luego.
Con el fin de hacer este artículo mas ameno e instructivo, voy a utilizar un archivo xml de ejemplo. Para hacer los mismos ejemplos, a modo de ejercicio, el archivo de ejemplo se encuentra en el enlace del menú de comida en xml.
Utilizando xmllint
La primera de las herramientas a la que recurrí es xmllint
. Esta herramienta permite parsear de forma sencilla archivos xml.
Formateando XML
Así, por ejemplo, lo primero que se debe hacer para tratar con un archivo xml en Bash, es formatearlo para que sea mas presentable al ojo humano. Para ello, simplemente se debe ejecutar la siguiente línea.
xmllint --format sample.xml
Esto será de gran ayuda para el caso en que el contenido del archivo xml no esté correctamente formateado o que simplemente se encuentre en una única línea.
Si además se quisiera guardar formateado, simplemente tienes que redirigir la salida, como se muestra debajo.
xmllint --format simple.xml > simple_con_formato.xml
Extraer información
Como ya expliqué, en mi caso, lo que mas me interesaba precisamente es extraer la información contenida en ese archivo xml. Para esto en particular, la opción mas interesante que ofrece esta herramienta es -xpath
. Así, para poder obtener todos los precios del archivo de ejemplo hay que ejecutar la siguiente instrucción,
xmllint --xpath "/breakfast_menu/food/price" simple.xml
Esto devuelve algo como lo que se muestra a continuación,
<price>$5.95</price><price>$7.95</price><price>$8.95</price><price>$4.50</price><price>$6.95</price>
Evidentemente esto no es exactamente lo que se necesita, ya que se quiere exactamente el contenido. La solución es recurrir a alguna de las soluciones que existen en bash para pode filtrar texto. Yo, en particular he recurrido a sed
. Así, la solución a lo que se busca podría ser algo como lo siguiente.
xmllint --xpath "/breakfast_menu/food/price" simple.xml | sed -E 's/<price>([^<]*)<\/price>/\1 /g'
Y definitivamente esto te devuelve el resultado que se esperaba.
$5.95 $7.95 $8.95 $4.50 $6.95
Un paso mas allá…
Y si se quisiera imprimir para cada uno de los alimentos del menú, por ejemplo, el nombre y el precio. La solución podría ser algo como lo siguiente.
#/bin/bash nombres=$(xmllint --xpath /breakfast_menu/food/name simple.xml | sed -E 's/<name>([^<]*)<\/name>/\1;/g') precios=$(xmllint --xpath /breakfast_menu/food/price simple.xml | sed -E 's/<price>([^<]*)<\/price>/\1;/g') IFS=';' read -ra nombres <<< "$nombres" IFS=';' read -ra precios <<< $precios for i in $(seq 0 $((${#nombres[@]} - 1))) do echo "$((i+1)).- ${nombres[$i]} -> ${precios[$i]}" done
Esto te arrojará un resultado como el que ves a continuación,
1.- Belgian Waffles -> $5.95 2.- Strawberry Belgian Waffles -> $7.95 3.- Berry-Berry Belgian Waffles -> $8.95 4.- French Toast -> $4.50 5.- Homestyle Breakfast -> $6.95
Como se ve en este script he utilizado diferentes recursos que se encontraran en bash.
Otras soluciones
Lo cierto es que no me terminaba de convencer la solución anterior y le estuve dando vueltas a otras soluciones. Y es que uno de los problemas que me encontraba eran los dichosos espacios en blanco. La solución la encontré en cu
t.
Así, utilizando cut
la solución queda mas limpia, o por lo menos esa es la impresión que yo me he llevado. Observar el siguiente script.
#!/bin/bash nombres=$(xmllint --xpath /breakfast_menu/food/name simple.xml | \ sed -E 's/<name>([^<]*)<\/name>/\1;/g') precios=$(xmllint --xpath /breakfast_menu/food/price simple.xml | \ sed -E 's/<price>([^<]*)<\/price>/\1;/g') items=$(echo ${nombres//[^;]} | wc -c) for ((i=1;i<$items;i++)) do echo "$i.- $(echo $nombres | cut -d';' -f $i) -> \ $(echo $precios | cut -d';' -f $i)" done
Aquí se debe observar la forma en la que cuento los elementos, que es a partir de los separadores. En este caso he utilizado punto y coma. Para ello, lo que hago es quitar cualquier cosa que no sea punto y coma, y luego contarlo,
items=$(echo ${nombres//[^;]} | wc -c)
Y la otra parte interesante es el uso de cut
. Donde he definido como delimitador, de nuevo, el punto y coma, he indico el campo que hay que extraer,
$(echo $nombres | cut -d';' -f $i)
Instalación
xmllint
se encuentra en los repositorios oficiales de Ubuntu, dentro del paquete libxml2-utils
. Así para instalarlo es tan sencillo como ejecutar la siguiente instrucción en tu terminal,
sudo apt install libxml2-utils
Otras opciones para tratar xml en Bash
Por supuesto que estas no son las únicas opciones, seguro que se pueden encontrar muchas mas. Por ejemplo, también se puede utilizar xmlstarlet
. Esta opción tiene algunas ventajas respecto a la solución anterior y es la que utilicé en mi script.
xmlstarlet sel -t -v '/breakfast_menu/food/name/text()' simple.xml
Y la solución con esta otra herramienta podría tener un aspecto como el que se ve a continuación,
#!/bin/bash nombres=$(xmlstarlet sel -t -v '/breakfast_menu/food/name/text()' simple.xml) precios=$(xmlstarlet sel -t -v '/breakfast_menu/food/price/text()' simple.xml) items=$(echo "$nombres" | wc -l) for ((i=1;i<=$items;i++)) do nombre=$(echo "$nombres" | head -n $i | tail -n +$i) precio=$(echo "$precios" | head -n $i | tail -n +$i) echo "$i.- $nombre -> $precio" done
Esta otra herramienta también está en los repositorios oficiales de Ubuntu. Para instalarla solo tienes que ejecutar la siguiente instrucción en un terminal,
sudo apt install xmlstarlet
El caso real
Como prometí, dejo a continuación el script que utilizo para poder extraer los titulares de último minuto de mi sitio web de noticias. El sitio actualiza las noticias de modo automático cada 15 minutos y las publica en el feed RSS. Es por esta razón que luego de generar el script lo agrego a un cron para que el parseo de los títulos se actualice en modo automático cada 15 minutos durante la transmisión del evento en vivo.
#!/bin/bash wget --quiet -O rss.xml https://radiodogo.com/feed/ titulos=$(xmlstarlet sel -t -v '/rss/channel/item/title/text()' rss.xml) titulares="Titulos " items=$(echo "$titulos" | wc -l) for ((i=1;i<=$items;i++)) do titulo=$(echo "$titulos" | head -n $i | tail -n +$i) titulares="$titulares - $titulo" done echo $titulares > titulos.txt
Para que se vea el resultado final y práctico, dejo a continuación la transmisión realizada en vivo con las útlimas noticias al pié de la pantalla que se actualizan cada 15 minutos.
Conclusión
Como se mostró en las líneas anteriores, y como de costumbre, existen diferentes opciones para afrontar un determinado problema y llegar a la misma solución. Algunas de estas soluciones son mas rebuscadas o complejas y otras son mas limpias. En particular, mi recomendación, es que siempre se elijan las soluciones sencillas, porque al final, habrá que mantener el código del script.
Trabajando desde el año 1990 en el mercado de la tecnología. Técnico en Electrónica. Administrador de Sistemas. Administrador de Redes. Técnico en telecomunicaciones. Técnico de plataforma satelital. Incursiono en el Software Libre desde mediados del 1997. Desde entonces utilicé varias distribuciones GNU/Linux comenzando con un RedHat 5.0
Formé parte del Core Team y miembro del grupo de desarrollo del Proyecto UTUTO.