Способы обработки XML в Qt — Stream, SAX, DOM

Многие сталкивались с XML-документами и знают что это такое, ведь стандарт рассматриваемого языка разметки опубликован в далеком 1998 году. Язык XML используется во многих областях, но чаще всего для передачи информации через Internet — не случайно стандарт разработан Консорциумом Всемирной паутины (W3C) [1].

Очень много информации в этом мире записано и передается в формате XML, например ленты новостей RSS и Atom. В связи с этим не будут лишними навыки использования библиотек для обработки XML-файлов.

В статье рассмотрены три варианта разбора файлов в формате XML средствами библиотеки Qt. В качестве примера используется файл, возвращаемый Центральным банком Российской Федерации на запрос курса доллара в заданный период [2].

Для получения файла с курсом валюты на сайт Центрального банка высылается запрос. Используется QNetworkAccessManager, подробно описанный в статье «Получение данных с сайта. Шаблон Producer/Consumer» [3]. Данные, извлеченные из файла, выводятся на график средствами библиотеки Qwt [4].

Screenshot_of_the_schedule_currency

рис. 1 Снимок окна с графиком курсов валют

Архитектура приложения, запрос курса валют

Класс RateReceiver передает запрос курса доллара сайту Центрального банка и осуществляет обработку, приходящего в ответ XML-файла. Когда ответ от сайта будет полностью получен, объект класса QNetworkAccessManager вырабатывает сигнал finished(), связанный со слотом on_load класса RateReceiver. В слоте on_load() из входного файла выделяется информация для построения графика и передается с сигналами rate() экземпляру MainWidget. По завершению обработки файла вырабатывается сигнал loadFinished().

Объект класса MainWidget накапливает данные о курсах валют в векторе, а затем, по приходу сигнала loadFinished(), выводит точки на график.

UML_class_diagram

рис. 2 UML диаграмма классов

На диаграмме классов показано, что RateReceiver сам справляется с обработкой входного файла в слоте on_load и, лишь при использовании SAX-интерфейса (см. ниже), работа с файлом делегируется отдельному объекту (XmlSaxHandler).

class MainWidget : public QWidget {
  Q_OBJECT
public:
  explicit MainWidget(QWidget *parent = 0);
public slots:
  void on_loadClicked();
  void on_rate(const QDate& date, const double rate);
  void on_loadFinished();
protected:
  Ui::ratesform *m_pUI;
  RateReceiver *m_pRateReceiver;
  QwtPlotCurve m_curve;
  QVector<QPointF> m_points;
};

MainWidget::MainWidget(QWidget *parent) :
  QWidget(parent),
  m_pUI(new Ui::ratesform), m_pRateReceiver(new RateReceiver(this)) {
  m_pUI->setupUi(this);
  // ...
  connect(m_pUI->load, SIGNAL(clicked()), SLOT(on_loadClicked()));
  connect(m_pRateReceiver, SIGNAL(rate(QDate,double)), SLOT(on_rate(QDate,double)));
  connect(m_pRateReceiver, SIGNAL(loadFinished()), SLOT(on_loadFinished()));
}

void MainWidget::on_rate(const QDate &date, const double rate) {
  const int x = date.toJulianDay();
  const int i = x - m_pUI->from->date().toJulianDay();
  m_points[i] = QPointF(x, rate);
}

void MainWidget::on_loadFinished() {
  // ...
  m_curve.setSamples(m_points);
  m_pUI->diag->replot();
}

void MainWidget::on_loadClicked() {
  // ...
  m_points.clear();
  m_points.resize(ndays);

  m_pRateReceiver->rateRequest(m_pUI->from->date(), m_pUI->to->date());
}

// ...
RateReceiver::RateReceiver(QObject *parent) :
  QObject(parent), m_nam(new QNetworkAccessManager(this))
  /* ... */ {
  connect(m_nam, SIGNAL(finished(QNetworkReply*)), SLOT(on_load(QNetworkReply*)));
  //...
}

