C++/Qt: Получение полного содержимого XML-тега (со вложенными) при SAX-разборе

Jun 09, 2014 15:53

Задача: при SAX-разборе перехватить полное содержимое определённых тегов в виде исходного текста, как оно есть, со всеми вложенными подтегами.
Хотя задача эта, по-моему, достаточно простая, банальная и затребованная, но по какой-то не понятной причине не нашёл избытка описания готовых её решений.
Зачем удобно перехватывать полный «сырой» текст XML? С ходу столкнулся с двумя возможными причинами:
1. Как известно и у SAX и у DOM разбора есть свои преимущества и недостатки. Возможен большой XML-документ, который по крайней мере удобно разбирать SAX-парсером, но в нём попадаются отсносительно небольшие участки, которые как раз было бы удобно и естественно с помощью DOM.
2. В определённных XML-тегах может находится XHTML-содержимое, разбирать которое не нужно вообще, его надо просто целиком сохранить, например, отправить на визуализацию в GUI-контрол.

Вероятно хватает и других задач, при которых удобно временно перевести SAX-разбор в режим «перехвата», при попадании на определённый тег.

Итак, как оказалось в Qt это сделать очень просто: достаточно перекрыть в классе QXmlInputSource публичный виртуальный метод next, который «кормит» прасер анализируемым XML посимвольно.

Вариант №1:
Обработчик содержимого (наследник QXmlDefaultHandler):
(В случае если при SAX-разборе попался один из интересующих нас «особых» тегов, мы переходим в режим «перехвата», а при закрытии тега выходим из этого режима, обрабатываем результат).

bool Report::startElement(const QString &namespaceURI, const QString &localName,
const QString &name, const QXmlAttributes &attrs)
{
if (name=="information") // "information" is a special tag name
{
m_xmlsource->BeginIntercept();
}

return RemoteTable::startElement(namespaceURI, localName, name, attrs);
}
// RemoteTable is a parent class that handle XML content

bool Report::endElement(const QString &namespaceURI, const QString &localName,
const QString &name)
{
if (name=="information")
{
QString info = m_xmlsource->EndIntercept();

if (m_pHeader!=NULL)
{
// send XHTML content into QLabel
const QString was = m_pHeader->text();
m_pHeader->setText( was+info );
}
}

return RemoteTable::endElement(namespaceURI, localName, name);
}
Класс ExtXmlInputSource:

#ifndef EXTXMLINPUTSOURCE_H
#define EXTXMLINPUTSOURCE_H

#include

#include

class ExtXmlInputSource : public QXmlInputSource
{
public:
ExtXmlInputSource(QIODevice* dev);

virtual void BeginIntercept();
virtual QString EndIntercept();
virtual QChar next();

protected:
bool m_interception;
QString m_content;
};

#endif // EXTXMLINPUTSOURCE_H

// cpp:

ExtXmlInputSource::ExtXmlInputSource(QIODevice* dev)
: QXmlInputSource(dev)
, m_interception(false)
{
}

void ExtXmlInputSource::BeginIntercept()
{
m_interception = true;
}

QChar ExtXmlInputSource::next()
{
QChar ret = QXmlInputSource::next();

if (m_interception)
m_content+=ret;

return ret;
}
Таким образом в накопителе ExtXmlInputSource::m_content получится нужный блок XML-текста плюс закрывающийся тег (если открывающий тег не был вообще «самозакрытым»). Тег для симментрии убираем (если результат направляется в DOM, можно наоборот в начале приписать открывающийся тег):

