I. L'article original

Qt Quarterly est une revue trimestrielle électronique proposée par Qt à destination des développeurs et utilisateurs de Qt. Vous pouvez trouver les versions originales.

Nokia, Qt, Qt Quarterly et leurs logos sont des marques déposées de Nokia Corporation en Finlande et/ou dans les autres pays. Les autres marques déposées sont détenues par leurs propriétaires respectifs.

Cet article est la traduction de l'article Checking the Weather with XQuery de David Boddie paru dans la Qt Quarterly Issue 33.

Cet article est une traduction de l'un des tutoriels en anglais écrits par Nokia Corporation and/or its subsidiary(-ies), inclus dans la documentation de Qt. Les éventuels problèmes résultant d'une mauvaise traduction ne sont pas imputables à Nokia.

II. Introduction

Dans l'édition 25 du magazine Qt Quarterly, nous avons regardé le module QtXmlPatterns, une technologie pour traiter des documents XML en utilisant le langage XQuery. L'article « Requêtes sérieuses avec XQuery » était principalement un exercice de l'utilisation de requêtes pour transformer un document XML en texte brut ou en une série d'éléments.

Tandis que ceci peut être utile à des applications où le XML doit être transformé pour affichage dans une page Web, cela l'est encore plus pour prendre le résultat d'une requête et en extraire les valeurs pour affichage dans une interface utilisateur simple. Une manière de le faire est d'écrire une requête qui produit une sortie qui peut être interprétée en tant que texte brut.

Bien que ceci puisse être la manière la plus facile de récupérer des données d'un document XML, il apparaît que l'on perd parfois la structure du document. En même temps, lors de l'utilisation de requête comme suit, nous ne faisons jamais que de l'extraction d'informations d'un extrait de fichier XML - n'est-ce pas là ce que QtXmlPatterns est censé faire à notre place ?

Dans cet article, nous examinerons un document XML et jetterons un coup d'œil aux différentes manières d'utiliser des requêtes pour en extraire des informations.

III. Le document

Nous avons obtenu quelques informations sur la météo du Norwegian Meteorological Institute dans un format XML. Le document contient une collection de bulletins météo pour des périodes précises sur quelques jours. Un extrait de ce fichier en montre la structure générale :

 
Sélectionnez
    <weatherdata>
      ...
      <sun rise="2010-03-17T06:24:09" set="2010-03-17T18:23:22" />
      <forecast>
        <tabular>
          <time from="2010-03-17T07:00:00" to="2010-03-17T13:00:00" period="1">
            <!-- Valid from 2010-03-17T07:00:00 to 2010-03-17T13:00:00 -->
            <symbol number="2" name="Fair" />
            <precipitation value="0.0" />
            <!-- Valid at 2010-03-17T07:00:00 -->
            <windDirection deg="113.7" code="ESE" name="East-southeast" />
            <windSpeed mps="1.7" name="Light breeze" />
            <temperature unit="celcius" value="2" />
            <pressure unit="hPa" value="1029.8" />
          </time>
          ...
        </tabular>
      </forecast>
    </weatherdata>

À creuser dans le XML, on trouve moult informations mais nous sommes principalement intéressés par le temps, les températures et les symboles. Nous regarderons l'élément <sun> et ses attributs, les éléments <time>, leurs enfants et leurs attributs.

IV. Créer et exécuter une requête

Le langage XQuery est assez différent des langages impératifs comme le C++, un peu de réflexion est parfois requise pour formuler les requêtes qui vont donner les résultats voulus. La requête suivante extrait les noms de chaque élément du document forecast.xml :

 
Sélectionnez
doc("forecast.xml")//*/fn:node-name(.)

Une brève explication est requise. La première partie de la requête fait référence au document obtenu du fichier forecast.xml :

 
Sélectionnez
doc("forecast.xml")

Le double slash indique que nous nous intéressons aux éléments situés n'importe où dans le document, l'astérisque, que tout élément convient à notre but :

 
Sélectionnez
doc("forecast.xml")//*

Nous voulons connaître le nom de chaque élément, nous invoquons donc la fonction de nom de nœud sur le nœud context, ce qui s'indique par le biais du caractère point :

 
Sélectionnez
doc("forecast.xml")//*/fn:node-name(.)

L'une des méthodes les plus simples d'exécution de requête est l'outil xmlpatterns, habituellement livré dans toute installation standard de Qt. Il se lance avec le nom d'un fichier contenant une requête. À la ligne de commande, nous entrons le répertoire contenant les fichiers nodenames.xq et forecast.xml et tapons ceci :

 
Sélectionnez
xmlpatterns nodenames.xq

