Comment analyser XML dans Bash?


Réponses:


153

C'est vraiment juste une explication de la réponse de Yuzem , mais je n'avais pas l'impression que tant de modifications devraient être faites à quelqu'un d'autre, et les commentaires ne permettent pas le formatage, alors ...

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

Appelons cela "read_dom" au lieu de "rdom", espacez-le un peu et utilisez des variables plus longues:

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

D'accord, il définit une fonction appelée read_dom. La première ligne rend IFS (le séparateur de champ d'entrée) local pour cette fonction et le change en>. Cela signifie que lorsque vous lisez des données au lieu d'être automatiquement divisées sur un espace, une tabulation ou des sauts de ligne, elles sont divisées sur «>». La ligne suivante dit de lire l'entrée de stdin, et au lieu de s'arrêter à une nouvelle ligne, arrêtez quand vous voyez un caractère «<» (le drapeau -d pour le déliminateur). Ce qui est lu est ensuite divisé en utilisant l'IFS et assigné à la variable ENTITY et CONTENT. Alors prenez ce qui suit:

<tag>value</tag>

Le premier appel pour read_domobtenir une chaîne vide (puisque le '<' est le premier caractère). Cela est divisé par IFS en simplement «», car il n'y a pas de caractère «>». Read attribue ensuite une chaîne vide aux deux variables. Le deuxième appel obtient la chaîne «tag> valeur». Cela est ensuite divisé par l'IFS dans les deux champs «tag» et «value». Read attribue ensuite les variables telles que: ENTITY=taget CONTENT=value. Le troisième appel obtient la chaîne «/ tag>». Cela est divisé par l'IFS dans les deux champs «/ tag» et «». Read attribue ensuite les variables comme: ENTITY=/tagetCONTENT= . Le quatrième appel renverra un statut différent de zéro car nous avons atteint la fin du fichier.

Maintenant, sa boucle while s'est un peu nettoyée pour correspondre à ce qui précède:

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

La première ligne dit simplement, "tandis que la fonction read_dom renvoie un statut zéro, procédez comme suit." La deuxième ligne vérifie si l'entité que nous venons de voir est "title". La ligne suivante fait écho au contenu de la balise. Les quatre lignes sortent. Si ce n'était pas l'entité de titre, la boucle se répète sur la sixième ligne. Nous redirigeons "xhtmlfile.xhtml" vers l'entrée standard (pour leread_dom fonction) et redirigeons la sortie standard vers "titleOfXHTMLPage.txt" (l'écho du début de la boucle).

Maintenant, étant donné ce qui suit (similaire à ce que vous obtenez en répertoriant un compartiment sur S3) pour 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>

et la boucle suivante:

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

Tu devrais obtenir:

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

Donc, si nous whileécrivons une boucle comme celle de Yuzem:

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

Nous obtiendrions une liste de tous les fichiers du compartiment S3.

EDIT Si, pour une raison quelconque, local IFS=\>ne fonctionne pas pour vous et que vous le définissez globalement, vous devez le réinitialiser à la fin de la fonction comme:

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

Sinon, tout fractionnement de ligne que vous ferez plus tard dans le script sera perturbé.

EDIT 2 Pour séparer les paires nom / valeur d'attribut, vous pouvez augmenter les éléments read_dom()comme:

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

Ensuite, écrivez votre fonction pour analyser et obtenir les données que vous voulez comme ceci:

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
}

Puis pendant que vous read_domappelez parse_dom:

while read_dom; do
    parse_dom
done

Ensuite, étant donné l'exemple de balisage suivant:

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

Vous devriez obtenir cette sortie:

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

EDIT 3, un autre utilisateur a déclaré avoir des problèmes avec FreeBSD et a suggéré de sauvegarder l'état de sortie de read et de le renvoyer à la fin de read_dom comme:

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

Je ne vois aucune raison pour laquelle cela ne devrait pas fonctionner


