Решил попробовать класс QXmlStreamReader. Изучив пример streambookmarks я реализовал свой парсер. И вот что мне не понравилось. Можно написать простенький код, который будет сравнивать каждое название ключа на токен isStartElement с нужным нам. Недостаток такого метода - большое количество операций сравнений и падение скорости. Зато очень просто, когда ключи имеют уникальные имена, не нужно контролировать вложенность, родительские отношения. Второй вариант (правильный). На каждый уровень дерева выделяется отдельный метод с while(!atEnd()). Подобное выделение требует создание нового метода для каждого такого уровня. Если глубина дерева в xml'e большая, то можно себе представить какое огромное количество методов получится! Например у меня глубина дерева такая:
tourml/sources/source/packets/packet/packetheader/spo/dates/date
tourml/references/tourtypes/tourtype
Код в студию, можно занятся оптимизаторством и универсальностью кода.
Q_ASSERT(isStartElement() && name() == "TourML");
while (!atEnd()) {
readNext();
if (isStartElement()) {
if (name() == "country") {
qDebug() << attributes().value("nameLat");
} else if (name() == "tour") {
qDebug() << attributes().value("name");
} else if (name() == "spo") {
qDebug() << attributes().value("issue");
} else if (name() == "priceQuantity") {
qDebug() << readElementText();
}
}
}
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();
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
Жесть!
А почему нельзя сделать рекурсию с созданием дерева наследников? Меньше копи/паста одного и того же кода.
Да уж, что-то не очень получается. Если делать рекурсию без дерева наследников, то получается каждый ключ сравнивается со всем списком ключей снова, это замедляет парсинг. Есть мысль сделать дерево наследников таким образом: "tourml/sources/source/packets/packet/packetheader/spo/dates/date" + QStringList list = str.split("/");
Затем в рекурсии проходить по элементам списка и удалять из него пройденные ключи. Но этот вариант мне кажется каким-то странным, слабеньким вариантом XPath.
---
Так, теперь я узнал, что есть разные варианты парсеров: DOM, SAX, StAX. Причем QXmlStreamReader это StAX парсер, который по сути вырос из Pull парсера. StAX и SAX совершенно разные вещи, первый не использует callback функции в отличие от второго.
В общем посмотрел я десяток примеров использования StAX парсера на Java и пришел к выводу, что это, в принципе, нормальный код. В основном, как я и говорил люди выносят циклы while() в отдельные методы и жестко вызывают метод nextTag() (которого кстати нет в Qt, а он пропускает комментарии всё остальное, что не является tag'ом), который устанавливает курсор на следующий элемент, подразумевая тема самым, что если сначала шел Tag = "Name", то за ним обязательно должен идти Tag = "Surname". Т.е. надеются на жесткий порядок следования tag'ов, чтобы уменьшить количество итераций через while(). Тем не менее почти везде такие здоровые портянки. Нигде не обнаружил рекурсии или какой-то специальной оптимизации.
Ты немного не понял, рекурсия 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 - это данные которые тебе нужны для обработки или еще чего там...
}
общая идею думаю должна быть понятна. Да оно всё это понятно, я тоже об этом говорю. Сам подход странный с точки зрения стиля программирования. Это получается некий гибрид Pull и Push парсера. Просто в таком случае тогда проще взять обычный SAX2 (push) парсер - QXmlReader. Но он не поддерживает потоковые данные и не умеет писать xml'и. Да и заполнять надо как-то этот вектор хитро. Например на одну ветку может приходится несколько нужных подветок, тогда придется дублировать имена основной ветки по количеству необходимых дочерних узлов. Или отказываться от вектора и сделать через дерево указателей. Опять же, такое дерево должно отражать некий путь парсинга. Насколько будет удобно изменять этот путь, подстраиваясь под новые форматы xml'ей? А мне надо в итоге написать 4 xml парсера. В принципе как я говорил выше, можно сделать некий аналог XPath'а, распарсить строчки с нужными путями в дерево. Правда в итоге получится новый оверхед. Тут выбор стоит, либо красивый и компактный код, но в жертву приносится скорость парсинга. Либо увеличение размера приложения из-за копи-пастинга и отвратительный код с точки зрения стиля программирования, но шустрый парсер.
Так в том то и дело, что в твоем случае как я понял идут уникальные имена подветок и соответственно не нужно отслеживать, а если нужно наследовать, то достаточно ввести еще один параметр в массив:
struct sCallName
{
QString m_szName; // Имя XML узла
void (*m_pFunc)(void*); // Call back функция которую нужно вызвать для этого имени.
sCallName *m_pParent; // Родитель
};
и все так же с вектором, а так как родитель в вектор попадет раньше дочки, то и указатель на родителя будет и при изменении расположения элементов и их родителей никаких проблем с изменением и никакой путаницы.То, что тут пытаетесь изобразить, называется конечный автомат.
Т. е. нужно написать таблицу состаяний и таблицу переходов.
Таблица переходов - это двумерный массив индексирующийся состоянием и сигналом.
В твоём случае сигнал - это имя тега.
Таблица состояний может просто состаять из указателей на функции обработки.
Тогда код разбора будет примерно такой:
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);
}
Как я и писал выше затык будет в заполнении таблицы. Мы всё-таки дерево парсим, где может быть не один ребенок у одной ветки. Сложно представить как управлять таким автоматом. Я переделал так, мне кажется это золотая середина:
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();
}
}
Такой вопрос насчет QXmlStreamReader::PrematureEndOfDocumentError. Предположим я выкачиваю xml из инета и тут же паршу. Где-то в середине на каком-нибудь "packet" данные заканчиваются (медленная скорость выкачивания). Мне надо добавить новые данные через addData() в буффер, когда эти данные скачаются. Так вот я не понимаю, при возникновении ошибки у парсера 2 выхода - добавить данные или завершится. Если данных пока нет и надо их подождать, то ничего не остается как завершить парсинг и начать всё заново, когда поступят все данные. Напрашивается вариант использовать waitForReadyRead, но у QNetworkReply вроде как этот метод не реализован. Как же заморозить текущее состояние QXmlStreamReader, создавать QEventLoop чтоль и ждать?
---
Подобный вопрос задавался в http://lists.trolltech.com/qt-interest/2008-03/thread00406-0.html 2008 года, но Thiago оставил этот вопрос без ответа.
---
Проштудировал 2 страницы тем на QtCentre где упоминалось слово QXmlStreamReader, полезной информации ноль. Примеры все шуточные, докачка нигде не реализована. Пошел штудировать QtForum.
---
На QtForum ситуация еще хуже, тем 5-6 и все одно и то же мусолят.
На prog.org'е вообще тишина, 2 темы и все не о том.
На vingrad'e только один человек тему завел.
На sources вообще одна тема и то там попался include, а не тема про парсер
На этом форуме поиск тоже выдал только меня.
В общем с этим классом дорожка не проторенная ни в рунете ни в мире. Похоже есть только один выход изучать примеры на .Net и Java.
Нашел забавную http://www.devx.com/Java/Article/30298/1954 про 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);
}
void Tag1()
{
while(!atEnd) {
...
}
}
void Tag2()
{
while(!atEnd) {
...
}
}
void Tag3()
{
while(!atEnd) {
...
}
}
void Tag4()
{
while(!atEnd) {
...
}
}
void moveReaderToElement(const QString &name)
{
while(!atEnd) {
...
if (startElement == name)
break;
}
}
. 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;
}
}
}
<name>Витя</name>
<name>Петя</name>
<name>Вася</name>
<name>Коля</name>
Сегодня заметил http://labs.trolltech.com/blogs/2009/07/22/weather-info-for-qts60/ в блоге на 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();
}
xmlstreamreaderhelper.zip ( 2.43 килобайт )
: 315
В общем я немного заморочился на эту тему и написал класс, который назвал по-страшному - xmlstreamreaderhelper
Несмотря на то, что понять как он работает не просто я постараюсь все же объяснить суть. Все построено на полиморфизме, чтобы избежать дублирование кода. Есть 2 класса:
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();
};
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
}
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.";
}
AbstractTagImplementator references("references", tourMLparser.data());
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();
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
Форум Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)