QString ExtXmlInputSource::EndIntercept()
{
m_interception = false;

const QString res = m_content;
m_content.clear();

// if needed: remove the last (closing) tag
if (!res.isEmpty()) // non-empty tag?
{
int pos = res.lastIndexOf(");
if (pos>=0)
return res.left(pos);
}

return res;
}
У этого метода есть недостаток - в режиме «перехвата» парсер всё равно вызывает виртуальные методы обработчика содержимого (в данном случае класса Report, наследника QXmlDefaultHandler), что во-первых не нужно и грузит машину бесполезной работой, во-вторых вносит путаницу и является потенциальной причиной ошибок.

У меня есть несколько идей, как обойти это. Например, смелая идея (не знаю, на сколько это реализуемо) - запустить DOM-обработчик, на том же QIODevice-источнике информации.
Ниже привожу наверное самое простое решение - временную замену обработчика содержимого.

Вариант №2 (улучшенный):
Класс ExtXmlInputSource2:

#ifndef EXTXMLINPUTSOURCE2_H
#define EXTXMLINPUTSOURCE2_H

#include "extxmlinputsource.h"

#include
#include

class ExtXmlInputSource2 : public ExtXmlInputSource, public QXmlDefaultHandler
{
public:
ExtXmlInputSource2(QIODevice* dev);

void Intercept(const QString& spectag, QXmlSimpleReader* reader, QXmlDefaultHandler* old);

virtual bool endElement(const QString& , const QString& , const QString &name);

protected:
QString m_spectag;
QXmlSimpleReader* m_reader;
QXmlDefaultHandler* m_old;
};

#endif // EXTXMLINPUTSOURCE2_H

// cpp:

ExtXmlInputSource2::ExtXmlInputSource2(QIODevice* dev)
: ExtXmlInputSource(dev)
{
}

void ExtXmlInputSource2::Intercept(const QString& spectag, QXmlSimpleReader* reader, QXmlDefaultHandler* old)
{
m_spectag = spectag;
m_reader = reader;
m_old = old;

if (m_reader!=NULL)
m_reader->setContentHandler(this);

BeginIntercept();
}

bool ExtXmlInputSource2::endElement(const QString& , const QString& , const QString &name)
{
if (name==m_spectag) // warning: no nested tags 'spectag' are expected!
{
if (m_old!=NULL)
m_old->characters( EndIntercept() );

if (m_reader!=NULL)
m_reader->setContentHandler(m_old);
}

return true; // QXmlDefaultHandler::endElement(namespaceURI, localName, name); == always true
}
Вызов из основного класса-обработчика содержимого происходит с помощью метода Intercept, а приём перехваченного XML происходит как-бы обычным вызовом виртуального метода characters в рабочем обработчике.
В метод Intercept кроме названия тега передаётся указатель на класс SAX-парскра и указатель на рабочий (основной) обработчик содержимого для автоматического возврата в основной режим, после закрывающегося «особого» тега:

bool Report::startElement(const QString &namespaceURI, const QString &localName,
const QString &name, const QXmlAttributes &attrs)
{
if (name=="information")
{
m_xmlsource->Intercept(name, m_reader, this);
}

return RemoteTable::startElement(namespaceURI, localName, name, attrs);
}
// RemoteTable is a parent class that handle XML content

bool Report::characters(const QString &str)
{
if (m_name=="information") // m_name set in RemoteTable::startElement
{
if (m_pHeader!=NULL)
{
// send XHTML content into QLabel
const QString was = m_pHeader->text();
m_pHeader->setText( was+str );
}
}

return RemoteTable::characters(str);
}

Оба эти варианты работают с предположением, что «специальные» теги, которые надо перехватывать не будут вложенными.

Приложение.
На форуме RSDN человек по имени Константин дал мне ценные указания, как это сделать технологиями Microsoft:
«MSXML позволяет это делать минимальными усилиями: класс MXXMLWriter умеет из SAX-событий делать строку с текстом, или писать XML документ в любой IStream, или, как для твоей задачи, делать DOM из SAX.
Когда в своей реализации ISAXContentHandler встретился определённый тег, создавай DOM document, создавай экземпляр COM-класса MXXMLWriter назначив ему output новый DOM document, потом нужно аккуратно форвардить события ISAXContentHandler в этот MXXMLWriter, пока определённый тег не закроется. Когда закроется, получиццо DOM-документ с содержимым тега».

Там же мне дали ссылку по этому вопросу на Java:
http://stackoverflow.com/questions/7998733/loading-local-chunks-in-dom-while-parsing-a-large-xml-file-in-sax-java
(
Comments
|Comment on this)

it, c++, xml, sax, qt

Previous post Next post
Up