void RateReceiver::rateRequest(const QDate &dateBegin, const QDate &dateEnd) {
  QUrlQuery postData;
  postData.addQueryItem("date_req1", dateBegin.toString("dd/MM/yyyy"));
  postData.addQueryItem("date_req2", dateEnd.toString("dd/MM/yyyy"));
  postData.addQueryItem("VAL_NM_RQ", "R01235"); // ID of USD

  QNetworkRequest req(QUrl("http://www.cbr.ru/scripts/XML_dynamic.asp"));
  req.setHeader(QNetworkRequest::ContentTypeHeader,
                "application/x-www-form-urlencoded");

  m_nam->post(req, postData.toString(QUrl::FullyEncoded).toUtf8());
}

Код приведен схематично, внимание можно обратить на слот rateRequest, производящий запрос к сайту Центрального банка. В запросе передаются параметры — начало и конец временного интервала, код валюты. Для передачи параметров запроса в библиотеке Qt используется класс QUrlQuery.

<?xml version="1.0" encoding="windows-1251" ?>
<ValCurs  ID="R01235" DateRange1="26/06/2014" DateRange2="29/06/2014"   name="Foreign Currency Market Dynamic">
<Record Date="26.06.2014" Id="R01235"><Nominal>1</Nominal><Value>33,9070</Value></Record>
<Record Date="27.06.2014" Id="R01235"><Nominal>1</Nominal><Value>33,7508</Value></Record>
<Record Date="28.06.2014" Id="R01235"><Nominal>1</Nominal><Value>33,6306</Value></Record>
</ValCurs>

Средства обработки XML в библиотеке Qt

В библиотеку Qt включены три класса, реализующих различные подходы к обработке XML-документов:

  • QXmlStreamReader [5], воспринимает документ в виде потока тегов. Интерфейс класса позволяет перемещаться по этому набору. Класс предоставляет самый быстрый обработки XML-документов, однако об иерархической структуре документа класс ничего не знает и не подходит для добавления информации в документ;
  • QXmlSimpleReader [6], реализует SAX-интерфейс (Java API) для работы с XML. Фактически, является тем же QXmlStreamReader, но в другой обертке — при считывании тега в объекте-обработчике вызывается виртуальная функция, соответствующая типу тега. Функция-обработчик ничего не знает о предыдущих вызовах и обрабатывает только текущий тег, поэтому требует также мало памяти, как QXmlStreamReader и не подходит для изменения документа;
  • QDomDocument [7], реализует DOM-интерфейс (W3C API) обработки XML. Считывает весь документ в память в виде дерева, поэтому не рекомендуется для обработки больших документов. Позволяет изменять дерево и, соответственно, документ.

Пример использования QXmlStreamReader

Объект класса QXmlStreamReader может быть инициирован открытым файлом или массивом байт, содержащим XML-документ. Вызов метода readNext() приводит к считыванию следующего токена и возвращает его тип (TokenType). Из множества типов токенов нас будут интересовать StartElement и EndElement, отвечающие за открывающий и закрывающий тэги соответственно. Тэг имеет имя, которое возвращает метод name(), и набор атрибутов. Набор атрибутов (QXmlStreamAttributes) можно получить методом attributes(), он представляет собой вектор именованных атрибутов. Для обращения к атрибуту по имени используется метод value().

void RateReceiver::on_load(QNetworkReply *reply) {
  QLocale german(QLocale::German);
  QByteArray buff = reply->readAll();
  QXmlStreamReader xmlDoc(buff);

  double valRate(-1);  // курс валюты
  QDate date;

  while (!xmlDoc.atEnd() && !xmlDoc.hasError()) {
    QXmlStreamReader::TokenType token = xmlDoc.readNext();
    if (token == QXmlStreamReader::StartElement) {
      if (xmlDoc.name() == "Record") {
        QXmlStreamAttributes attrib = xmlDoc.attributes();
        date = QDate::fromString(attrib.value("Date").toString(), "dd.MM.yyyy");
      }

      if (xmlDoc.name() == "Value") {
        valRate = german.toDouble(xmlDoc.readElementText());
        continue;
      }
    }
    if (token == QXmlStreamReader::EndElement && xmlDoc.name() == "Record")
      emit rate(date, valRate);
  }

  emit loadFinished();
  delete reply;
}

Во время работы приведенного листинга в памяти одновременно будет находиться информация, связанная лишь с одним тегом. Для файлов сложной структуры код может быть очень запутанным — в этих случаях предпочтительно использовать SAX.

Пример использования QXmlSimpleReader

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

