martedì 27 novembre 2007

Javascript: processare documenti XML

Javascript mette a disposizione una serie di oggetti e funzioni per processare e manipolare documenti XML. Possono tornare utili in diversi casi per cui penso sia interessante vedere alcuni esempi di codice.

Il documento XML che utilizzeremo negli esempi è compositori.xml (se cliccate il link dovreste visualizzarlo nel vostro browser).

Non è il caso di fare qui troppa teoria su XML, basterà dire quanto basta per comprendere il codice.

Un documento XML è organizzato gerarchicamente in una struttura ad albero costituita da un nodo radice che a sua volta contiene tutti gli altri nodi. Ogni nodo, ad eccezione della radice, ha un nodo padre e può contenere altri nodi, detti figli. I nodi con lo stesso nodo padre sono detti fratelli, mentre i figli dei figli di un nodo (così come i figli dei figli dei figli per potenzialmente infinite generazioni) sono detti discendenti del nodo. A ciascun nodo inoltre possono essere associate delle proprietà (attributi).

Parser XML

Prima di poter compiere una qualsiasi operazione su file XML è necessario ottenere un'istanza del parser XML. Per fare questo, come spesso succede, si è costretti a scrivere codice Javascript differenziato tra Internet Explorer e resto del mondo.

1) Internet Explorer

var xmlDoc=new ActiveXObject("Microsoft.XMLDOM");

2) Firefox, Mozilla, Opera e altri

var xmlDoc=document.implementation.createDocument("","",null);

Ottenuta una istanza del parser possiamo caricare il documento in questo modo

xmlDoc.async=false;
xmlDoc.load("compositori.xml");

Impostando async a false ci assicuriamo che lo script attenda il completo caricamento del documento XML prima di proseguire l'esecuzione.

Radice (root) del documento XML

L'elemento radice del documento può essere sempre ottenuto con

var root = xmlDoc.documentElement;

Tipi di nodo

L'oggetto radice, come tutti gli altri nodi, possiede tre proprietà fondamentali: nodeType, nodeName, nodeValue.

Se, facendo riferimento al nostro documento, scriviamo

document.write('Nome: ' + root.nodeName);
document.write('<br />');
document.write('Tipo: ' + root.nodeType);
document.write('<br />');
document.write('Valore: ' + root.nodeValue);

Otteniamo il seguente output

Nome: compositori
Tipo:1
Valore: null

che ci dice che il nodo radice del documento ha nome compositori ed è di tipo Elemento. La proprietà nodeType può avere infatti i seguenti valori

  • 1 - Elemento
  • 2 - Attributo
  • 3 - Testo
  • 8 - Commento
  • 9 - Documento

Il nodo non ha valore (nodeValue è null) come tutti i nodi di tipo Elemento.

Proprietà childNodes

La proprietà childNodes di un nodo di tipo Elemento restituisce la lista di tutti i sottoelementi o nodi figli.

Scorriamo i figli del nodo radice compositori

for (var i=0; i < root.childNodes.length;  i++)  {
  document.write('Nome: ' + root.childNodes[i].nodeName);
  document.write('<br />');
  document.write('Tipo: ' + root.childNodes[i].nodeType);
  document.write('<br />');
  document.write('Valore: ' + root.childNodes[i].nodeValue);
  document.write('<br /><br />');
}

Se eseguiamo il codice si ottiene

Nome: compositore
Tipo: 1
Valore: null

Nome: compositore
Tipo: 1
Valore: null

Nome: compositore
Tipo: 1
Valore: null

Infatti il nodo radice (compositori) ha tre nodi figli (compositore) tutti di tipo Elemento, basta un'occhiata alla struttura del documento per capire che tutto torna. C'è solo un problema. Quanto riportato sopra si ottiene eseguendo il codice in Internet Explorer. Con Firefox e gli altri browser lo stesso codice genera il seguente output

Nome: #text
Tipo: 3
Valore:

Nome: compositore
Tipo: 1
Valore: null

Nome: #text
Tipo: 3
Valore:

Nome: compositore
Tipo: 1
Valore: null

Nome: #text
Tipo: 3
Valore:

Nome: compositore
Tipo: 1
Valore: null

Nome: #text
Tipo: 3
Valore: 

Da dove vengono quei 4 nodi di tipo Testo in più? Sono gli a capo e i caratteri di tabulazione usati per separare e spaziare i tag relativi ai nodi figlio (<compositore>) rispetto al tag del nodo padre (<compositori>). In Internet Explorer vengono scartati automaticamente, negli altri browser vengono considerati nodi figlio di tipo Testo.

Un modo per ovviare al problema, e uniformare i risultati tra i diversi browser quando si utilizza Javascript per processare documenti XML, è scriversi una funzione personalizzata che, dato un nodo, ne restituisca i nodi figli di tipo Elemento escludendo gli altri tipi. La funzione potrebbe essere questa.

