Проверка погоды с помощью XQuery

Использование поддержки Qt для XQuery при обработке прогноза погоды, представленного в XML формате

Автор: David Boddie, Nokia, Qt Development Frameworks

В Qt Quarterly 25 мы рассмотрели QtXMLPatterns, технологию для обработки XML с помощью языка XQuery. Статья "Serious Enquiries with XQuery" была по большей части упражнением в использовании запросов для преобразования XML в обычный текст или набор элементов.

Это может быть полезно для приложений, в которых XML преобразуется для отображения на Web странице. Однако часто бывает полезным иметь возможность получить результат запроса и извлечь из него данные для использования в простом пользовательском интерфейсе. Одним из способов сделать это  - написать запрос, который сформирует вывод в виде обычного текста.

Возможно, это самый простой способ получить данные из XML документа. Мы, по сути, просто отбрасываем структуру документа. В то же время, используя запрос подобным образом, мы завершаем извлечение информации из XML фрагмента – не это ли XMLPatterns делал для нас?

В данной статье мы рассмотрим XML документ и исследуем несколько различных способов использования запросов для извлечения из него информации.

Документ

Мы получили некую информацию о погоде из Норвежского метеорологического института в XML формате. Этот документ содержит коллекцию прогнозов погоды за несколько дней. Фрагмент из этого файла представляет общую его структуру:

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

Покопаемся в XML. В нем есть много интересной информации, но главным образом нас интересует time, temperature и symbol. Мы рассмотрим элемент <sun> и его атрибуты, и элементы <time>, его детей и их атрибуты.

Создание и выполнение запроса

Язык XQuery достаточно отличается от императивных языков, подобных C++. Иногда,  он для получения желаемых результатов при  формировании запросов, может заставить нас немного подумать. Следующий запрос извлекает имя каждого элемента в документе forecast.xml:  

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

Кратко поясним. Первая часть запроса ссылается на документ, полученный из файла forecast.xml:

  doc("forecast.xml")

Двойной слеш показывает, что нас интересуют элементы, расположенные в любом месте внутри документа, а звездочка указывает, что для нас подходит любой элемент:

  doc("forecast.xml")//*

Мы хотим знать имя каждого элемента, поэтому мы вызываем функцию node-name на контекстном узле, что указывается с помощью символа точка:

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

Один из простейших способов выполнения запроса заключается в использовании утилиты xmlpatterns, которая обычно включается в стандартную установку Qt и запускается с именем файла, содержащего запрос. В командной строке мы вводим каталог, содержащий файлы nodenames.xq и forecast.xml и следующую команду:

  xmlpatterns nodenames.xq

Выходные данные – это длинный список имен, разделенных пробелами, которые мы здесь убрали для удобства чтения:

  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

  ...

Важно отметить, что для данного запроса XML файл и файл XQuery должны находиться в одном каталоге. При выполнении запроса ссылка к "forecast.xml" интерпретируется как относительный URL адрес.

Другой способ выполнения запроса – использование класса QXmlQuery. Пример textquery поставляется с данной статьей – это небольшая программа, которая выполняет тот же запрос из C++ и выводит результаты в виде последовательности строк. Она состоит из одной функции main():  

  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;

}

Здесь мы сделали два важные вещи. Во-первых, мы связали в запросе переменную "url" со значением, переданным пользователем. В тексте запроса это представляется как "$url". Во-вторых, поскольку мы используем запрос, который предназначается для возврата обычного текста, мы можем запросить поместить результат запроса в QStringList,  используя соответствующую форму QXmlQuery::evaluateTo().

Когда запускается данный пример, мы должны передать ему URL для однозначного определения расположения файла forecast.xml. Обычно достаточно передать полный путь к файлу.

Если мы имеем дело исключительно со строками, мы также можем использовать QXmlResultItems для хранения результата запроса. Пример Xmlitemquery показывает, как извлечь и напечатать полученные строки:

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

    }

Мы упомянули этот способ обработки результатов запросов, поскольку он является более общим по сравнению с QStringList, и далее в этой статье мы будем использовать этот подход.

Получение структурированных результатов

Как мы уже отмечали ранее, выполнение простых запросов такого рода и выборка нужных строк подходит для простых задач, но нам часто нужно делать более сложные вещи. Обычно, необходимые нам данные структурированы, и мы не хотим игнорировать их структуру. Мы можем создавать запросы для сохранения и преобразования этой структуры.

Например, следующий запрос используется для получения набора элементов из документа forecast.xml:

  doc("forecast.xml")//sun,

  doc("forecast.xml")//tabular/time

Обратите внимание, что запрос состоит из двух частей, разделенных запятыми. Первая часть получает из документа все элементы <sun>. Вторая часть получает все элементы <time>, являющиеся непосредственными потомками всех элементов <tabular>. В результате запятая просто заставляет добавить список элементов <time> к элементу <sun>.

Поскольку в документе всегда должен быть только один элемент <sun> и один элемент <tabular>, выполнение запроса xmlpatterns:

  xmlpatterns queries/sun-times.xq

приведет к следующим выходным данным, которые выглядят следующим образом:

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

  ...

  [другие элементы <time>]

Мы можем написать код для выполнения этого запроса на C++. Однако поскольку мы в результате запроса получаем XML, то встает вопрос – в чем же преимущество XQuery по сравнению с применением парсера  и обработкой XML файла непосредственно с помощью потокового ридера, классов SAX и DOM?