La sortie est une longue liste de noms, séparés par des espaces, que nous n'affichons pas ici en entier pour des raisons de lisibilité :

 
Sélectionnez
weatherdata location name type country timezone location credit link links
link link link link link meta lastupdate nextupdate sun forecast tabular time
symbol precipitation windDirection windSpeed temperature pressure

Il est important de noter que, pour cette requête, le fichier XML ainsi que le fichier de requête XQuery doivent résider dans le même répertoire. Quand la requête est exécutée, les références au fichier forecast.xml sont interprétées comme des URI relatives.

Une autre méthode pour traiter cette requête est la classe QXmlQuery. L'exemple textquery, fourni avec cet article, est un court programme qui exécute la même requête en C++ et en affiche le résultat, en une seule fonction main() :

 
Sélectionnez
  int main(int argc, char *argv[])
  {
      QCoreApplication app(argc, argv);

      if (app.arguments().count() != 2) {
          std::cout << qPrintable(QString("Usage: textquery <URL of XML file>\n"));
          return 1;
      }

      QString url = app.arguments()[1];

      QXmlQuery query;
      query.bindVariable("url", QXmlItem(QVariant(url)));

      query.setQuery("declare variable $url external;"
                     "doc($url)//*/fn:string(fn:node-name(.))");

      QStringList results;

      if (query.isValid()) {
          if (query.evaluateTo(&results)) {
              foreach (const QString &result, results)
                  std::cout << qPrintable(result) << std::endl;
          }
      }

      return 0;
  }

Nous y faisons deux choses remarquables. Tout d'abord, nous lions la variable url à une valeur passée par l'utilisateur - elle est identifiée par $url dans le texte de la requête. Deuxièmement, parce que nous utilisons une requête qui est prévue pour retourner du texte brut, nous pouvons demander que le résultat de la requête soit placé dans une QStringList en utilisant les formes appropriées de QXmlQuery::evaluateTo().

Lors du lancement de cet exemple, nous devons lui passer l'URL qui spécifie sans ambiguïté l'emplacement du fichier forecast.xml. Passer le chemin complet du fichier est généralement suffisant.

Si nous ne devons gérer que des chaînes, nous pouvons aussi utiliser QXmlResultItems pour stocker le résultat de la requête. L'exemple xmlitemquery montre comment extraire et afficher les chaînes obtenues :

 
Sélectionnez
    QXmlResultItems results;

    if (query.isValid()) {
        query.evaluateTo(&results);
        do {
            QXmlItem item = results.next();
            if (item.isNull())
                break;
            
            std::cout << qPrintable(item.toAtomicValue().toString()) << std::endl;
        } while (true);
    }

Nous mentionnons cette méthode de traitement du résultat, car elle est plus générale que d'utiliser des QStringList, nous utiliserons d'ailleurs cette approche dans la suite de l'article.

V. Obtenir des résultats structurés

Comme nous l'avons déjà noté, exécuter cette sorte de requête simple et collecter quelques chaînes est très bien pour des tâches simples, mais nous avons souvent besoin de faire plus. En général, les données que nous voulons obtenir contiennent une structure intéressante que nous ne voulons pas perdre. Nous pouvons construire des requêtes pour conserver et transformer cette structure.

Par exemple, la requête suivante est utilisée pour obtenir une série d'éléments du document forecast.xml :

 
Sélectionnez
doc("forecast.xml")//sun,
doc("forecast.xml")//tabular/time

Notez qu'il y a deux parties dans cette requête, séparées par une virgule : la première récupère tous les éléments <sun> ; l'autre, les éléments <time> qui sont les enfants immédiats de tous les éléments <tabular>. La virgule fait simplement que la liste des éléments <time> sera mise à la suite des éléments <sun> dans le résultat.

Puisqu'il ne devrait y avoir jamais qu'un seul élément <sun> et un seul <tabular> dans le document, lancer xmlpatterns avec cette requête

 
Sélectionnez
xmlpatterns queries/sun-times.xq

devrait produire quelque chose comme ceci :

 
Sélectionnez
  <sun rise="2010-03-17T06:24:09" set="2010-03-17T18:23:22"/>
  <time from="2010-03-17T07:00:00" to="2010-03-17T13:00:00" period="1">
      <!-- Valid from 2010-03-17T07:00:00 to 2010-03-17T13:00:00 -->
      <symbol number="2" name="Fair"/>
      <precipitation value="0.0"/>
      <!-- Valid at 2010-03-17T07:00:00 -->
      <windDirection deg="113.7" code="ESE" name="East-southeast"/>
      <windSpeed mps="1.7" name="Light breeze"/>
      <temperature unit="celcius" value="2"/>
      <pressure unit="hPa" value="1029.8"/>
  </time>
  ...
  [several more <time> elements]