2
Si vous rendez IFS (le séparateur de champ d'entrée) global, vous devez le réinitialiser à sa valeur d'origine à la fin, j'ai modifié la réponse pour l'avoir. Sinon, tout autre fractionnement d'entrée que vous effectuez plus tard dans votre script sera perturbé. Je soupçonne que la raison pour laquelle local ne fonctionne pas pour vous est que soit vous utilisez bash dans un mode de compatibilité (comme votre shbang est #! / Bin / sh) soit c'est une ancienne version de bash.
chad

30
Ce n'est pas parce que vous pouvez écrire votre propre analyseur que vous devriez le faire.
Stephen Niedzielski

1
@chad cela dit certainement quelque chose sur le flux de travail / l'implémentation d'AWS que je recherchais une réponse à "bash xml" pour également récupérer le contenu d'un compartiment S3!
Alastair

2
@Alastair voir github.com/chad3814/s3scripts pour un ensemble de scripts bash que nous utilisons pour manipuler des objets S3
chad

5
Affecter IFS à une variable locale est fragile et pas nécessaire. Faites simplement:, IFS=\< read ...qui ne définira IFS que pour l'appel de lecture. (Notez que je n'approuve en aucun cas la pratique consistant à utiliser readpour analyser xml, et je pense que cela comporte de nombreux risques et devrait être évité.)
William Pursell

64

Vous pouvez le faire très facilement en utilisant uniquement bash. Il vous suffit d'ajouter cette fonction:

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

Vous pouvez maintenant utiliser rdom comme read mais pour les documents html. Lorsqu'il est appelé, rdom attribuera l'élément à la variable E et le contenu à la var C.

Par exemple, pour faire ce que vous vouliez faire:

while rdom; do
    if [[ $E = title ]]; then
        echo $C
        exit
    fi
done < xhtmlfile.xhtml > titleOfXHTMLPage.txt

Pouvez-vous développer? Je parie que c'est parfaitement clair pour vous ... et cela pourrait être une excellente réponse - si je pouvais dire ce que vous faisiez là-bas ... pouvez-vous le décomposer un peu plus, éventuellement générer un échantillon de sortie?
Alex Gray du

1
Cred to the original - ce one-liner est tellement élégant et incroyable.
maverick

1
super hack, mais j'ai dû utiliser des guillemets doubles comme echo "$ C" pour empêcher l'expansion du shell et une interprétation correcte des lignes de fin (dépend de l'encadrement)
user311174

8
L'analyse XML avec grep et awk n'est pas correcte . Cela peut être un compromis acceptable si les XML sont assez simples et que vous n'avez pas trop de temps, mais cela ne peut jamais être qualifié de bonne solution.
peterh - Réintégrer Monica le

59

Les outils de ligne de commande qui peuvent être appelés à partir de scripts shell incluent:

  • 4xpath - wrapper de ligne de commande autour du package 4Suite de Python
  • XMLStarlet
  • xpath - wrapper de ligne de commande autour de la bibliothèque XPath de Perl
  • Xidel - Fonctionne avec les URL ainsi que les fichiers. Fonctionne également avec JSON

J'utilise également xmllint et xsltproc avec de petits scripts de transformation XSL pour effectuer un traitement XML à partir de la ligne de commande ou dans des scripts shell.


2
Où puis-je télécharger «xpath» ou «4xpath»?
Opher

3
oui, un deuxième vote / demande - où télécharger ces outils, ou voulez-vous dire qu'il faut écrire manuellement un wrapper? Je préfère ne pas perdre de temps à faire ça, sauf si c'est nécessaire.
David

4
sudo apt-get install libxml-xpath-perl
Andrew Wagner

22

Vous pouvez utiliser l'utilitaire xpath. Il est installé avec le package Perl XML-XPath.

Usage:

/usr/bin/xpath [filename] query

ou XMLStarlet . Pour l'installer sur OpenSuse, utilisez:

sudo zypper install xmlstarlet

ou essayez cnf xmlsur d'autres plateformes.


5
Utiliser xml starlet est certainement une meilleure option que d'écrire son propre sérialiseur (comme suggéré dans les autres réponses).
Bruno von Paris

Sur de nombreux systèmes, celui xpathqui est préinstallé n'est pas adapté à une utilisation en tant que composant dans des scripts. Voir par exemple stackoverflow.com/questions/15461737/… pour une élaboration.
tripleee

2
Sur Ubuntu / Debianapt-get install xmlstarlet
rubo77



5

à partir de la réponse du chad, voici la solution de travail COMPLÈTE pour analyser UML, avec une gestion correcte des commentaires, avec seulement 2 petites fonctions (plus de 2 mais vous pouvez toutes les mélanger). Je ne dis pas que celui de chad n'a pas fonctionné du tout, mais il avait trop de problèmes avec les fichiers XML mal formatés: vous devez donc être un peu plus délicat pour gérer les commentaires et les espaces mal placés / CR / TAB / etc.

Le but de cette réponse est de donner des fonctions bash prêtes à l'emploi et prêtes à l'emploi à quiconque a besoin d'analyser UML sans outils complexes utilisant perl, python ou autre. Quant à moi, je ne peux pas installer cpan, ni les modules perl pour l'ancien système d'exploitation de production sur lequel je travaille, et python n'est pas disponible.

Tout d'abord, une définition des mots UML utilisés dans cet article:

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

EDIT: fonctions mises à jour, avec la poignée de:

  • Websphere xml (attributs xmi et xmlns)
  • doit avoir un terminal compatible avec 256 couleurs
  • 24 nuances de gris
  • compatibilité ajoutée pour IBM AIX bash 3.2.16 (1)

Les fonctions, le premier est le xml_read_dom qui est appelé récursivement par 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
}