Ответ заключается в следующем. В любом случае нам для получения результата необходимо обработать XML, но мы можем гарантировать сохранность его структуры, да и обработка будет легче, поскольку мы интерпретируем только интересующую нас часть документа.

Например, мы можно отфильтровать большинство элементов в документе, оставляя на выходе только последовательность температур:

  for $time in doc("forecast.xml")//tabular/time

      return <time>

          { $time/@from }

          <temperature>

              { $time/temperature/@value, $time/temperature/@unit }

          </temperature>

         </time>

Основной новой возможностью этого запроса является цикл for, чей специфический синтаксис для выбора элементов схож с концепцией генераторов в языке программирования Python. Этот цикл обеспечивает эффект возврата в последовательности элементов <time>, каждый из которых содержит элемент <temperature>.

Отметим один интересный момент. Возвращаемые циклом элементы <time> являются вновь созданными элементами, несмотря на то, что элементами итерации цикла являются элементы <time> исходного документа. Как результат, такой синтаксис шаблонов дает нам возможность как переименовать, так и трансформировать элементы.

В данном запросе синтаксис {...} используется в применении к атрибутам новых элементов <time>. Например, $time/@from выбирает атрибут "from" указанного элемента исходного документа, а {$time/@from} применяет этот атрибут для нового элемента <time> результата. Отметим, что $time является произвольным именем переменной. Мы могли бы вместо него использовать $x или любое другое имя.

Аналогичным образом следующий фрагмент запроса берет атрибуты элемента <temperature> исходного документа и применяет их к новым элементам <temperature>  результата:

          <temperature>

              { $time/temperature/@value, $time/temperature/@unit }

          </temperature>

Еще можно отметить включение ссылок  @from, @value и @unit на атрибуты from, value и unit элементов <temperature>.

Выполняя утилиту xmlpatterns с этим запросом

  xmlpatterns queries/sun-temperatures.xq

получим на выходе следующее:

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

Пример receiver показывает, как мы можем интерпретировать этот ограниченный формат данных, полученный в результате запроса. Для изучения этих элементов мы должны создать подкласс класса QAbstractXmlReceiver, который обрабатывает элементы и их атрибуты.

Мы создаем структуру Forecast для хранения сведений о времени и температуре.

  typedef struct

  {

      QHash<QString, QString> time;

      QHash<QString, QString> temperature;

  } Forecast;

Поскольку в настоящее время формат весьма ограничен, наш подкласс Receiver очень прост:

  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;

  };

Мы должны обеспечить фиктивную реализацию чисто виртуальных функций в QAbstractXmlReceiver. Помимо этого, выполнение функций касается сохранения данных о времени и температуре в объектах Forecast при получении данных из атрибутов, и добавления прогнозов в список по окончании каждого элемента <time>.

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

  }

В главной функции мы используем преимущество такой работы, где мы формируем запрос во вновь созданном объекте receiver:

  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;

          }

      }

  }

Заметьте, мы передаем объект QXmlNamePool внутрь класса Receiver. Нам это нужно, чтобы на выходе получить корректные имена элементов.

Пример выполняется точно также как и предыдущие примеры, в результате получим следующее:

  2010-03-17T07:00:00

  2 celcius

  2010-03-17T13:00:00

  15 celcius

  ...

  2010-03-25T13:00:00

  17 celcius

Теперь, имея механизм для сбора структурированных данных, полученных в результате запроса, мы можем с пользой применить этот подход.

Формирование запросов о погоде

Пример weatherreceiver, который сопровождает данную статью, использует XQuery для получения и обработки данных из Норвежского метеорологического института. В нем используется более сложный вариант предыдущего запроса. На этот раз мы убедимся, что список элементов <time> в результате сортируется по времени прогноза, и мы соберем нужную для использования информацию о symbol:

  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>

Каждый из symbols описывается именем, которое обеспечивает краткую сводку погоды и числом, которое ссылается на изображение, используемое нами в пользовательском интерфейсе.

Getting a weather forecast

The weatherreceiver example in use

Пример немного более сложный, чем можно было бы ожидать, поскольку мы реализуем правило десятиминутного кэширования , чтобы избежать слишком частых запросов в службу погоды. Это дано в материале об условиях использования сервиса (подробнее см. ниже).

Далее

Как мы уже видели, использование XQuery для получения и обработки данных из исходного XML проще, чем можно было бы ожидать. Однако эффективность XQuery в ваших приложениях зависит вашего умения писать запросы. Документация QtXMLPatterns содержит некоторые ссылки на online спецификации и руководства, которые могут помочь, но требуется также и определенное количество практики.

В будущей статье мы рассмотрим возможности модуля QtXMLPatterns, которые позволяют нам моделировать другие типы данных подобно XML, следовательно, мы сможем более широко применять наши знания языка XQuery. Мы также рассмотрим один из способов визуализации запросов наших данных.

Условия использования

В этой статье и примерах программ мы использовали данные из Норвежского метеорологического институт (met.no) и Норвежской корпорация радиовещания (НРК). Смотрите следующие страницы для получения информации о службе погоды yr.no и ссылки на условия использования:

http://www.yr.no/verdata/1.6810075

Изображения, используемые для символов погоды, в каждом из соответствующих примеров были получены со следующей страницы, которая также содержит информацию об условия их использования:

http://www.yr.no/informasjon/1.1940495

Исходные тексты

Исходный код примеров, приведенных в данной статье, доступен с Web сайта Qt Quarterly: qq33-xquery.zip

Об авторе

David Boddie – технический писатель подразделения разработки Qt.

Сайт создан в системе uCoz