SABROG |
Дата 29.8.2009, 14:40 |
|
xmlstreamreaderhelper.zip ( 2,43 килобайт )
Кол-во скачиваний: 333
В общем я немного заморочился на эту тему и написал класс, который назвал по-страшному - xmlstreamreaderhelper
Несмотря на то, что понять как он работает не просто я постараюсь все же объяснить суть. Все построено на полиморфизме, чтобы избежать дублирование кода. Есть 2 класса:
- AbstractTagImplementator
- AbstractTagProcessor
Класс AbstractTagProcessor наследует класс AbstractTagImplementator и определяет в себе ряд методов для работы с тэгами xml'я. В то время как класс AbstractTagImplementator определяет общий функционал парсера.
Если разбирать мой xml из прошлых постов, то его парсинг теперь выглядит так:
//определяем в заголовке набор классов, каждый из которых представление необходимых тегов //.h
class TourML : public AbstractTagProcessor { public: TourML(const QString &name = QString(), AbstractTagImplementator *parent = 0); bool beforeEvent(); void afterEvent(); private: QTime t; };
class Country : public AbstractTagProcessor { public: Country(const QString &name = QString(), AbstractTagImplementator *parent = 0); bool beforeEvent(); };
class Packet : public AbstractTagProcessor { public: Packet(const QString &name = QString(), AbstractTagImplementator *parent = 0); bool beforeEvent(); };
В этих классах переопределяются так называемые эвенты базовых классов. Всего 3 эвента: beforeEvent(), event(), afterEvent(). В зависимости от сложности парсера переопределяются нужные. В простом случае нужен только beforeEvent(), а вот если нужно будет перебрать все существующие теги или обработать неизвестные, или просто хочется реализовать свой функционал иначе, то надо переопределять метод event().
//реализация классов тегов //.cpp
TourML::TourML(const QString &name, AbstractTagImplementator *parent) : AbstractTagProcessor(name, parent) { }
bool TourML::beforeEvent() { t.start(); moveToTag("TourML"); if (xml()->attributes().value("version") != "1.0") { xml()->raiseError(QObject::tr("The file is not an TourML version 1.0 file.")); return true; } return false; }
void TourML::afterEvent() { qDebug() << t.elapsed() << "ms."; }
Country::Country(const QString &name, AbstractTagImplementator *parent) : AbstractTagProcessor(name, parent) { }
bool Country::beforeEvent() { qDebug() << xml()->tokenString() << xml()->name() << name() << "Country: " << xml()->attributes().value(QLatin1String("nameLat")).toString(); return true; // interrupt parse }
Packet::Packet(const QString &name, AbstractTagImplementator *parent) : AbstractTagProcessor(name, parent) { }
bool Packet::beforeEvent() { QString packet; moveToTag(QLatin1String("tour")); packet = xml()->attributes().value(QLatin1String("name")).toString(); moveToTag(QLatin1String("spo")); packet += " (" + xml()->attributes().value(QLatin1String("issue")).toString() + ')'; skipSubTree(); moveToTag(QLatin1String("priceQuantity")); packet += " [" + xml()->readElementText() + ']'; qDebug() << packet; return true; //parse next tag }
//Подготовка и вызов парсера //.cpp
QString fileName = "file.xml"; QFile file(fileName); if (!file.open(QFile::ReadOnly | QFile::Text)) { QMessageBox::warning(this, tr("Xml Parser"), tr("Cannot read file %1:\n%2.") .arg(fileName) .arg(file.errorString())); return; }
reader = QSharedPointer<QXmlStreamReader>(new QXmlStreamReader(&file)); tourMLparser = QSharedPointer<TourML>(new TourML("TourML")); TourML::setXmlReader(reader.data()); AbstractTagImplementator references("references", tourMLparser.data()); Country country("country", &references); AbstractTagImplementator sources("sources", tourMLparser.data()); Packet packet("packet", &sources);
if (!tourMLparser->parse() && tourMLparser->xml()->hasError() && tourMLparser->xml()->error() != QXmlStreamReader::PrematureEndOfDocumentError) { qDebug() << "Error when parsing data from socket."; }
Т.е. открываем файл, создаем объект QXmlStreamReader, которому и передаем device (file). Затем создается родительский объект TourML, и по аналогии с QObject'ами тэги-классы выстраиваются в дерево. Потом на корневом теге вызывается метод parse(), который уже проходит по всему дереву детей. Если в процессе парсинга любого тэга xml прервется, при передаче по сокету например, то запускается локальный QEventLoop и ожидает новые данные с устройства. Если они приходят, то парсинг продолжается дальше. Я пробовал генерировать бесконечный поток xml'я по сокету, парсер работает не прерываясь.
Имена, которые передаются в конструкторы объектов - реальные названия тегов в xml'е чувствительные к регистру. Еще вот этот код прокомментирую:
AbstractTagImplementator references("references", tourMLparser.data());
В этом случае тэг "references" является всего-лишь "чекпоинтом", нам тег не нужен сам по себе, но мы должны указать короткую дорогу парсеру, чтобы он мог найти тег "country", поэтому нам нет необходимости наследоваться от класса AbstractTagImplementator и мы используем его базовый функционал, которого достаточно. С другой стороны мы могли бы отказаться от этого чекпоинта и парсер бы все равно нашел нужный тег, однако в разных ситуациях это может замедлить скорость парсинга, т.к. заставит парсер сравнивать имена ключей, которые мы могли бы отмести заранее. А в случае, когда у разных подветок имеются ключи с одинаковым названием это может привести к путанице и невозможности узнать какой из подветок принадлежит тег, чтобы соотнести его.
Как по мне, так это выглядит более правильно, чем мой первоначальный вариант:
Q_ASSERT(isStartElement() && name() == QLatin1String("TourML"));
while (!atEnd()) { readNext(); if (isStartElement()) { if (name() == QLatin1String("references")) { while (!(isEndElement() && name() == QLatin1String("references"))) { readNext(); if (isStartElement() && name() == QLatin1String("countries")) { while (!(isEndElement() && name() == QLatin1String("countries"))) { readNext(); if (isStartElement() && name() == QLatin1String("country")) qDebug() << attributes().value(QLatin1String("nameLat")); } } } } else if (name() == QLatin1String("sources")) { while (!(isEndElement() && name() == QLatin1String("sources"))) { readNext();
if (isStartElement() && name() == QLatin1String("source")) { while (!(isEndElement() && name() == QLatin1String("source"))) { readNext(); if (isStartElement() && name() == QLatin1String("packets")) { while (!(isEndElement() && name() == QLatin1String("packets"))) { readNext(); if (isStartElement() && name() == QLatin1String("packet")) { while (!(isEndElement() && name() == QLatin1String("packet"))) { readNext(); if (isStartElement() && name() == QLatin1String("packetHeader")) { while (!(isEndElement() && name() == QLatin1String("packetHeader"))) { readNext(); if (isStartElement()) { if (name() == QLatin1String("tour")) { qDebug() << attributes().value(QLatin1String("name")); } else if (name() == QLatin1String("spo")) { qDebug() << attributes().value(QLatin1String("issue")); } else if (name() == QLatin1String("spoInfo")) { while (!(isEndElement() && name() == QLatin1String("spoInfo"))) { readNext(); if (isStartElement() && name() == QLatin1String("priceQuantity")) qDebug() << readElementText(); } } } } } } } } } } } } } } }
К тому же замеры скорости не показали каких либо отличительных различий. Классы на C++ я проектирую впервые поэтому был бы рад узнать где я сделал что-то не так и как этом можно улучшить.
Есть мысль написать кодо-генератор для парсера, где можно было бы загрузить дерево xml'я и проставляя галочки выбрать путь для парсера.
Выкладываю для критики и дополнений исходники.
xmlstreamreaderhelper.zip ( 2,43 килобайт )
Кол-во скачиваний: 333
|
Litkevich Yuriy |
Дата 24.7.2009, 13:39 |
|
Цитата(SABROG @ 24.7.2009, 17:27) я немного успокоился, раз уж сами Qt'шники занимаются копи-пастингом |
SABROG |
Дата 24.7.2009, 13:27 |
|
Сегодня заметил статью в блоге на QtSoftware и о том, что в демке используется QXmlStreamReader. Глянув на код я немного успокоился, раз уж сами Qt'шники занимаются копи-пастингом (насчитал 3 вложенных цикла while)...
#define GET_DATA_ATTR xml.attributes().value("data").toString()
void digest(const QString &data) {
QColor textColor = palette().color(QPalette::WindowText); QString unitSystem;
delete m_iconItem; m_iconItem = new QGraphicsSvgItem(); m_scene.addItem(m_iconItem); m_iconItem->setParentItem(m_statusItem); qDeleteAll(m_dayItems); qDeleteAll(m_conditionItems); qDeleteAll(m_rangeItems); qDeleteAll(m_forecastItems); m_dayItems.clear(); m_conditionItems.clear(); m_rangeItems.clear(); m_forecastItems.clear();
QXmlStreamReader xml(data); while (!xml.atEnd()) { xml.readNext(); if (xml.tokenType() == QXmlStreamReader::StartElement) { if (xml.name() == "city") { city = GET_DATA_ATTR; setWindowTitle(city); } if (xml.name() == "unit_system") unitSystem = xml.attributes().value("data").toString(); // Parse current weather conditions if (xml.name() == "current_conditions") { while (!xml.atEnd()) { xml.readNext(); if (xml.name() == "current_conditions") break; if (xml.tokenType() == QXmlStreamReader::StartElement) { if (xml.name() == "condition") { m_conditionItem->setPlainText(GET_DATA_ATTR); } if (xml.name() == "icon") { QString name = extractIcon(GET_DATA_ATTR); if (!name.isEmpty()) { delete m_iconItem; m_iconItem = new QGraphicsSvgItem(name); m_scene.addItem(m_iconItem); m_iconItem->setParentItem(m_statusItem); } } if (xml.name() == "temp_c") { QString s = GET_DATA_ATTR + QChar(176); m_temperatureItem->setPlainText(s); } } } } // Parse and collect the forecast conditions if (xml.name() == "forecast_conditions") { QGraphicsTextItem *dayItem = 0; QGraphicsSvgItem *statusItem = 0; QString lowT, highT; while (!xml.atEnd()) { xml.readNext(); if (xml.name() == "forecast_conditions") { if (dayItem && statusItem && !lowT.isEmpty() && !highT.isEmpty()) { m_dayItems << dayItem; m_conditionItems << statusItem; QString txt = highT + '/' + lowT; QGraphicsTextItem* rangeItem; rangeItem = m_scene.addText(txt); rangeItem->setDefaultTextColor(textColor); m_rangeItems << rangeItem; QGraphicsRectItem *box; box = m_scene.addRect(0, 0, 10, 10); box->setPen(Qt::NoPen); box->setBrush(Qt::NoBrush); m_forecastItems << box; dayItem->setParentItem(box); statusItem->setParentItem(box); rangeItem->setParentItem(box); } else { delete dayItem; delete statusItem; } break; } if (xml.tokenType() == QXmlStreamReader::StartElement) { if (xml.name() == "day_of_week") { QString s = GET_DATA_ATTR; dayItem = m_scene.addText(s.left(3)); dayItem->setDefaultTextColor(textColor); } if (xml.name() == "icon") { QString name = extractIcon(GET_DATA_ATTR); if (!name.isEmpty()) { statusItem = new QGraphicsSvgItem(name); m_scene.addItem(statusItem); } } if (xml.name() == "low") lowT = toCelcius(GET_DATA_ATTR, unitSystem); if (xml.name() == "high") highT = toCelcius(GET_DATA_ATTR, unitSystem); } } }
} }
m_timeLine.stop(); layoutItems(); animate(0); m_timeLine.start(); }
|
SABROG |
Дата 22.7.2009, 15:33 |
|
Нашел забавную статью про StAX, пример рассматривается на Java и XmlStreamReader. Забавно в ней то каким образом они решают проблему здоровых циклов while(). Смысл следующий. Создается абстрактный базовый класс типа ComponentParser. Есть 2 ключа "author" и "entry", при вхождении в эти ключи должен вызываться свой парсер. Поэтому на эти ключи создается 2 класса AuthorParser и EntryParser, оба на базе ComponentParser. Создается map (типа QMap) - {"ИмяКлюча":ОбъектНаБазеComponentParser}. Уже начинает напоминать наши callback'и. Внутри каждого такого ComponentParser'а также есть свой map для дочерних узлов. Вызов метода парсинга для ключа выглядит так:
if (delegates.containsKey(element)) { ComponentParser parser = (ComponentParser) delegates.get(element); parser.parse(staxXmlReader); }
delegates - наш map, parser - объект, который отвечает за парсинг конкретного ключа.
На мой взгляд это мало чем отличается от этого:
void Tag1() { while(!atEnd) { ... } }
void Tag2() { while(!atEnd) { ... } }
void Tag3() { while(!atEnd) { ... } }
void Tag4() { while(!atEnd) { ... } }
Просто они взяли "детский" пример (глубина дерева 1 уровень) xml'я, да еще умудрились наворотить всего вокруг него, а циклы while() заменили на StaxUtil.moveReaderToElement("name",staxXmlReader);, по сути одно и тоже что и это:
void moveReaderToElement(const QString &name) { while(!atEnd) { ... if (startElement == name) break; } } .
В моем предыдущем варианте я использовал свой метод readNext(), который пропускал энное количество ключей. В этой статье предпочитают не пропускать ключи, а искать нужный. Минус в том, что это дополнительные операции сравнения строк имен на каждом ключе. Плюс в том, что код выглядит не уродско. Снова надо делать выбор, либо красота кода, либо скорость парсинга. Таким образом выходит, что для удобной работы в классе QXmlStreamReader не хватает методов типа: nextTag, nextTag("name"), skipSubTree() и возможности работать асинхронно в цикле событий как QNetworkAccessManager, или потокового варианта QXmlStreamReaderThreaded. Дело в том, что если устройство установлено через setDevice, то парсер не увидит новых приходящих данных, скажем по сокету пока у парсера не будет возможности дать отработать циклу событий. QEventLoop хоть и работает, но это имхо костыль. Интерфейс программы размораживается при таком раскладе, значит нужен модальный диалог. Также встает вопрос о прерывании парсинга. Это все таки цикл, значит надо каким-то образом дать знать парсеру, что пора выходить. --- Кстати в статье упоминается метод parseElement, на самом деле это опечатка, в исходниках метод parse(). Еще нашел в исходниках реализацию метода moveReaderToElement:
public static void moveReaderToElement(String target, XMLStreamReader reader) throws XMLStreamException {
// If current element is equal to target
String readElement = null; for (int event = reader.next(); event != XMLStreamConstants.END_DOCUMENT; event = reader.next()) {
if ((event == XMLStreamConstants.START_ELEMENT) && (reader.getLocalName().equals(target))) { return; } } }
Он хорош только для ключей в одном экземпляре. Если в xml файле будут списки типа:
<name>Витя</name> <name>Петя</name> <name>Вася</name> <name>Коля</name>
То нужен цикл while() и проверка на выход из поддерева иначе парсер поползет дальше по всему xml'ю до самого конца. К сожалению, как я уже говорил, пример в статье слабый, хотя и пишется, что ориентирован он на серьезные xml'и. Продумали не все. |
SABROG |
Дата 15.7.2009, 21:35 |
|
Такой вопрос насчет QXmlStreamReader::PrematureEndOfDocumentError. Предположим я выкачиваю xml из инета и тут же паршу. Где-то в середине на каком-нибудь "packet" данные заканчиваются (медленная скорость выкачивания). Мне надо добавить новые данные через addData() в буффер, когда эти данные скачаются. Так вот я не понимаю, при возникновении ошибки у парсера 2 выхода - добавить данные или завершится. Если данных пока нет и надо их подождать, то ничего не остается как завершить парсинг и начать всё заново, когда поступят все данные. Напрашивается вариант использовать waitForReadyRead, но у QNetworkReply вроде как этот метод не реализован. Как же заморозить текущее состояние QXmlStreamReader, создавать QEventLoop чтоль и ждать? --- Подобный вопрос задавался в рассылке 2008 года, но Thiago оставил этот вопрос без ответа. --- Проштудировал 2 страницы тем на QtCentre где упоминалось слово QXmlStreamReader, полезной информации ноль. Примеры все шуточные, докачка нигде не реализована. Пошел штудировать QtForum. --- На QtForum ситуация еще хуже, тем 5-6 и все одно и то же мусолят. На prog.org'е вообще тишина, 2 темы и все не о том. На vingrad'e только один человек тему завел. На sources вообще одна тема и то там попался include, а не тема про парсер На этом форуме поиск тоже выдал только меня. В общем с этим классом дорожка не проторенная ни в рунете ни в мире. Похоже есть только один выход изучать примеры на .Net и Java. |
SABROG |
Дата 15.7.2009, 13:16 |
|
Как я и писал выше затык будет в заполнении таблицы. Мы всё-таки дерево парсим, где может быть не один ребенок у одной ветки. Сложно представить как управлять таким автоматом. Я переделал так, мне кажется это золотая середина:
bool XmlSpoListParser::read(QIODevice *device) { setDevice(device);
while (!atEnd()) { readNext();
if (isStartElement()) { if (name() == QLatin1String("TourML") && attributes().value(QLatin1String("version")) == QLatin1String("1.0")) { readSpoList(); break; } else { raiseError(QObject::tr("The file is not an TourML version 1.0 file.")); break; } } }
return !error(); }
void XmlSpoListParser::readSpoList() { Q_ASSERT(isStartElement() && name() == QLatin1String("TourML"));
while (!atEnd()) { readNext();
if (isStartElement()) { if (name() == QLatin1String("references")) { readCountries(); } else if (name() == QLatin1String("sources")) { readTours(); } else { skipSubTree(); } } }
void XmlSpoListParser::readCountries() { Q_ASSERT(isStartElement() && name() == QLatin1String("references"));
readNext(); //skip "countries" tag readNext(); //skip "Characters" token
while (!(isEndElement() && name() == QLatin1String("references"))) { readNext(); if (isStartElement()) { if (name() == QLatin1String("country")) { qDebug() << attributes().value(QLatin1String("nameLat")); } else { skipSubTree(); } } } }
void XmlSpoListParser::readTours() { Q_ASSERT(isStartElement() && name() == QLatin1String("sources"));
//something like nextTag() in Java, reduce while cycles
readNext(); //skip "source" tag readNext(); //skip "Characters" token readNext(); //skip "quotaServices" tag readNext(); //skip "Characters" token readNext(); //skip "endElement" token readNext(); //skip "packets" tag readNext(); //skip "Characters" token
while (!(isEndElement() && name() == QLatin1String("sources"))) { readNext(); if (isStartElement()) { if (name() == QLatin1String("packet")) { readPacket(); } else { skipSubTree(); } } } }
void XmlSpoListParser::readPacket() { Q_ASSERT(isStartElement() && name() == QLatin1String("packet")); readNext(); //skip "packetHeader" tag readNext(); //skip "Characters" token
while (!(isEndElement() && name() == QLatin1String("packet"))) { readNext(); if (isStartElement()) { if (name() == QLatin1String("tour")) qDebug() << attributes().value(QLatin1String("name")); else if (name() == QLatin1String("spo")) qDebug() << attributes().value(QLatin1String("issue")); else if (name() == QLatin1String("spoInfo")) { readNext(); //move to "priceQuantity" tag readNext(); //skip "Characters" token qDebug() << readElementText(); } else skipSubTree(); } } }
void XmlSpoListParser::skipSubTree() { Q_ASSERT(isStartElement());
while (!atEnd()) { readNext(); if (isEndElement()) break; if (isStartElement()) skipSubTree(); } }
Кстати как я не изгалялся над алгоритмом время парсинга почему-то всегда константно: 297ms. Если использую qDebug() для вывода в консоль, то время парсинга увеличивается до 1048ms. Правда у этого варианта тоже есть существенный недостаток, чтобы использовать стриминг надо проверять на ошибку после каждого readNext() иначе есть шанс прочитать несуществующие аттрибуты или тектовые элементы. |
Tonal |
Дата 15.7.2009, 12:38 |
|
То, что тут пытаетесь изобразить, называется конечный автомат. Т. е. нужно написать таблицу состаяний и таблицу переходов. Таблица переходов - это двумерный массив индексирующийся состоянием и сигналом. В твоём случае сигнал - это имя тега. Таблица состояний может просто состаять из указателей на функции обработки.
Тогда код разбора будет примерно такой:
typedef QVector<QHash<int> > transit_table_t; typedef void(func_t*)(args); QVector<func_t> state_table_t
transit_table_t transit_table; state_table_t state_table;
size_t curr_state = 0; while (!atEnd()) { readNext(); if (!isStartElement()) continue; curr_state = transit_table[curr_state][name()]; if (func_t func = state_table[curr_state]) func(args); }
Заполнение таблиц и точные типы по вкусу. |
ViGOur |
Дата 15.7.2009, 12:24 |
|
Так в том то и дело, что в твоем случае как я понял идут уникальные имена подветок и соответственно не нужно отслеживать, а если нужно наследовать, то достаточно ввести еще один параметр в массив:
struct sCallName { QString m_szName; // Имя XML узла void (*m_pFunc)(void*); // Call back функция которую нужно вызвать для этого имени.
sCallName *m_pParent; // Родитель }; и все так же с вектором, а так как родитель в вектор попадет раньше дочки, то и указатель на родителя будет и при изменении расположения элементов и их родителей никаких проблем с изменением и никакой путаницы.
Вплоть до того, что ты сам программно сможешь менять значения, положения элементов и их родителей, а потом сохранять. Только в структуру нужно будет еще несколько аргументов ввести. |
SABROG |
Дата 15.7.2009, 11:14 |
|
Да оно всё это понятно, я тоже об этом говорю. Сам подход странный с точки зрения стиля программирования. Это получается некий гибрид Pull и Push парсера. Просто в таком случае тогда проще взять обычный SAX2 (push) парсер - QXmlReader. Но он не поддерживает потоковые данные и не умеет писать xml'и. Да и заполнять надо как-то этот вектор хитро. Например на одну ветку может приходится несколько нужных подветок, тогда придется дублировать имена основной ветки по количеству необходимых дочерних узлов. Или отказываться от вектора и сделать через дерево указателей. Опять же, такое дерево должно отражать некий путь парсинга. Насколько будет удобно изменять этот путь, подстраиваясь под новые форматы xml'ей? А мне надо в итоге написать 4 xml парсера. В принципе как я говорил выше, можно сделать некий аналог XPath'а, распарсить строчки с нужными путями в дерево. Правда в итоге получится новый оверхед. Тут выбор стоит, либо красивый и компактный код, но в жертву приносится скорость парсинга. Либо увеличение размера приложения из-за копи-пастинга и отвратительный код с точки зрения стиля программирования, но шустрый парсер. |
ViGOur |
Дата 15.7.2009, 10:32 |
|
Ты немного не понял, рекурсия c примерно таким условием:
struct sCallName { QString m_szName; // Имя XML узла void (*m_pFunc)(void*); // Call back функция которую нужно вызвать для этого имени. };
// ...
QVector<sCallName*> m_vecNames;
// Где-то m_vecNames заполняется // ...
while( !isEndElement() && name() == m_vecNames[i].m_szName) { readNext(); m_vecNames[i].(*m_pFunc)( pVoid); // pVoid - это данные которые тебе нужны для обработки или еще чего там... } общая идею думаю должна быть понятна. если не понятно, постараюсь описать подробней. |
Просмотр темы полностью (откроется в новом окне) |
|