et le second:

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
}

et enfin, les fonctions rtrim, trim et 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; }

Colorisation:

oh et vous aurez besoin de quelques variables dynamiques de colorisation soignées à définir dans un premier temps, et également exportées:

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

Comment charger tout ça:

Soit vous savez créer des fonctions et les charger via FPATH (ksh) ou une émulation de FPATH (bash)

Sinon, copiez / collez tout sur la ligne de commande.

Comment ça marche:

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

Avec le mode de débogage (-d), les commentaires et les attributs analysés sont imprimés sur stderr


J'essaye d'utiliser les deux fonctions ci-dessus qui produisent ce qui suit ./read_xml.sh: line 22: (-1): substring expression < 0:?
khmarbaise

Ligne 22:[ "x${ATTRIBUTES:(-1):1}x" == "x?x" ] ...
khmarbaise

désolé khmarbaise, ce sont des fonctions bash shell. Si vous souhaitez les adapter en tant que scripts shell, vous devez certainement vous attendre à quelques adaptations mineures! Les fonctions mises à jour gèrent également vos erreurs;)
scavenger

4

Je ne connais aucun outil d'analyse XML pure shell. Vous aurez donc probablement besoin d'un outil écrit dans une autre langue.

Mon module XML :: Twig Perl est livré avec un tel outil:, xml_grepoù vous écririez probablement ce que vous voulez xml_grep -t '/html/head/title' xhtmlfile.xhtml > titleOfXHTMLPage.txt(l' -toption vous donne le résultat sous forme de texte au lieu de xml)


4

Un autre outil de ligne de commande est mon nouveau Xidel . Il prend également en charge XPath 2 et XQuery, contrairement au xpath / xmlstarlet déjà mentionné.

Le titre peut être lu comme:

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

Et il a également une fonctionnalité intéressante pour exporter plusieurs variables vers bash. Par exemple

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

définit $titlele titre et $imgcountle nombre d'images dans le fichier, ce qui devrait être aussi flexible que de l'analyser directement dans bash.


C'est exactement ce dont j'avais besoin! :)
Thomas Daugaard

2

Eh bien, vous pouvez utiliser l'utilitaire xpath. Je suppose que XML :: Xpath de Perl le contient.


2

Après quelques recherches pour la traduction entre les formats Linux et Windows des chemins de fichiers dans les fichiers XML, j'ai trouvé des tutoriels et des solutions intéressants sur:


2

Bien qu'il existe de nombreux utilitaires de console prêts à l'emploi qui pourraient faire ce que vous voulez, il vous faudra probablement moins de temps pour écrire quelques lignes de code dans un langage de programmation à usage général tel que Python que vous pouvez facilement étendre et adapter à vos besoins.

Voici un script python qui utilise lxml pour l'analyse - il prend le nom d'un fichier ou d'une URL comme premier paramètre, une expression XPath comme deuxième paramètre, et imprime les chaînes / nœuds correspondant à l'expression donnée.

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

lxmlpeut être installé avec pip install lxml. Sur ubuntu, vous pouvez utiliser sudo apt install python-lxml.

Usage

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

lxml accepte également une URL comme entrée:

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

