Дневник Шестеро Михаила

Oct. 7th, 2014

04:29 pm - Чтение простого XML с помощью RapidXML

Я люблю хранить технологическую конфигурацию моих программ и утилит в XML-файле.
Пока я делал приложения с использованием Qt использовал небольшой класс, который читал эту конфигурацию и сохранял её в QStringMap.

Однако возникла потребность сделать простую утилиту на C++ но без Qt, XML-парсером которой я пользовался.
Типичный формат моего XML таков:

<?xml version="1.0" encoding="utf-8"?>
<registry>
    <version>1</version>
    <section id="common" >
        <parameter id="locale" >ru_RU.utf8</parameter>

        <parameter id="parameter1" >value1</parameter>
        <parameter id="parameter2" >value2</parameter>
    </section>
</registry>
Для чтения я без проблем воспользовался бесплатной библиотекой RapidXML 1.13. Сейчас она входит в Boost, но я попробовал также использовать её и отдельно (при сборке с помощью GNU C++/MinGW под Windows). Собственно библиотека эта состоит из трёх шабонных HPP-файлов, которые не требуют компиляции.

Публикую код, который читает XML и сохраняет конфигурацию в STL-коллекцию:
//#include <rapidxml.hpp>
//using namespace rapidxml;
#include <boost/property_tree/detail/rapidxml.hpp>
using namespace boost::property_tree::detail::rapidxml;

// .................

    // ---
    xml_document<char> doc;

    //cout << "Parse..." << endl;

    try {
        ifstream file( fname_xml.c_str() );
        vector<char> buffer((istreambuf_iterator<char>(file)), istreambuf_iterator<char>());
        buffer.push_back('\0');

        doc.parse<0>( &buffer[0] );
    }
    catch (parse_error e) {
        cerr << "Fail. Parse error " << e.what() << endl;
        return 0;
    }

    xml_node<> *root_node = doc.first_node();
    //cout << "Root found?" << endl;

    if (root_node!=NULL)
    {
        //cout << "Root found!" << root_node->name() << ":" << root_node->value() << endl;
        xml_node<> *node = root_node->first_node("section");
        //if (node!=NULL) cout << "section found" << endl;
        if (node) node = node->first_node("parameter");
        //if (node!=NULL) cout << "first parameter found" << endl;
        while (node!=NULL)
        {
            xml_attribute<>* a = node->first_attribute("id");
            if (a!=NULL)
            {
                config[ a->value() ] = node->value();
            }

            node = node->next_sibling();
        }
    }
    cout << "========" << endl << "Check loaded configuration:" << endl;

    typedef map<string,string>::const_iterator it;
    for (it i=config.begin(); i!=config.end(); i++)
    {
        cout << i->first << "\t= " << i->second << endl;
    }
    cout << "Configuration loaded" << endl;
    // ---
    

Стоит добавить, что когда я подставлял в doc.parse<0>( ... ) для проверки забитую в коде текстовую константу, это приводило к падению программы во время работы парсера (run-time exception). Так получалось потому, что RapidXML для скорости применяет "разрушающий" разбор - точнее в процессе работы он заменяет символы после концов символьных сущонстей XML нулевыми байтами. Таким образом они превращаются в "отдельные" строки с точки зрения C/C++ без копирования их содержимого.

Tags: , , , , ,
Current Mood: busy

Jun. 9th, 2014

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

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