RateReceiver::RateReceiver(QObject *parent) :
  QObject(parent), m_nam(new QNetworkAccessManager(this)),
  m_pXmlSaxHandler(new XmlSaxHandler(this)) {
  connect(m_nam, SIGNAL(finished(QNetworkReply*)), SLOT(on_load(QNetworkReply*)));

  connect(m_pXmlSaxHandler, SIGNAL(rate(QDate,double)), SLOT(on_rate(QDate,double)));
  connect(m_pXmlSaxHandler, SIGNAL(finished()), SLOT(on_finished()));
}

void RateReceiver::on_load(QNetworkReply *reply) {
  QByteArray buff = reply->readAll();

  m_pXmlSaxHandler->setData(buff);

  delete reply;
}

void RateReceiver::on_finished() {
  emit loadFinished();
}

void RateReceiver::on_rate(const QDate &date, const double &valRate) {
  emit rate(date, valRate);
}

Теперь класс RateReceiver является лишь посредником в передаче сигнала. При получении файла с сервера RateReceiver вызывает метод setData, чем инициирует обработку файла и выдачу сигналов.

class XmlSaxHandler : public QObject, public QXmlDefaultHandler {
  Q_OBJECT
public:
  explicit XmlSaxHandler(QObject *parent = 0);

  bool startElement(const QString &namespaceURI,
                    const QString &localName,
                    const QString &qName,
                    const QXmlAttributes &attribs);
  bool endElement(const QString &namespaceURI,
                  const QString &localName,
                  const QString &qName);
  bool characters(const QString &str);
  bool fatalError(const QXmlParseException &exception);

  void setData(QByteArray& buff);
signals:
  void rate(const QDate& date, const double &rate);
  void finished();
protected:
  QDate m_date;
  double m_rate;
  enum State {
    Idle, InValue
  } m_state;
};

XmlSaxHandler::XmlSaxHandler(QObject *parent) : QObject(parent) {
}

void XmlSaxHandler::setData(QByteArray &buff) {
  QXmlSimpleReader reader;
  QXmlInputSource source;
  source.setData(buff);

  reader.setContentHandler(this);
  reader.setErrorHandler(this);

  reader.parse(source);
}

bool XmlSaxHandler::startElement(const QString &, const QString &,
                                const QString &qName, const QXmlAttributes &attribs) {
  if (qName == "Record")
    m_date = QDate::fromString(attribs.value("Date"), "dd.MM.yyyy");
  else if (qName == "Value")
    m_state = State::InValue;
  return true;
}

bool XmlSaxHandler::endElement(const QString &, const QString &, const QString &qName) {
  if (qName == "Value")
    m_state = State::Idle;
  else if (qName == "ValCurs")
    emit finished();
  else if (qName == "Record")
    emit rate(m_date, m_rate);
  return true;
}

bool XmlSaxHandler::characters(const QString &str) {
  QLocale german(QLocale::German);
  if (m_state == State::InValue)
    m_rate = german.toDouble(str);
  return true;
}

bool XmlSaxHandler::fatalError(const QXmlParseException &) {
  return false;
}

Класс XmlSaxHandler наследует виртуальные методы, которые вызываются при считывании парсером лексемы определенного типа — startElement(), endElement(), characters(), а также метод fatalError(), срабатывающий при возникновении ошибки. Все перечисленные методы возвращают true если обработка прошла успешно, иначе — false.

Каждая лексема обрабатывается независимо от других, поэтому чтобы сохранить информацию (например дату и значение курса валюты) нужно добавлять поля класса. По той же причине парсер не владеет информацией о контексте, в котором использована лексема — если требуется учитывать иерархическую вложенность тегов, можно ввести дополнительное поле, отвечающее за состояние парсера.

Пример использования QDomDocument

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

Для получения корня дерева используется метод documentElement(). Все остальные узлы (QDomNode) дерева являются дочерними для других узлов, получить первый дочерний узел можно методом firstChild(), перейти к следующему узлу — методом nextSibling(). У узла есть имя, возвращаемое методом nodeName() и набор именованных атрибутов (QDomNamedNodeMap). Получить набор, ассоциированных атрибутов можно методом attributes(), получить узел атрибута по имени — методом namedItem(). Атрибут является обычным узлом дерева (QDomNode), поэтому его значение возвращает метод nodeValue().

