INaLCO - M2 Traductique
Introduction à XML

Jean-François Perrot 

DOM (Document Object Model) - 1 : Lecture

Version en PHP

  1. Principe de DOM

  2. La triade DOMDocument, DOMElement, DOMText
    1. Le document XML, sa totalité, ses parties
    2. L'objet DOMDocument.
    3. Traitement d'un objet DOMElement.
    4. Traitement d'un objet DOMText.
    5. Intégration

  3. Savoir à qui on s'adresse
    1. Le genre DOMNode
    2. Problème souvent rencontré
    3. Un autre mode d'accès : getElementsByTagName(un-nom)

  4. Exemples
    1. Exploitation d'un fichier de noms & notes (représentation par attributs)
    2. Même chose en représentation par enfants

  1. Principe de DOM

    1. DOM est un système pour représenter en mémoire centrale le contenu d'un fichier XML.
      Dans cette perpective, un contenu XML est vu comme une totalité immédiatement accessible.
      En termes informatiques, c'est un objet.
      DOM offre le moyen
      • de construire (en mémoire) l'objet correspondant à un fichier XML (sur disque)
        par une opération d'analyse syntaxique (parsing) effectuée par un parseur.
      • de manipuler par programme un tel objet XML en mémoire
        • lecture
        • modification
        • construction
      • d'écrire dans un fichier le texte XML correpondant à un objet en mémoire.

    2. DOM offre un cadre abstrait pour décrire et effectuer les opérations ci-dessus.
      Ce cadre se réalise dans divers langages de programmation, en utilisant de manière essentielle la notion informatique d'objet
      qui est à peu près la même dans tous les langages.
      La manière de programmer avec DOM est donc grosso modo la même en Java, en C++, en PHP-5 et en Perl.

      Dans ce cours nous utiliserons PHP-5.
      Pour le même exposé avec Perl, voir ici.

    3. Du point de vue de l'utilisateur, DOM se présente comme la technique fondamentale pour toute espèce de travail sur des documents XML.
      D'autres techniques existent, mieux adaptées à différentes applications (XPath, XSLT notamment).
      DOM permet de traiter tous les problèmes, en adoptant un point de vue global.
      Une seule contre-indication : les très gros fichiers, qui conduisent à une occupation excessive de la mémoire centrale.



  2. La triade DOMDocument, DOMElement, DOMText

    1. Le document XML, sa totalité, ses parties

      • L'objet associé à un fichier XML est unique mais complexe.
        Dans la terminologie DOM on l'appelle un DOMDocument,
        au sens de cet objet appartient à l'espèce DOMDocument.

      • Les multiples parties de cet objet sont aussi des objets, qui appartiennent à plusieurs espèces différentes.
        On peut connaître l'espèce d'un objet en lui demandant son nodeType (valeur : un nombre).
        On peut aussi lui demander son nodeName, qui est plus explicite - mais il faut savoir l'interpréter !

        En plus de DOMDocument, deux de ces espèces méritent en priorité notre attention : DOMElement et DOMText.

        1. DOMDocument
          Pour un objet de l'espèce DOMDocument, le nodeType vaut 9 et le nodeName est la chaîne constante #document.
          Le DOMDocument contient un DOMElement particulier qui est la racine de l'arbre XML.
          Il possède aussi d'autres appendices, comme
          • l'instruction de traitement qui invoque une feuille de style (nodeType = 7, nodeName = xml-stylesheet)
          • la DTD (nodeType = 10, nodeName = le nom de la balise-racine)

        2. DOMElement
          C'est l'espèce des sommets de l'arbre qui correspondent aux balises dans le texte XML.
          Le nodeType correspondant vaut 1, et le nodeName est le nom de la balise.
          Un DOMElement possède un nom (tagName, celui de la balise), des attributs et des enfants.
          Le DOMDocument contient un DOMElement particulier qui est la racine de l'arbre XML.

        3. DOMText 
          C'est l'espèce des objets qui représentent les contenus textuels qui apparaissent entre les balises.
          Le nodeType correspondant vaut 1.
          Dans le système DOM, ces contenus sont représentés comme des objets à part entière.
          Un DOMText sera toujours enfant d'un DOMElement, tandis que lui-même n'aura ni attribut ni enfant,
          mais on pourra lui demander de livrer la chaîne de caractères qu'il contient.

      Exemple : Anatomie du document associé au fichier Voiture.xml

      <?xml version="1.0"?>
      <?xml-stylesheet type="text/css" href="Voiture.css"?>
      <!DOCTYPE Voiture SYSTEM "Voiture.dtd" >
      <Voiture marque="Renault" modèle="Safrane">
        <Carosserie couleur="rouge">
          <Capot>Un peu cabossé</Capot>
        </Carosserie>
        <Moteur>
          <Cylindres />
          <Allumage>Défectueux</Allumage>
        </Moteur>
        <Transmission type="automatique" nb_vitesses="5">
          <Boîte />
          <TrainAV />
          <TrainAR />
        </Transmission>
      </Voiture>


      Voiture

      Il y a un seul DOMDocument, 10 DOMElements et 2 DOMTexts.
      Du moins c'est ce que nous croyons à la lecture du fichier.
      Comme on verra plus loin, la réalité est un peu différente...
    2. L'objet DOMDocument.

      • En PHP, on commence par créer un DOMDocument vide, et on y charge le contenu d'un fichier :
            $doc = new DOMDocument();
          $doc->load("Voiture.xml");

        N.B. Si on veut charger depuis une chaîne de caractères, il faut employer loadXML(la_chaîne).

        Essai : le code PHP et le résultat de l'exécution

        echo "$doc->nodeType:$doc->nodeName\n";
        //
        nodeType et nodeName sont en PHP des attributs, on les interroge donc sans parenthèses

        $top = $doc->childNodes; // idem
        foreach ( $top as $enf ){
           echo "$enf->nodeType:$enf->nodeName - ";
        }

        9:#document
        7:xml-stylesheet - 10:Voiture - 1:Voiture -



      • N.B la méthode getDocumentElement() qui livre l'Element-racine n'existe pas en PHP.
        L'instruction Perl
        my $voiture = $doc->getDocumentElement();
        se traduit (de manière moins explicite) par
        $voiture = $doc->lastChild;
        (car les autres appendices du Document sont placés avant l'arbre XML).

        Essai : le code PHP et le résultat de l'exécution

        echo "$voiture->nodeType:$voiture->nodeName\n";

        1:Voiture



    3. Traitement d'un objet DOMElement.

      • Accès aux valeurs de ses attributs, connaissant leurs noms : getAttribute(le-nom)
        $marque = $voiture->getAttribute("marque");
        $modele = $voiture->
        getAttribute("modele");

        Attention !  "modele", pas comme en Perl "mod\x{00E8}le"
        PHP-5 n'a toujours pas adopté Unicode.  On attend toujours la version 6 qui devrait le faire...
        Pour éviter d'abominables casse-têtes, on fera l'hypothèse que les éléments structurels de nos documents sont en ASCII.

        Essai : le code PHP et le résultat de l'exécution

        echo "$marque $modele\n";

        Renault Safrane


      • Accès à la liste de ses enfants par l'attribut childNodes
        $cmt = $voiture->childNodes;
        foreach ( $cmt as
        $enf ){
           echo "$enf->nodeType:$enf->nodeName - ";
        }


      • Attention ! l'attribut childNodes nous donne tous les enfants du DOMElement concerné.
        Notamment tous les contenus textuels qu'on y a introduits pour rendre le fichier lisible :
        les indentations, les blancs et les sauts de ligne sont considérés par DOM comme des textes !!!!...
        Dans notre fichier-exemple, chaque DOMElement est entouré par deux objets DOMText.
        La boucle ci-dessus donne :
        3:#text - 1:Carosserie - 3:#text - 1:Moteur - 3:#text - 1:Transmission - 3:#text -

    4. Traitement d'un objet DOMText.

      • Attention ! Un objet DOMText n'est pas une chaîne de caractères !
        Un DOMText est une "boîte" qui contient une chaîne.
        C'est cette boite qu'on obtient comme enfant de la balise...

      • Il faut lui demander son attribut wholeText pour avoir la chaîne elle-même.

      • Cas d'une "feuille" : un DOMElement ne contenant que du texte (comme Capot et Allumage dans l'exemple ci-dessus)
        L'objet DOMText est alors son unique enfant, on l'obtient directement par l'attribut firstChild.
        $carosserie = $cmt->item(1); // après le #text dû à l'indentation

        Attention ! $cmt est une DOMNodeList, pas un banal tableau.
        On peut lui appliquer la boucle foreach, mais non pas l'indexer directement par $cmt[i]
        il faut dire à la place $cmt->item(1).

        $caps = $carosserie->childNodes;
        foreach ( $caps as $enf ){
           echo "$enf->nodeType:$enf->nodeName - ";
        }

        3:#text - 1:Capot - 3:#text -


        $capot = $caps->item(1); // idem
        $txt = $capot->firstChild; // capot est une feuille
        echo "$txt->nodeType:$txt->nodeName: \"$txt->wholeText\"\n";

        3:#text: "Un peu cabossé"

    5. Intégration

      Mise en œuvre minimale de tout ce que nous venons de voir : fichier voiture1.php

      <?php

      $doc = new DOMDocument();
      $doc->load("Voiture.xml");
      echo "$doc->nodeType:$doc->nodeName\n";
      $top = $doc->childNodes;
      foreach ( $top as $enf ){
         echo "$enf->nodeType:$enf->nodeName - ";
      }
      echo "\n";

      $voiture = $doc->lastChild;
      echo "$voiture->nodeType:$voiture->nodeName\n";

      $marque = $voiture->getAttribute("marque");
      $modele = $voiture->getAttribute("modele");
      echo "$marque $modele\n";

      $cmt = $voiture->childNodes;
      foreach ( $cmt as $enf ){
         echo "$enf->nodeType:$enf->nodeName - ";
      }
      echo "\n";

      $carosserie = $cmt->item(1);
      $caps = $carosserie->childNodes;
      foreach ( $caps as $enf ){
         echo "$enf->nodeType:$enf->nodeName - ";
      }
      echo "\n";

      $capot = $caps->item(1); // idem
      $txt = $capot->firstChild; // capot est une feuille
      echo "$txt->nodeType:$txt->nodeName: \"$txt->wholeText\"\n";

      ?>


      Exécution :
      jfp% php voiture1.php
      9:#document
      7:xml-stylesheet - 10:Voiture - 1:Voiture -
      1:Voiture
      Renault Safrane
      3:#text - 1:Carosserie - 3:#text - 1:Moteur - 3:#text - 1:Transmission - 3:#text -
      3:#text - 1:Capot - 3:#text -
      3:#text: "Un peu cabossé"
      jfp%


  3. Savoir à qui on s'adresse

    1. Le genre DOMNode

      Les trois espèces d'objets DOMDocument, DOMElement et DOMText appartiennent toutes au même genre, appelé DOMNode.
      (Le mot genre est pris ici au sens de la classfication linnéenne des êtres vivants, non au sens de la sémantique textuelle :
      famille > genre > espèce. Par exemple Orchidées [ Orchidaceae] > Dactylorhiza > Orchis tacheté [Dactylorhiza maculata].)

      Il faut bien distinguer, dans les comportements de nos objets, ceux qui sont communs à tout le genre DOMNode
      de ceux relèvent de leur espèce particulière DOMDocument, DOMElement ou DOMText.

      Les attributs que nous avons vus jusqu'ici :
      • nodeType (valeur  : un nombre entier), 
      • nodeName (valeur  : chaîne) 
      • firstChild, lastChild (valeur : un objet de genre DOMNode)
      • childNodes (valeur une DOMNodeList, sorte de tableau d'objets qui sont tous du genre DOMNode) , 
      appartiennent tous au genre DOMNode.

      En revanche,
      • l'attribut wholeText est spécifique à DOMText
      • la méthode (fonction) getAttribute() est spécifique à DOMElement.

      une liste de d'objets Node , contenant aussi bien des Element que des Text.
    2. Or, on ne peut pas s'adresser de la même manière à un Element et à un Text !
      On ne peut pas demander wholeTetxt à un Element, ni getAttribute(...) à un Text...
      Comment savoir à qui on s'adresse ?

    3. Problème souvent rencontré

      On veut parcourir la liste des enfants (childNodes) d'un DOMElement, et traiter chacun selon son espèce.
      A priori, on sait seulement que chacun est un DOMNode, il faut donc lui demander son nodeType pour savoir quoi faire...

      Voici un exemple de parcours récursif d'un DOMDocument (fichier parcours.php).

      <?php

      // un parcours générique

      function parcours($nd){ // l'argument $nd est un DOMNode
         
          switch( $nd->nodeType ){
              case 9 : { //DOMDocument
                  echo "Le document\n";
                  foreach( $nd->childNodes as $n ){
                      parcours($n);
                  }
                  break;
              }
             
              case 7 ; { // DOMProcessingInstruction (feuille de style)
                  echo "La feuille de style\n";
                  break;
              }
                 
              case 10 : { // DOMDocumentType (DTD)
                  echo "La DTD, de racine $nd->name, chemin : $nd->systemId\n";
                  break;
              }
                 
              case 1 : { // DOMElement
                  echo "$nd->tagName";
                  foreach( $nd->attributes as $na => $vala ){
                      echo " $na=\"$vala->value\"";
                  }
                  echo "\n";
                  foreach( $nd->childNodes as $n ){
                      parcours($n);
                  }
                  break;
              }
             
              case 3 : { // texte : on n'imprime pas les textes "parasites" à contenu blanc
                  $chn = $nd->wholeText;
                  if( !preg_match('/^\s+$/', $chn) ){
                      echo "Texte : \"$chn \"\n";
                  }
                  break;
              }
             
              default : echo "Objet de type inconnu : $nd->nodeType\n";
          }
      }//parcours

      $doc = new DOMDocument();
      $doc->load("Voiture/Voiture.xml");
      parcours($doc);

      ?>


      jfp% php parcours.php
      Le document
      La feuille de style
      La DTD, de racine Voiture, chemin : Voiture.dtd
      Voiture marque="Renault" modele="Safrane"
      Carosserie couleur="rouge"
      Capot
      Texte : "Un peu cabossé "
      Moteur
      Cylindres
      Allumage
      Texte : "Défectueux "
      Transmission type="automatique" nb_vitesses="5"
      Boite
      TrainAV
      TrainAR


    4. Un autre mode d'accès : getElementsByTagName(un-nom)

      Si la structure du document XML est connue, ce qui est fréquent (les DTDs sont là pour ça !),
      on peut employer une autre méthode pour accéder à coup sûr aux bons Elements :
      demandé à un Element ou à un Document, getElementsByTagName(un-nom) va renvoyer
      • la liste de tous ses descendants (directs et indirects)
      • qui sont des DOMElements et portent le nom passé en paramètre
      • dans l'ordre de parcours "préordre" (de gauche à droite, en profondeur).

      Si, comme le cas se présente souvent, les DOMElements de même nom sont tous des successeurs d'un même sommet,
      cette méthode est bien adaptée. Nous en verrons des exemples plus loin.

      Ce mode d'accès est excellent lorsqu'on connaît la structure (DTD) du document XML,
      et que cette structure est bien adaptée au but poursuivi. Nous allons l'illustrer dans une famille d'exemples.
      Dans le cas contraire, il faudra revenir à getChildNodes, et à un parcours attentif à la nature des enfants,
      du genre de celui du paragraphe précédent.
  4. Exemples

    Utilisation de getElementsByTagName(...) sur des documents bien structurés.

    1. Exploitation d'un fichier de noms & notes (représentation par attributs)

      Exemple : fichier Ex1.xml

      <?xml version="1.0" encoding="UTF-8"?>
      <liste>
       <eleve nom="Hélène" note="16"/>
       <eleve nom="Pierre" note="12"/>
       <eleve nom="Joëlle" note="15"/>
       <eleve nom="Paul" note="13"/>
       <eleve nom="Françoise" note="19"/>
       <eleve nom="Jacques" note="17"/>
      </liste>


      On lit le document en imprimant les noms & notes, et en même temps on calcule la moyenne
      fichier Lire_1.php

      <?php
      /* Lire des Noms-Notes en format 1 */

      function lire_1($doc){

          $depart = $doc->lastChild;
         
          $k = 0; //nombre de notes
          $s = 0; //le total
          $les_eleves = $depart->getElementsByTagName("eleve");
          foreach( $les_eleves as $l_eleve ){ // foreach marche avec NodeList !
              $le_nom = $l_eleve->getAttribute("nom");
              $la_note = $l_eleve->getAttribute("note");
              
              echo "$le_nom a pour note $la_note\n";
              $s += $la_note;
              $k++;
          }
          if( $k == 0 ){
              die("fichier vide");
          }else{
              return $s/$k;
          }
      }// lire_1
         
      function lecture ($fichIn){
          $doc = new DOMDocument();
          $doc->load($fichIn);
          $moy = lire_1($doc);
          echo "\nMoyenne : $moy\n";
      }//lecture

      lecture("Ex1.xml");
      ?> 


      Exécution :
      jfp% php Lire_1.php
      Hélène a pour note 16
      Pierre a pour note 12
      Joëlle a pour note 15
      Paul a pour note 13
      Françoise a pour note 19
      Jacques a pour note 17

      Moyenne : 15.3333333333


    2. Même chose en représentation par enfants

      Exemple : fichier Ex2.xml
      <?xml version="1.0" ?>
      <liste>
         <eleve>
          <nom> Toto</nom> <note> 12 </note>
         </eleve>
         <eleve>
          <nom> Tata</nom> <note> 13 </note>
         </eleve>
         <eleve>
          <nom> Tutu</nom> <note> 17 </note>
         </eleve>
      </liste>


      Le programme est le même que ci-dessus, sauf la phase de "lecture" qui devient plus compliquée.
      On note au passage que le nombre d'élèves peut être connu directement (c'est la longueur de la liste)
      au lieu d'être calculé au cours de la boucle.
      Mettons cette remarque en pratique...

      fichier Lire_2.php

      <?php
      /* Noms-Notes en format 2 */

      function lire_2($doc){

          $depart = $doc->lastChild;
         
          $k = 0; //nombre de notes
          $s = 0; //le total
          $les_eleves = $depart->getElementsByTagName("eleve");
          foreach( $les_eleves as $l_eleve ){
              $elt_nom = $l_eleve->getElementsByTagName("nom")->item(0);
              $elt_note = $l_eleve->getElementsByTagName("note")->item(0);
              $le_nom = $elt_nom->firstChild->wholeText;
              $la_note = $elt_note->firstChild->wholeText;
             
              echo "$le_nom a pour note $la_note\n";
              $s += $la_note;
              $k++;
          }
          if( $k == 0 ){
              die("fichier vide");
          }else{
              return $s/$k;
          }
      }// lire_1
         
      function lecture ($fichIn){
          $doc = new DOMDocument();
          $doc->load($fichIn);
          $moy = lire_2($doc);
          echo "\nMoyenne : $moy\n";
      }//lecture

      lecture("Ex2.xml");
      ?>
         


      Exécution : pourquoi les noms sont-ils "indentés" ?
      jfp% php Lire_2.php Ex2.xml
       Toto a pour note  12
       Tata a pour note  13
       Tutu a pour note  17

      Moyenne : 14
      jfp%