Winner Code
Veni, vidi, programmare!
Veni, vidi, programmare!
26 июля 2011
Здравствуйте. Сегодня я опишу создание приложения для слежения за посылками от доблестной и уважаемой “Почты России”. Я раскрою такие темы:
Статья написана для одного моего замечательного друга — Алексея. Надеюсь, он поймет и усвоит весь материал, который я опишу в этой статье.
Для начала разберемся с инструментарием и разобьем работу на шаги. В качестве ЯП я выбрал C++, в качестве GUI/рендера — любимую библиотеку Qt. Все это будет кодиться и собираться под QtCreator. Благодаря выбранному инструментарию программа получится полностью кроссплатформенной.
Опишу процесс работы программы: окно с полем ввода для номера посылки, кнопка. По нажатию на кнопку получаем неведомым образом таблицу из сайта Почты России и выводим в нашем окне через веб-компонент (и правда, зачем парсить возвращаемую html-таблицу, перерисовывать на свой интерфейс в программе, если можно взять и отобразить этот html прямо в окне).
Конечно же, я опишу процесс “вытаскивания” значений из таблицы для дальнейшей обработки (email оповещение и т.д.).
Вот ссылка, по которой можно получить состояние посылки: http://www.russianpost.ru/resp_engine.aspx?Path=rp/servise/ru/home/postuslug/trackingpo.
По её структуре сразу заметно, какого качества программисты писали им сайт.
Как я уже не раз писал в своих статьях по парсингу, нам понадобится удобный плагин под FireFox — FireBug. С его помощью мы сможем отследить куда и какой запрос идет, чтобы получить нужный ответ. Другими словами, после ввода трекинг-номера в форму мы сможем увидеть, куда посылается POST-запрос и какие параметры посылаются вообще.

Для отслеживания любых запросов, нужно перейти на вкладку Net. Теперь вставьте любой трекинг-номер в поле и нажмите кнопку Найти.

Красным я выделил нужный нам POST-запрос, давайте посмотрим на детальную информацию о нем (нажмите просто). В появившемся поле перейдите на вкладку POST, там увидите поля с их значениями, которые передаются запросом. На вкладке RESPONSE будет возвращаемый html, который и содержит таблицу с информацией.

BarCode — поле, которое содержит наш трекинг-номер. Ещё, как видно, CDAY, CMONTH, CYEAR содержат информацию о текущей дате (странные они, зачем пост-запросом отправлять информацию, если это же можно получить в скрипте на сервере автоматически).
HTML в Response-вкладке мы пока рассматривать не будем, вернемся к этому позже.
Создаем новый Qt GUI Application проект. Сразу после запуска мы увидим небольшое пустое окно. Давайте набросаем на него все нужные виджеты таким образом:

Имя кнопки — doCheck, поля ввода: trackingNumber, QWebView (белое большое поле, в котором будем отображать html) — htmlData.
Ещё момент, чтобы использовать QWebView (компонент, использующий webkit для рендера html), нам нужно открыть pro-файл проекта и добавить 2 подключения 2 модулей (network, webkit):
QT += core gui network webkit xml
Нажмите правой кнопкой по кнопке и выберите пункт Go to slot... В появившемся окне выберите первый сигнал — clicked(). После нажатия автоматически сгенерируется слот, в котором нужно отписать код, реагирующий на нажатие кнопки.
Есть неплохая статья (http://vasinnet.blogspot.com/2010/01/post-qt-qnetworkaccessmanager.html) В ней описывается отправка POST-запросов с помощью QNetworkAccessManager.
Для осуществления запроса нам нужно получить данные: трекинг номер из поля ввода, текущую дату (частями). Первое нужно реализовать в слоте-обработчике нажатия кнопки (мы его создали в предыдущем пункте).
QDate curDate = QDate::currentDate(); QString day = QVariant(curDate.day()).toString(); QString month = QVariant(curDate.month()).toString(); QString year = QVariant(curDate.year()).toString();
С помощью этого кода мы можем получить текущую дату (день, месяц, год). Подробнее можно почитать в документации по QDateTime классам. Также отмечу, что нормального метода перевода int → QString я не нашел, поэтому приходится извращаться, переводя int в QVariant (аналог boost::any), а его уже в строку.
// Получаем трекинг-номер из поля ввода QString number = ui->trackingNumber->text(); // Составляем пост-запрос. Основу для строки я взял из FireBug'a в поле POST-запроса QString postRequest = "OP=&PATHCUR=rp/servise/ru/home/postuslug/trackingpo&PATHFROM=&WHEREONOK=&ASP=&PARENTID=&FORUMID=&" "NEWSID=&DFROM=&DTO=&CA=&CDAY=" + day + "&CMONTH=" + month + "&CYEAR=" + year + "&NAVCURPAGE=&" "SEARCHTEXT=&searchAdd=&PATHWEB=RP/INDEX/RU/Home&PATHPAGE=RP/INDEX/RU/Home/Search&search1=&" "BarCode=" + number + "&searchsign=1"; qDebug() << postRequest;
Первой строкой я получаю трекинг-номер из поля ввода, а далее я генерирую post-запрос. Как я уже писал раньше в своих статьях, post-запросы передаются в виде: name1=value1&name2=value2&...
Строку, которую вы видите вверху, я получил из FireBug'a в разделе POST-запроса, в параметрах (выше есть скрин, в самом низу сгенерирована эта строка). Только нужно заменить данные, например CDAY, CMONTH на наши переменные.
// Вот здесь находится скрипт, обрабатывающий пост-запросы QString siteUrl = "http://www.russianpost.ru/resp_engine.aspx?Path=rp/servise/ru/home/postuslug/trackingpo"; QNetworkAccessManager *pManager = new QNetworkAccessManager; connect(pManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinish(QNetworkReply*))); pManager->post(QNetworkRequest(QUrl(siteUrl)), postRequest.toUtf8());
Отправляется пост-запрос вот таким вот простым кодом. Всем этим управляет класс QNetworkAccessManager. В нем есть сигнал finished, вызывается, когда запрос завершен и получен ответ. В качестве слота нужно создать:
private slots: void replyFinish(QNetworkReply*);
В классе вашего окна, и именно к этому слоту подключать сигнал. (Кто не понял, к статье приложу архив проекта).
void MainWindow::replyFinish(QNetworkReply *reply) { QString answer = QString::fromUtf8(reply->readAll()); //qDebug() << answer; QDomDocument doc; doc.setContent(answer); QDomNodeList tables = doc.elementsByTagName("table"); QDomNode table1 = tables.item(9); QDomNode table2 = tables.item(10); // … Далее будет }
В классе ответа QNetworkReply есть метод readAll, с его помощью можно получить текстовую составляющую ответа. Если теперь вывести переменную answer, то вы увидите html-код вернувшейся страницы, на которой находится наша таблица. Вся задача — вытащить эту таблицу.
Рассказ о базовой работе с DOM и QDocument занял бы много времени, поэтому я взамен буду только предлагать ссылки на документацию.
setConent из QDomDocument позволяет вручную установить html-содержимое. Теперь у нас есть некоторые функционал для манипулирования и поиском в DOM-дереве.
Кто хочет “крутого” кода, можно с помощью firebug'a найти таблицу в html'e и посмотреть её класс или прочие атрибуты, по которым можно искать, но чтобы не усложнять статью я методом тыка перебрал все таблицы и нашел нужные мне.
Функция getElementsByTagName возвращает нам все хендлы на переданный тег (в нашем случае — все таблицы). Перебором, как я уже говорил, нашел таблицу с информацией по отправке).
Осталось только перевести каким-то методом полученные хендлы таблиц в html-вид и передать браузеру, но как? Минут 5 гугления по документации и я нашел работоспособный код.
QString htmlTable; QTextStream stream(&htmlTable); table1.save(stream, 2); table2.save(stream, 2); qDebug() << htmlTable;
В классе QDomNode есть метод: save, он принимает на вход stream (поток, например: файл, строка, ввод) и количество пробелов для отделения между тегами (форматирование кода). Как видите по коду, я подцепил строку htmlTable к TextStream и передал этот поток в save-функцию. На выходе я получил строку htmlTable, внутри которой html-отображение 2 таблиц.
Осталось самое простое — передать этот html в браузер, делается это одной строкой:
ui->htmlData->setHtml(htmlTable);
В следующей статье я опишу, как автоматически отсылать запрос и по изменению состояния оповещать владельца имейлом об изменениях в статусе заказа.
24 августа 2011, 19:17
Нормальный метод перевода int в QString - это QString::number().
Кроме этого там есть метод arg(), с помощью которого можно подставлять в строку значения вместо плейсхолдеров вида {0}, {1} (так, имхо, удобнее формировать запрос)
14 сентября 2011, 18:01
В общем случае некорректно разбирать html при помощи QDomDocument, очень часто html не является валидным xml-ем.
И еще у вас нигде не удаляется менеджер, опять же тут это некритично, но лучше сразу создавать его корректно pManager = new QNetworkAccessManager(this);
16 сентября 2011, 17:12
Oleg, спасибо за дополнение.