Remarque : Si votre XML a un espace de noms par défaut sans préfixe (par exemple xmlns=http://abc...), vous devez utiliser le ppréfixe (fourni par le 'hack') dans vos expressions, par exemple //p:modulepour obtenir les modules à partir d'un pom.xmlfichier. pSi le préfixe est déjà mappé dans votre XML, vous devrez alors modifier le script pour utiliser un autre préfixe.


Exemple 2

Un script unique qui sert le but étroit d'extraire les noms de module d'un fichier apache maven. Notez comment le nom du nœud ( module) est préfixé avec l'espace de noms par défaut{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)

C'est génial lorsque vous voulez éviter d'installer des packages supplémentaires ou que vous n'y avez pas accès. Sur une machine de construction, je peux justifier un supplément pip installsur apt-getou yumappel. Merci!
E. Moffat

0

La méthode de Yuzem peut être améliorée en inversant l'ordre des signes <et >dans la rdomfonction et les affectations de variables, de sorte que:

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

devient:

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

Si l'analyse n'est pas effectuée de cette manière, la dernière balise du fichier XML n'est jamais atteinte. Cela peut être problématique si vous avez l'intention de générer un autre fichier XML à la fin de la whileboucle.


0

Cela fonctionne si vous souhaitez des attributs 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

-1

Bien qu'il semble que "ne jamais analyser XML, JSON ... à partir de bash sans un outil approprié" est un conseil judicieux, je ne suis pas d'accord. Si c'est un travail secondaire, il est à la taille de chercher le bon outil, puis apprenez-le ... Awk peut le faire en quelques minutes. Mes programmes doivent fonctionner sur toutes les données mentionnées ci-dessus et sur d'autres types de données. Bon sang, je ne veux pas tester 30 outils pour analyser 5-7-10 formats différents dont j'ai besoin si je peux résoudre le problème en quelques minutes. Je me fiche de XML, JSON ou autre! J'ai besoin d'une solution unique pour tous.

À titre d'exemple: mon programme SmartHome gère nos maisons. Tout en le faisant, il lit une pléthore de données dans trop de formats différents que je ne peux pas contrôler. Je n'utilise jamais d'outils dédiés et appropriés car je ne veux pas passer plus de minutes à lire les données dont j'ai besoin. Avec les ajustements FS et RS, cette solution awk fonctionne parfaitement pour tout format textuel. Mais ce n'est peut-être pas la bonne réponse lorsque votre tâche principale est de travailler principalement avec des tonnes de données dans ce format!

Le problème de l'analyse XML de bash auquel j'ai été confronté hier. Voici comment je le fais pour tout format de données hiérarchique. En prime, j'attribue des données directement aux variables dans un script bash.

Pour faciliter la lecture des minces, je présenterai la solution par étapes. À partir des données de test OP, j'ai créé un fichier: test.xml

Analyser ledit XML en bash et extraire les données en 90 caractères:

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

J'utilise normalement une version plus lisible car elle est plus facile à modifier dans la vraie vie car j'ai souvent besoin de tester différemment:

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

Peu m'importe comment le format est appelé. Je ne cherche que la solution la plus simple. Dans ce cas particulier, je peux voir à partir des données que la nouvelle ligne est le séparateur d'enregistrement (RS) et les champs de délimitation <> (FS). Dans mon cas d'origine, j'avais une indexation compliquée de 6 valeurs dans deux enregistrements, les reliant, trouver quand les données existent et les champs (enregistrements) peuvent ou non exister. Il a fallu 4 lignes de awk pour résoudre parfaitement le problème. Alors, adaptez l'idée à chaque besoin avant de l'utiliser!

La deuxième partie regarde simplement s'il y a une chaîne voulue dans une ligne (RS) et si c'est le cas, imprime les champs nécessaires (FS). Ce qui précède m'a pris environ 30 secondes pour copier et adapter la dernière commande que j'ai utilisée de cette façon (4 fois plus longtemps). Et c'est tout! Fait en 90 caractères.

Mais, j'ai toujours besoin d'intégrer parfaitement les données dans les variables de mon script. Je teste d'abord les constructions comme ceci:

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

Dans certains cas, j'utilise printf au lieu de print. Quand je vois que tout va bien, je finis simplement d'attribuer des valeurs aux variables. Je sais que beaucoup pensent que "eval" est "diabolique", pas besoin de commenter :) Trick fonctionne parfaitement sur mes quatre réseaux depuis des années. Mais continuez à apprendre si vous ne comprenez pas pourquoi cela peut être une mauvaise pratique! Y compris les affectations de variables bash et un espacement suffisant, ma solution a besoin de 120 caractères pour tout faire.

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"
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.