function childElements(node) {
  var elements = new Array();
  for (var i=0; i < node.childNodes.length;  i++)  {
    if(node.childNodes[i].nodeType == 1) {
      elements.push(node.childNodes[i]);
    }
  }
  return elements;
}

Proprietà nextSibling e previousSibling

Nodi con lo stesso nodo padre sono detti fratelli. Un oggetto nodo possiede due proprietà, previousSibling e nextSibling, che restituiscono rispettivamente il nodo fratello precedente e successivo, nell'ordine in cui i nodi appaiono nel documento XML.

Nel documento di esempio i nodi nome, data_nascita e data_morte del medesimo nodo compositore sono tra loro fratelli.

Otteniamo i nodi figli del primo nodo compositore utilizzando la funzione childElements() creata sopra. Poi proviamo a passare dal primo nodo al nodo fratello

var elements = childElements(root);
//elements[0] -> primo nodo compositore
var siblings = childElements(elements[0]);
document.write(siblings[0].nodeName);
document.write('<br />');
document.write(siblings[0].nextSibling.nodeName);

L'output in Internet Explorer è

nome
data_nascita

E tutto torna. Il primo figlio del primo nodo compositore è il nodo nome e il successivo fratello è il nodo data_nascita

Però lo stesso codice in Firefox e gli altri browser dà questo output

nome
#text

Il successivo nodo fratello di nome in questo caso è considerato un nodo Testo, i caratteri a capo e tabulazione che nel documento si trovano tra i tag </nome> e <data_nascita>.

E' chiaro quindi che se utilizziamo queste proprietà direttamente avremo sempre risultati differenti a seconda del browser in cui il nostro codice Javascript viene eseguito.

Possiamo di nuovo creare delle funzioni personalizzate

function nextSiblingElement(node) {
  var sibling = node.nextSibling;
  while(sibling && sibling.nodeType != 1) {
    sibling = sibling.nextSibling;
  }
  return sibling;
}

La funzione omologa, previousSiblingElement(), potete trovarla negli esempi di codice scaricabili dal sito.

Proprietà firstChild, lastChild

Queste due proprietà di un oggetto nodo restituiscono rispettivamente il primo e l'ultimo dei nodi figli. Anche in questo caso, come a questo punto è facile aspettarsi, i caratteri a capo e tabulazione presenti tra i nodi sono scartati automaticamente da Internet Explorer, ma considerati nodi di tipo Testo dagli altri browser.

Negli esempi di codice sono presenti due funzioni firstChildElement() e lastChildElement() che possono essere utilizzate per ovviare a queste discrepanze.

Metodo getElementsByTagName()

I nodi possono essere estratti per nome dal documento tramite il metodo getElementsByTagName()
node.getElementsByTagName("tag")

che restituisce una lista di nodi discendenti di node e aventi tag come valore della proprietà nodeName.

Esempio:

var nodes = root.getElementsByTagName('nome');
for (var i=0; i < nodes.length;  i++)  {
  document.write('Nome: ' + nodes[i].nodeName);
  /*...*/
}

Ritorna tutti i nodi con tag nome presenti nel documento in quanto abbiamo invocato il metodo sul nodo radice (compositori).

Notate che i nodi nome sono figli dei nodi compositore non del nodo radice. Perchè sia incluso nella lista restituita da getElementsByTagName() è sufficiente che un nodo sia discendente del nodo su cui il metodo è invocato.

Attributi dei nodi

Ad un nodo può essere associata una lista di attributi. Ogni attributo è un oggetto che ha un nome (proprietà name) e un valore (proprietà value).

Nel documento XML di esempio, epoca è un attributo di tutti i nodi compositore. La proprietà attributes di un nodo restituisce la lista di attributi di quel nodo. Si può accedere così alle proprietà di ogni attributo.

Esempio:

nodes = root.getElementsByTagName('compositore');
for (i=0; i < nodes.length; i++) {
  attrs = nodes[i].attributes;
  for(x=0; x < attrs.length; x++) {
    document.write(attrs[x].name + ' | ' + attrs[x].value);
    document.write("<br />");
  }
}

Output:

epoca | classicismo
epoca | romanticismo
epoca | romanticismo

Dato un nodo e il nome di un attributo, il metodo getAttribute() restituisce il valore dell'attributo.

Esempio:

nodes = root.getElementsByTagName('compositore');
for (i=0; i < nodes.length; i++) {
  document.write(nodes[i].getAttribute('epoca'));
  document.write("<br />");
}

Output:

classicismo
romanticismo
romanticismo

Accedere al contenuto dei nodi Testo

