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

Jun. 9th, 2014

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

Previous Entry Add to Memories Tell A Friend Next Entry

Задача: при 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 <QXmlInputSource>

#include <QIODevice>


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 <QXmlDefaultHandler>
#include <QXmlSimpleReader>

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

Tags: , , , ,
Current Mood: [mood icon] satisfied