Nous pouvons écrire du code C++ pour exécuter cette requête. Cependant, puisque nous obtenons du XML comme réponse de la requête, quel est l'avantage d'utiliser XQuery au lieu de simplement parser et traiter le fichier XML directement avec un lecteur de flux, une classe SAX ou DOM ?

La réponse est que nous devons toujours traiter le XML que nous obtenons en résultat mais que nous pouvons garantir ce que sera sa structure, ce qui le rend beaucoup plus facile à utiliser parce qu'il n'est nécessaire que d'interpréter les parties du document qui nous intéressent.

Par exemple, nous pouvons filtrer la majorité des éléments hors du document, ne laissant qu'une séquence de températures :

 
Sélectionnez
  for $time in doc("forecast.xml")//tabular/time
      return <time>
          { $time/@from }
          <temperature>
              { $time/temperature/@value, $time/temperature/@unit }
          </temperature>
          </time>

La nouvelle fonctionnalité majeure de cette requête est la boucle, donc la syntaxe spéciale pour choisir des éléments pourrait avoir l'air reconnaissable aux développeurs familiers avec le concept de générateurs du langage Python. Cette boucle a l'effet de retourner une séquence d'éléments <temperature>.

Un point intéressant à noter est que les éléments <time> retournés par la boucle sont des éléments nouvellement créés, même si les éléments itérés par la boucle sont des éléments <time> dans le document original. Cette syntaxe, semblable aux templates, devrait nous permettre de renommer ou de transformer des éléments dans le résultat.

Dans cette requête, la syntaxe { ... } est utilisée pour appliquer des attributs aux nouveaux éléments <time>. Par exemple, $time/@from sélectionne l'attribut from de l'élément sélectionné dans le document d'origine. { $time/@from } applique l'attribut à un nouvel élément <time> dans le résultat. Notez que $time est un nom de variable arbitraire - nous aurions pu utiliser $x ou tout autre nom de variable.

De même, la pièce suivante de la requête applique les attributs des éléments <temperature> dans le document d'origine aux nouveaux éléments <temperature> du résultat :

 
Sélectionnez
          <temperature>
              { $time/temperature/@value, $time/temperature/@unit }
          </temperature>

D'autres choses à noter incluent que @from, @value et @unit font référence aux attributs from, value et unit des éléments<temperature>.

Lancer l'outil xmlpatterns avec cette requête

 
Sélectionnez
xmlpatterns queries/sun-temperatures.xq

devrait produire quelque chose du genre :

 
Sélectionnez
  <time from="2010-03-17T07:00:00">
      <temperature value="2" unit="celcius"/>
  </time>
  <time from="2010-03-17T13:00:00">
      <temperature value="15" unit="celcius"/>
  </time>
  ...
  <time from="2010-03-25T13:00:00">
      <temperature value="17" unit="celcius"/>
  </time>

L'exemple receiver montre comment nous pouvons interpréter ce format de données restreint quand nous recevons le résultat de la requête. Pour examiner ces éléments, nous devons créer une sous-classe de QAbstractXmlReceiver, qui s'occupe des éléments et de leurs attributs.

Nous créons une structure Forecast pour contenir les informations sur le temps et la température.

 
Sélectionnez
  typedef struct
  {
      QHash<QString, QString> time;
      QHash<QString, QString> temperature;
  } Forecast;

Le format étant maintenant assez limité, notre classe Receiver est assez simple :

 
Sélectionnez
  class Receiver : public QAbstractXmlReceiver
  {
  public:
      Receiver(const QXmlNamePool &namePool);
      void attribute(const QXmlName &name, const QStringRef &value);
      void endElement();
      void startElement(const QXmlName &name);

      void atomicValue(const QVariant &) {}
      void characters(const QStringRef &) {}
      void comment(const QString &) {}
      void endDocument() {}
      void endOfSequence() {}
      void namespaceBinding(const QXmlName &) {}
      void processingInstruction(const QXmlName &, const QString &) {}
      void startDocument() {}
      void startOfSequence() {}

      QList<Forecast> forecasts;

  private:
      QXmlNamePool namePool;
      QStack<QString> elements;
      Forecast currentForecast;
  };

Nous devons fournir des implémentations des fonctions virtuelles pures dans QAbstractXmlReceiver. À part celles-là, l'implémentation des fonctions concerne le stockage du temps et de la température dans des objets Forecast quand nous avons les données des attributs et l'ajout des bulletins météo à une liste quand chaque élément <time> se termine.

 
Sélectionnez
  Receiver::Receiver(const QXmlNamePool &namePool)
      : namePool(namePool)
  {
  }

  void Receiver::attribute(const QXmlName &xmlname, const QStringRef &valueref)
  {
      QString name = xmlname.localName(namePool);
      QString value = valueref.toString();
      QString currentElement = elements.top();

      if (currentElement == "time")
          currentForecast.time[name] = value;
      else if (currentElement == "temperature")
          currentForecast.temperature[name] = value;
  }

  void Receiver::startElement(const QXmlName &xmlname)
  {
      QString name = xmlname.localName(namePool);
      elements.push(name);

      if (name == "time")
          currentForecast = Forecast();
  }

  void Receiver::endElement()
  {
      if (elements.pop() == "time")
          forecasts.append(currentForecast);
  }