void RateReceiver::on_load(QNetworkReply *reply) {
  QLocale german(QLocale::German);
  QByteArray buff = reply->readAll();

  double valRate(-1);  // курс валюты
  QDate date;

  QDomDocument doc;
  if (false == doc.setContent(buff))
     throw QString("bad XML-file: setContent");

  QDomElement root = doc.documentElement();
  if (root.tagName() != "ValCurs")
    throw QString("bad XML-file: tagname() != ValCurs");

  QDomNode record_node = root.firstChild();
  while (false == record_node.isNull()) {
    if (record_node.toElement().tagName() != "Record")
      throw QString("bad XML-file: tagname() != Record");
    date = QDate::fromString(record_node.attributes().namedItem("Date").nodeValue(),
                             "dd.MM.yyyy");

    QDomNode node = record_node.firstChild();
    while (false == node.isNull()) {
      if (node.nodeName() == "Value") {
        valRate = german.toDouble(node.toElement().text());
        break;
      }
      node = node.nextSibling();
    }
    emit rate(date, valRate);
    record_node = record_node.nextSibling();
  }

  emit loadFinished();
  delete reply;
}

Последний пример учитывает то, что тег Value должен встречаться лишь внутри тега Record, а тег Record является дочерним для ValCurs. При использовании QXmlStreamReader и QXmlSimpleReader мы не обращали на это внимание. Если требуется учитывать иерархическую вложенность тегов, то удобнее использовать либо модель DOM, либо вводить состояния в модели SAX.

Если необходимо часто изменять документ, то альтернативы DOM нет, т.к. SAX и Stream рассматривают документ как обычный текстовый файл, добавить данные в середину которого невозможно. Если же изменения вносятся достаточно редко, то возможно создавать новый документ на основе старого, используя обычный файловый поток (QTextStream), при этом во время записи требуется экранировать служебные символы методом Qt::escape().

Ось времени Qwt

На рисунке 1 можно заметить необычную горизонтальную ось на графике. Установка осей в Qwt выполняется методом setAxisScaleDraw(), аргументом которого должен являться объект, которому будет делегироваться обязанность отрисовки значений на оси. Чтобы создать свою собственную ось, достаточно породить наследника класса QwtScaleDraw и реализовать в нем виртуальный метод label(double v).

class DayScaleDraw : public QwtScaleDraw {
public:
  DayScaleDraw();

  virtual QwtText label(double v) const;
};

DayScaleDraw::DayScaleDraw() : QwtScaleDraw() {
}

/*virtual*/ QwtText DayScaleDraw::label(double v) const {
  return QDate::fromJulianDay(v).toString("dd.MM.yy");
}

Исходный код примеров проекта можно скачать. Для сборки требуется установить библиотеку Qwt и в файле проекта изменить путь к ней.

Список использованной литературы

  1. XML на официальном сайте Консорциума Всемирной паутины \ http://www.w3.org/standards/xml/
  2. Технические ресурсы Центрального банка Российской Федерации. Получение данных, используя XML \ http://www.cbr.ru/scripts/Root.asp?PrtId=SXML
  3. Получение данных с сайта. Шаблон Producer/Consumer \ https://pro-prof.com/archives/1034
  4. Cистема плагинов Qt, построение графиков и Qt Script \ https://pro-prof.com/archives/1316
  5. QXmlStreamReader Class \ http://qt-project.org/doc/qt-5/QXmlStreamReader.html
  6. QXmlSimpleReader Class \ http://qt-project.org/doc/qt-5/QXmlSimpleReader.html
  7. QDomDocument Class \ http://qt-project.org/doc/qt-5/QDomDocument.html

2 thoughts on “Способы обработки XML в Qt — Stream, SAX, DOM

  1. Максим

    В примере использования QXmlSimpleReader, должно быть
    m_state = Idle;
    и так в трёх местах
    вместо
    m_state = State::Idle;

    иначе

    `State’ is not a class or namespace

    Reply
    1. admin Post author

      Вы правы, недочет там реально есть, править его можно по разному.
      Когда я писал этот пример — думал о scoped enums из стандарта С++11, они разрешают такую запись, как я применил. Однако, только сейчас, благодаря Вам я заглянул в стандарт и заметил, что не все перечисления согласно стандарта являются «scoped» — для этого необходимо использовать запись типа:

      enum class State {
        Idle, InValue
      } m_state;
      
      Reply

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*

code