Здравствуйте. Сегодня я опишу создание приложения для слежения за посылками от доблестной и уважаемой “Почты России”. Я раскрою такие темы:

  • С++ & QtCreator
  • Разбор DOM-дерева полученной страницы
  • Работа с QDateTime, получение текущей даты в Qt
  • Работа с FireBug
  •    Работа с формами
  •    Отслеживание POST-запросов

Статья написана для одного моего замечательного друга — Алексея. Надеюсь, он поймет и усвоит весь материал, который я опишу в этой статье.

Подготовка

Для начала разберемся с инструментарием и разобьем работу на шаги. В качестве ЯП я выбрал C++, в качестве GUI/рендера — любимую библиотеку Qt. Все это будет кодиться и собираться под QtCreator. Благодаря выбранному инструментарию программа получится полностью кроссплатформенной.

Опишу процесс работы программы: окно с полем ввода для номера посылки, кнопка. По нажатию на кнопку получаем неведомым образом таблицу из сайта Почты России и выводим в нашем окне через веб-компонент (и правда, зачем парсить возвращаемую html-таблицу, перерисовывать на свой интерфейс в программе, если можно взять и отобразить этот html прямо в окне).

Конечно же, я опишу процесс “вытаскивания” значений из таблицы для дальнейшей обработки (email оповещение и т.д.).

Разбираем сайт

Вот ссылка, по которой можно получить состояние посылки: http://www.russianpost.ru/resp_engine.aspx?Path=rp/servise/ru/home/postuslug/trackingpo.

По её структуре сразу заметно, какого качества программисты писали им сайт.

FireBug и все-все-все

Как я уже не раз писал в своих статьях по парсингу, нам понадобится удобный плагин под FireFox — FireBug. С его помощью мы сможем отследить куда и какой запрос идет, чтобы получить нужный ответ. Другими словами, после ввода трекинг-номера в форму мы сможем увидеть, куда посылается POST-запрос и какие параметры посылаются вообще.

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

Тайна одиночного POST-запроса

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

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

HTML в Response-вкладке мы пока рассматривать не будем, вернемся к этому позже.

С++&Qt

Интерфейс

Создаем новый 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(). После нажатия автоматически сгенерируется слот, в котором нужно отписать код, реагирующий на нажатие кнопки.

POST запрос средствами Qt

Есть неплохая статья (http://vasinnet.blogspot.com/2010/01/post-qt-qnetworkaccessmanager.html) В ней описывается отправка POST-запросов с помощью QNetworkAccessManager.

Для осуществления запроса нам нужно получить данные: трекинг номер из поля ввода, текущую дату (частями). Первое нужно реализовать в слоте-обработчике нажатия кнопки (мы его создали в предыдущем пункте).

Текущая дата в Qt
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), а его уже в строку.

Генерация POST-запроса
// Получаем трекинг-номер из поля ввода
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 на наши переменные.

Отправка POST-запроса
// Вот здесь находится скрипт, обрабатывающий пост-запросы
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*);

В классе вашего окна, и именно к этому слоту подключать сигнал. (Кто не понял, к статье приложу архив проекта).

Обработка POST-ответа

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

Рассказ о базовой работе с DOM и QDocument занял бы много времени, поэтому я взамен буду только предлагать ссылки на документацию.

setConent из QDomDocument позволяет вручную установить html-содержимое. Теперь у нас есть некоторые функционал для манипулирования и поиском в DOM-дереве.

Кто хочет “крутого” кода, можно с помощью firebug'a найти таблицу в html'e и посмотреть её класс или прочие атрибуты, по которым можно искать, но чтобы не усложнять статью я методом тыка перебрал все таблицы и нашел нужные мне.

Функция getElementsByTagName возвращает нам все хендлы на переданный тег (в нашем случае — все таблицы). Перебором, как я уже говорил, нашел таблицу с информацией по отправке).

QDomNode в строку

Осталось только перевести каким-то методом полученные хендлы таблиц в 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);

Скачать проект.

В следующей статье я опишу, как автоматически отсылать запрос и по изменению состояния оповещать владельца имейлом об изменениях в статусе заказа.