Nous pouvons prendre pour acquis le travail de la fonction main(), où nous évaluons la requête à un objet receveur nouvellement créé :

 
Sélectionnez
  Receiver receiver(query.namePool());

  if (query.isValid()) {
      if (query.evaluateTo(&receiver)) {
          foreach (const Forecast &forecast, receiver.forecasts) {
              std::cout << qPrintable(forecast.time["from"]) << std::endl;
              std::cout << qPrintable(forecast.temperature["value"] + " " + forecast.temperature["unit"]) << std::endl;
          }
      }
  }

Notez que nous passons un objet QXmlNamePool dans la classe Receiver. Nous devons le faire pour obtenir les noms corrects des éléments du résultat.

L'exemple est lancé de la même manière que les précédents, provoquant une sortie comme celle-ci :

 
Sélectionnez
  2010-03-17T07:00:00
  2 celcius
  2010-03-17T13:00:00
  15 celcius
  ...
  2010-03-25T13:00:00
  17 celcius

Avec un mécanisme mis en place pour collecter les données structures des résultats de la requête, nous pouvons maintenant appliquer l'approche à un exemple utile.

VI. Mener l'enquête pour la météo

L'exemple weatherreceiver qui accompagne cet article utilise XQuery pour obtenir et traiter les données du Norwegian Meteorological Institute. La requête qu'il utilise est une version plus complexe de la précédente. Cette fois, nous nous assurons que la liste des éléments <time> dans le résultat est triée par les heures des bulletins et nous collectons des informations sur les symboles à utiliser :

 
Sélectionnez
  doc("forecast.xml")//sun,
  for $time in doc("forecast.xml")//tabular/time
      order by $time/@from
      return <time>
          { $time/@from }
          <symbol>
              { $time/symbol/@name, $time/symbol/@number }
          </symbol>
          <temperature>
              { $time/temperature/@value, $time/temperature/@unit }
          </temperature>
          </time>

Chacun des symboles est décrit par un nom, qui fournit un bref résumé de la météo, et un nombre, qui fait référence à une image que nous pouvons utiliser dans l'interface utilisateur.

Image non disponible
L'exemple weatherreceiver

Cet exemple est légèrement plus compliqué ce que l'on pourrait attendre, car nous implémentons un cache de dix minutes pour éviter d'envoyer des requêtes en surnombre au service météorologique - ceci est couvert par les termes d'utilisation du service (voir plus bas pour plus d'informations).

VII. Aller plus loin

Comme nous l'avons vu, utiliser XQuery pour obtenir et traiter des données de sources XML est plus simple qu'espéré. Cependant, l'efficacité de XQuery dans vos applications est dépendante de votre capacité à écrire des requêtes. La documentation de QtXmlPatterns fournit quelques liens sur des spécifications en ligne et des guides qui peuvent aider mais une certaine pratique est requise.

Dans un futur article, nous regarderons les fonctionnalités du module QtXmlPatterns qui nous permettent de modeler d'autres types de donnés en XML pour appliquer nos connaissances de XQuery encore plus loin. Nous regarderons aussi une manière de visualiser nos requêtes sur nos données.

VIII. Conditions d'utilisation

Dans cet article et les programmes d'exemple, nous avons utilisé des données du Norwegian Meteorological Institute et de la Norwegian Broadcasting Corporation. Regardez la page suivante pour des informations sur le service météorologique yr.no et un lien vers les termes d'utilisation : http://www.yr.no/verdata/1.6810075.

Les images utilisées pour les symboles du temps dans chacun des exemples ont été obtenues de la page suivante, qui contient aussi des informations sur leurs termes d'utilisation : http://www.yr.no/informasjon/1.1940495

IX. Le code source

Le code source pour les exemples mentionnés dans cet article est disponible.

X. L'auteur

David Boddie est un écrivain technique chez Qt Development Frameworks.

XI. Divers

J'adresse ici de chaleureux remerciements à beaucoup de monde, en particulier à Mahefasoa pour sa relecture et ses conseils.

Au nom de toute l'équipe Qt, j'aimerais adresser le plus grand remerciement à Nokia pour nous avoir autorisé à traduire cet article !