Supponiamo di voler estrarre dal documento un elenco delle date di nascita di tutti i compositori. Si potrebbe essere tentati di scrivere

nodes = root.getElementsByTagName('data_nascita');
for (i=0; i < nodes.length; i++) {
  document.write(nodes[i].nodeValue);
  document.write("<br />");
}

Viene intuitivo (almeno così è successo a me la prima volta che ho fatto qualcosa di questo genere) pensare al testo all'interno dei tag <data_nascita></data_nascita> come valore del nodo corrispondente. Ma non è così, infatti il risultato del codice sopra riportato è

null
null
null

Il codice corretto è

nodes = root.getElementsByTagName('data_nascita');
for (i=0; i < nodes.length; i++) {
  document.write(nodes[i].childNodes[0].nodeValue);
  document.write("<br />");
}

Che dà il risultato che ci aspettiamo

1756-01-27
1797-01-31
1770-12-17

La proprietà nodeValue di un nodo di tipo Elemento è sempre null. Il contenuto di un nodo di tipo Elemento è a sua volta un nodo (di tipo Testo in questo caso) ed è la proprietà nodeValue di questo nodo figlio (ottenuto tramite childNodes) che contiene il testo che ci interessa.

Un'altra cosa che conviene sottolineare è che childNodes restituisce sempre una lista di nodi per cui anche quando i figli siano solo uno si deve usare l'indice (zero).

Attraversare l'albero del documento

Abbiamo visto le funzioni principali per estrarre informazioni da un documento XML con Javascript. Per finire eccovi una funzione in qualche modo riassuntiva che utilizza molte delle funzioni e metodi visti (ma anche qualcuno che non abbiamo esaminato in dettaglio).

La funzione parte dalla radice del documento o da un altro nodo passato come argomento, scorre ricorsivamente tutto l'albero (o il sottoalbero dato dai soli discendenti del nodo ricevuto come argomento) e stampa una serie di informazioni sulle proprietà dei singoli nodi.

function readXML(node) {
  var cn = node.childNodes;
  var ct = node.childNodes.length;
  for (var i=0; i<ct; i++)  {
    document.write('---- Nodo ' + cn[i].nodeName + 
    ' ----<br />');
    var pn = cn[i].parentNode;
    document.write('Nodo Padre: ' + pn.nodeName);
    document.write("<br />");
    document.write('Tipo:' + cn[i].nodeType);
    document.write("<br />");
    document.write('Nome: ' + cn[i].nodeName);
    document.write("<br />");
    document.write('Valore: ' + cn[i].nodeValue);
    document.write("<br />");
    
    if(cn[i].nodeType == 1) {
      var attrs = cn[i].attributes;
      document.write("---- Attributi ----<br />");
      for(x=0; x < attrs.length; x++) {
        document.write(attrs[x].name + ' = ' + attrs[x].value);
        document.write("<br />");
      }
      document.write("<br />");
    }
    var ps = cn[i].previousSibling;
    if(ps) {
      document.write('Fratello Prec.: ' + ps.nodeName);
      document.write("<br />");
    }
    var ns = cn[i].nextSibling;
    if(ns) {
      document.write('Fratello Succ.: ' + ns.nodeName);
      document.write("<br />");
    }
    document.write('N. Figli: ' + cn[i].childNodes.length);
    document.write("<br /><br />");
    if(cn[i].childNodes.length > 0) {
      readXML(cn[i]);
    }
  }
}
Il codice, come tutti gli altri esempi dell'articolo, è anche incluso nel file js-xml.zip che potete scaricare dal sito.

6 commenti:

Giulio ha detto...

grazie della guida, UTILISSIMA e soprattutto completa!
Mi chiedevo come mai lo script finale, cioè quello che comprende tutti gli script descritti, non funziona in safari e chrome, bisogna forse aggiungere qualche altro parametro per fare riconoscere il file xml?
grazie

Massimo ha detto...

Sembra che in Chrome (e presumo che in Safari sia lo stesso) l'oggetto in XmlDoc non supporti il metodo load. Provo a trovare una soluzione quando ho un po' di tempo.

Claudio ha detto...

Massimo, so che è passato molto tempo da quando hai scritto l'articolo, ma volevo dirti che mi è stato molto utile e l'ho utilizzato per scrivere la mia tesina di maturità.
Metto il link nella bibliografia ^.^
Grazie ancora, ciaooo

Massimo ha detto...

Mi fa piacere che il post ti sia stato utile, grazie della citazione in bibliografia :)

Anonimo ha detto...

Ottima guida! Complimenti! E' stata utilissima!

(27 novembre 2007... mai troppo tardi per i complimenti...)

Massimo ha detto...

Grazie anche a te :)

Posta un commento

Nota. Solo i membri di questo blog possono postare un commento.