Здравствуйте, гость ( Вход | Регистрация )
SABROG | Дата 29.8.2009, 14:40 |
![]() В общем я немного заморочился на эту тему и написал класс, который назвал по-страшному - xmlstreamreaderhelper ![]() Несмотря на то, что понять как он работает не просто я постараюсь все же объяснить суть. Все построено на полиморфизме, чтобы избежать дублирование кода. Есть 2 класса:
Класс AbstractTagProcessor наследует класс AbstractTagImplementator и определяет в себе ряд методов для работы с тэгами xml'я. В то время как класс AbstractTagImplementator определяет общий функционал парсера. Если разбирать мой xml из прошлых постов, то его парсинг теперь выглядит так: //определяем в заголовке набор классов, каждый из которых представление необходимых тегов //.h
В этих классах переопределяются так называемые эвенты базовых классов. Всего 3 эвента: beforeEvent(), event(), afterEvent(). В зависимости от сложности парсера переопределяются нужные. В простом случае нужен только beforeEvent(), а вот если нужно будет перебрать все существующие теги или обработать неизвестные, или просто хочется реализовать свой функционал иначе, то надо переопределять метод event(). //реализация классов тегов //.cpp
//Подготовка и вызов парсера //.cpp
Т.е. открываем файл, создаем объект QXmlStreamReader, которому и передаем device (file). Затем создается родительский объект TourML, и по аналогии с QObject'ами тэги-классы выстраиваются в дерево. Потом на корневом теге вызывается метод parse(), который уже проходит по всему дереву детей. Если в процессе парсинга любого тэга xml прервется, при передаче по сокету например, то запускается локальный QEventLoop и ожидает новые данные с устройства. Если они приходят, то парсинг продолжается дальше. Я пробовал генерировать бесконечный поток xml'я по сокету, парсер работает не прерываясь. Имена, которые передаются в конструкторы объектов - реальные названия тегов в xml'е чувствительные к регистру. Еще вот этот код прокомментирую:
В этом случае тэг "references" является всего-лишь "чекпоинтом", нам тег не нужен сам по себе, но мы должны указать короткую дорогу парсеру, чтобы он мог найти тег "country", поэтому нам нет необходимости наследоваться от класса AbstractTagImplementator и мы используем его базовый функционал, которого достаточно. С другой стороны мы могли бы отказаться от этого чекпоинта и парсер бы все равно нашел нужный тег, однако в разных ситуациях это может замедлить скорость парсинга, т.к. заставит парсер сравнивать имена ключей, которые мы могли бы отмести заранее. А в случае, когда у разных подветок имеются ключи с одинаковым названием это может привести к путанице и невозможности узнать какой из подветок принадлежит тег, чтобы соотнести его. Как по мне, так это выглядит более правильно, чем мой первоначальный вариант: Раскрывающийся текст
К тому же замеры скорости не показали каких либо отличительных различий. Классы на C++ я проектирую впервые поэтому был бы рад узнать где я сделал что-то не так и как этом можно улучшить. Есть мысль написать кодо-генератор для парсера, где можно было бы загрузить дерево xml'я и проставляя галочки выбрать путь для парсера. Выкладываю для критики и дополнений исходники. ![]() |
|
Litkevich Yuriy | Дата 24.7.2009, 13:39 |
я немного успокоился, раз уж сами Qt'шники занимаются копи-пастингом ![]() |
|
SABROG | Дата 24.7.2009, 13:27 |
Сегодня заметил статью в блоге на QtSoftware и о том, что в демке используется QXmlStreamReader. Глянув на код я немного успокоился, раз уж сами Qt'шники занимаются копи-пастингом (насчитал 3 вложенных цикла while)... Сиськи Здесь
|
|
SABROG | Дата 22.7.2009, 15:33 |
Нашел забавную статью про StAX, пример рассматривается на Java и XmlStreamReader. Забавно в ней то каким образом они решают проблему здоровых циклов while(). Смысл следующий. Создается абстрактный базовый класс типа ComponentParser. Есть 2 ключа "author" и "entry", при вхождении в эти ключи должен вызываться свой парсер. Поэтому на эти ключи создается 2 класса AuthorParser и EntryParser, оба на базе ComponentParser. Создается map (типа QMap) - {"ИмяКлюча":ОбъектНаБазеComponentParser}. Уже начинает напоминать наши callback'и. Внутри каждого такого ComponentParser'а также есть свой map для дочерних узлов. Вызов метода парсинга для ключа выглядит так:
delegates - наш map, parser - объект, который отвечает за парсинг конкретного ключа. На мой взгляд это мало чем отличается от этого:
Просто они взяли "детский" пример (глубина дерева 1 уровень) xml'я, да еще умудрились наворотить всего вокруг него, а циклы while() заменили на StaxUtil.moveReaderToElement("name",staxXmlReader);, по сути одно и тоже что и это: .В моем предыдущем варианте я использовал свой метод readNext(), который пропускал энное количество ключей. В этой статье предпочитают не пропускать ключи, а искать нужный. Минус в том, что это дополнительные операции сравнения строк имен на каждом ключе. Плюс в том, что код выглядит не уродско. Снова надо делать выбор, либо красота кода, либо скорость парсинга. Таким образом выходит, что для удобной работы в классе QXmlStreamReader не хватает методов типа: nextTag, nextTag("name"), skipSubTree() и возможности работать асинхронно в цикле событий как QNetworkAccessManager, или потокового варианта QXmlStreamReaderThreaded. Дело в том, что если устройство установлено через setDevice, то парсер не увидит новых приходящих данных, скажем по сокету пока у парсера не будет возможности дать отработать циклу событий. QEventLoop хоть и работает, но это имхо костыль. Интерфейс программы размораживается при таком раскладе, значит нужен модальный диалог. Также встает вопрос о прерывании парсинга. Это все таки цикл, значит надо каким-то образом дать знать парсеру, что пора выходить. --- Кстати в статье упоминается метод parseElement, на самом деле это опечатка, в исходниках метод parse(). Еще нашел в исходниках реализацию метода moveReaderToElement:
Он хорош только для ключей в одном экземпляре. Если в xml файле будут списки типа:
То нужен цикл 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 |
Как я и писал выше затык будет в заполнении таблицы. Мы всё-таки дерево парсим, где может быть не один ребенок у одной ветки. Сложно представить как управлять таким автоматом. Я переделал так, мне кажется это золотая середина: Раскрывающийся текст
Кстати как я не изгалялся над алгоритмом время парсинга почему-то всегда константно: 297ms. Если использую qDebug() для вывода в консоль, то время парсинга увеличивается до 1048ms. Правда у этого варианта тоже есть существенный недостаток, чтобы использовать стриминг надо проверять на ошибку после каждого readNext() иначе есть шанс прочитать несуществующие аттрибуты или тектовые элементы. |
|
Tonal | Дата 15.7.2009, 12:38 |
То, что тут пытаетесь изобразить, называется конечный автомат. ![]() Т. е. нужно написать таблицу состаяний и таблицу переходов. Таблица переходов - это двумерный массив индексирующийся состоянием и сигналом. В твоём случае сигнал - это имя тега. Таблица состояний может просто состаять из указателей на функции обработки. Тогда код разбора будет примерно такой:
Заполнение таблиц и точные типы по вкусу. ![]() |
|
ViGOur | Дата 15.7.2009, 12:24 |
Так в том то и дело, что в твоем случае как я понял идут уникальные имена подветок и соответственно не нужно отслеживать, а если нужно наследовать, то достаточно ввести еще один параметр в массив: и все так же с вектором, а так как родитель в вектор попадет раньше дочки, то и указатель на родителя будет и при изменении расположения элементов и их родителей никаких проблем с изменением и никакой путаницы.Вплоть до того, что ты сам программно сможешь менять значения, положения элементов и их родителей, а потом сохранять. Только в структуру нужно будет еще несколько аргументов ввести. ![]() |
|
SABROG | Дата 15.7.2009, 11:14 |
Да оно всё это понятно, я тоже об этом говорю. Сам подход странный с точки зрения стиля программирования. Это получается некий гибрид Pull и Push парсера. Просто в таком случае тогда проще взять обычный SAX2 (push) парсер - QXmlReader. Но он не поддерживает потоковые данные и не умеет писать xml'и. Да и заполнять надо как-то этот вектор хитро. Например на одну ветку может приходится несколько нужных подветок, тогда придется дублировать имена основной ветки по количеству необходимых дочерних узлов. Или отказываться от вектора и сделать через дерево указателей. Опять же, такое дерево должно отражать некий путь парсинга. Насколько будет удобно изменять этот путь, подстраиваясь под новые форматы xml'ей? А мне надо в итоге написать 4 xml парсера. В принципе как я говорил выше, можно сделать некий аналог XPath'а, распарсить строчки с нужными путями в дерево. Правда в итоге получится новый оверхед. Тут выбор стоит, либо красивый и компактный код, но в жертву приносится скорость парсинга. Либо увеличение размера приложения из-за копи-пастинга и отвратительный код с точки зрения стиля программирования, но шустрый парсер. | |
ViGOur | Дата 15.7.2009, 10:32 |
Ты немного не понял, рекурсия c примерно таким условием: общая идею думаю должна быть понятна. ![]() если не понятно, постараюсь описать подробней. |
|
Просмотр темы полностью (откроется в новом окне) | |
![]() |
|
Текстовая версия | Сейчас: 11.7.2025, 15:30 |