Парсер сайта на Qt, использование QRegExp

      Комментарии к записи Парсер сайта на Qt, использование QRegExp отключены

В этой теме 2 ответа, 1 участник, последнее обновление  Васильев Владимир Сергеевич 11 мес., 3 нед. назад.

  • Автор
    Сообщения
  • #3023

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

    Теперь мы возьмем немного более сложный пример — будем извлекать информацию о фотографиях с сайта goodfon.ru, а затем мы разместим картинки на виджете. В результате наше приложение будет выглядеть примерно следующим образом:
    Qt-parser-example

    Я опишу класс QRegExp, с помощью которого мы можем искать в тексте фрагменты, соответствующие регулярному выражению и немного расскажу про сами регулярные выражения — ту часть их синтаксиса, которая потребуется для реализации нашего парсера.

  • #3026

    Итак, чтобы написать парсер сайта, мы должны:

    1. зайти на этот сайт через браузер;
    2. определиться с тем, что мы хотим оттуда взять;
    3. получить html-код страницы (во всех известных мне браузерах при клике по странице правой кнопкой мыши вы сможете выбрать в меню соответствующий пункт);
    4. найти в html-коде нужную вам информацию;

    Проделав эти нехитрые манипуляции я получил следующий фрагмент:

    <div class="tabl_td" style="float:left;margin:3px;"> 
      <div style="padding:10px 5px;" itemscope itemtype="http://schema.org/ImageObject">    
        <a href="//www.goodfon.ru/wallpaper/anastasiya-panteleeva-devushka-1291.html" 
           title="Анастасия, пантелеева, девушка, красавица, шатенка" 
           itemprop="url">
                <img src="//img3.goodfon.ru/wallpaper/middle/5/ba/anastasiya-panteleeva-devushka-1291.jpg" 
                     width="290" height="181" alt="Обои анастасия, пантелеева, девушка" border="0" itemprop="thumbnail">
        </a> 
        <div class="left_f"> 
          <div class="ps"> 
            <a href="/catalog/girls/" class="big">Девушки</a>
          </div> 
        </div> 
        <div class="right_f"> 
           16 августа в 14:00 <br> 2560x1700 
     
        </div>  
      </div> 
    </div>   

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

    Теперь мне надо определить как эти фрагменты сможет выделить из кода страницы программ. Очевидно, я не могу искать просто ссылки, ведь они могут вести куда угодно. Я убедился, что описания всех иконок начинаются с тега <div class="tabl_td" и больше этот класс тега div нигде не встречается. В связи с этим, мой парсер будет искать на странице этот тег, получать его позицию (номер символа в исходном тексте страницы) и от нее начинать искать остальную нужную мне информацию.

    Опишем структуру нашей иконки, включив в нее всю нужную нам информацию:

    struct IconData {
      QString urlIconSrc;
      QString urlImagePage;
      QString datetime;
      QString size;
    };

    Опишем интерфейс класса-обработчика html-страницы:

    class IconsPageHandler : public QObject {
      Q_OBJECT
      public:
          explicit IconsPageHandler(QObject *parent = 0);
      public slots:
          void parse(QString url);
      private slots:
          void onPage_loaded(QNetworkReply*);
      signals:
          void finished(QList<site_parser::IconData>);
      private:
          QNetworkAccessManager *m_manager;
    };

    Класс допускает создание экземпляра, вызов функции parse (с указанием адреса страницы, с которой необходимо взять данные), информирование клиента о результатах обработки страницы при помощи сигнала finished.

    Конструктор класса создает экземпляр QNetworkAccessManager, выполняющего непосредственное получение данных с сайта и соединяет свой слот с его сигналом finished, содержащим ответ (html-код страницы):

    IconsPageHandler::IconsPageHandler(QObject *parent)
        : QObject(parent), m_manager(new QNetworkAccessManager(this)) {
    
        connect(m_manager, SIGNAL(finished(QNetworkReply*)), SLOT(onPage_loaded(QNetworkReply*)));
    }

    Функция parse принимает адрес страницы и формирует запрос (QNetworkRequest) к объекту QNetworkAccessManager:

    void IconsPageHandler::parse(QString url) {
        QNetworkRequest request(url);
        request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("text/plain"));
        m_manager->post(request, "");
    }

    После этого программа начинает получать данные с нужной вам страницы. Как только все данные будут получены, QNetworkAccessManager выработает сигнал и будет вызван связанный с ним слот:

    void IconsPageHandler::onPage_loaded(QNetworkReply *reply) {
        QString buff = reply->readAll();
    
        QRegExp iconsRegExp("<div class=\"tabl_td\".*<a href=\"//([^\"]+)\".*<img src=\"//([^\"]+)\".*([^<]+)<br>([^<]+)<");
        iconsRegExp.setMinimal(true);
    
        QList<IconData> icons;
    
        int lastPos = 0;
        while ((lastPos = iconsRegExp.indexIn(buff, lastPos)) != -1) {
            IconData iconData;
            lastPos += iconsRegExp.matchedLength();
    
            iconData.urlImagePage = iconsRegExp.cap(1);
            iconData.urlIconSrc = iconsRegExp.cap(2);
            iconData.datetime = iconsRegExp.cap(3);
            iconData.size = iconsRegExp.cap(4);
    
            qDebug() << iconData.urlIconSrc << iconData.urlImagePage
                     << iconData.datetime << iconData.size;
    
            icons.push_back(iconData);
        }
    
        emit finished(icons);
        reply->deleteLater();
    }

    Слот запускается с ответом сервера. Функцией readAll() все считанные данные могут помещены в строку (QString). Мы создаем список иконок, который затем функция распространит подписчикам при помощи сигнала (см. Мехнизм сигналов и слотов Qt и Паттерн MVC).

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

    1. <div class=\"tabl_td\", в начале описания иконки на странице находится такой фрагмент. Этот фрагмент содержит кавычки, поэтому мы экранируем с помощью слеша — иначе кавычка будет распознаваться как конец строки, а не как символ;
    2. .*<a href=\"//, первая ссылка в описании иконки — это ссылка на страницу с подробным описанием картинки. Между тегом div, описанным выше и этой ссылкой может находиться любое количество символов — в регулярных выражениях символ точки соответствует любому символу, а символ звездочки означает, что символов (стоящих перед звездочкой) может быть любое количество (ноль и более). Затем идет ссылка, заключенная в кавычки и два слеша;
    3. ([^\"]+)\", мы знаем что после ссылки стоит кавычка, поэтому нам надо считать все символы до нее. Фрагменты регулярного выражения, заключенные в круглые скобки — это то, что мы пытаемся извлечь — в данном случае нам нужна ссылка, поэтому в скобки помещен соответствующий ей фрагмент. Квадратные скобки в регулярном выражении задают возможные символы — например [123] будет соответствовать цифрам 1, 2 или 3. Однако, если первым символом в квадратных скобках является крышечка (^) — то выбираются любые символы, кроме перечисленных, например [^\"] будет соответствовать любому символу, кроме кавычки. Символ + работает также как звездочка, но требует, чтобы символов было больше нуля (хотя бы один символ). После всего этого, в строке идет еще одна кавычка.

    Разбор остальных параметров иконки производится аналогично, но адрес изображения начинается с <img src, а не с <a href как у адреса страницы.
    В этом примере, вызовом функции setMinimal(true) мы установили чтобы QRegExp выбирал минимальное совпадение шаблона со строкой, по умолчанию выбирается максимальное (самое длинное совпадение). Таким образом, если не изменить параметр setMinimal, то <div class=\"tabl_td\" будет взят от первой иконки, а <a href=\"//([^\"]+)\" — уже от последней. Однако, из-за того, что мы установили этот параметр, мы обязаны явно указывать где наш шаблон должен окончиться: ([^\"]+)\" если мы уберем \" с конца, то по шаблону [^\"]+ будет выбран только один символ (ведь мы требуем минимальное совпадение с шаблоном).

    Наш парсер должен перебрать все фрагменты, соответствующие иконкам и выделить из них нужную информацию, сделать это можно с помощью функции QRegExp::indexIn(QString, int), которая выбирает в заданной строке первый фрагмент, соответствующий шаблону, начиная с указанной позиции. Функция возвращает позицию, начиная с которой встречается вхождение шаблона или -1 в случае, если шаблон не встречается.

    Функция QRegExp::matchedLength() возвращает длину подстроки, совпавшую с шаблоном, это значение используется для изменения индекса элемента строки, с которого начнется поиск на следующей итерации цикла.

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

    Результат разбора html-страницы парсер помещает в описанную нами структуру и добавляет в список, а также для проверки выводит в qDebug().

  • #3034

    И так, наш парсер выполняет разбор страницы и результат (список иконок) передает подписчикам при помощи сигнала. Теперь нам надо создать подписчика, который будет помещать иконки в виде изображений на виджет. Для этого добавим в проект файл формы (.ui):
    add_ui_file_qt
    Выберем шаблон формы (в виде виджета):
    select_form_type
    Зададим имя класса формы:
    set_class_file_name_qt
    Откроем форму в редакторе Qt Creator и добавим на нее QListWidget, установим имя объекта формы, например listWidget:
    add-widget-on-form-qt

    Чтобы наш QListWidget занимал всю площадь формы, нужно добавить компоновщик, в редакторе форм Qt Creator для этого достаточно щелкнуть на форму правой кнопкой мыши и выбрать Компоновка->по сетке (на сомом деле, в нашем случае подойдет любой вид компоновщика).

    Итак, у нас в проект была добавлена форма (.ui-файл) и класс формы (размещенный в .cpp и .h файлах). В третьей части статьи Собственные виджеты в Qt Designer более подробно описано как можно использовать созданную нами форму.

    Картинки, которые мы хотим отображать на виджете находятся не на нем компьютере, а на удаленном сервере — поэтому для их отображения используем PixmapLoader (см. Получить изображение с сайта средствами Qt). Объект типа PixmapLoader добавим в класс формы, а также поместим туда слот-обработчик сигнала загрузки картинки и слот-обработчик сигнала finished от нашего парсера (метод setIcons):

    // gallery.h
    #ifndef GALLERY_H
    #define GALLERY_H
    
    #include <QWidget>
    #include "datastructures.h"
    #include "pixmaploader.h"
    
    namespace Ui {
        class Gallery;
    }
    
    class Gallery : public QWidget {
        Q_OBJECT
    public:
        explicit Gallery(QWidget *parent = 0);
        ~Gallery();
    private slots:
        void onPixmap_load(QPixmap);
    public slots:
        void setIcons(QList<site_parser::IconData>);
    private:
        Ui::Gallery *ui;
        PixmapLoader m_pixmapLoader;
    };
    
    #endif // GALLERY_H
    

    В конструкторе класса формы нужно установить для объекта listWidget свойство IconMode и размер иконок, это нужно чтобы отображать картинки на QListWidget. Кроме того, нам нужно соединиться со слотом PixmapLoader:

    Gallery::Gallery(QWidget *parent) : QWidget(parent), ui(new Ui::Gallery) {
        ui->setupUi(this);
        ui->listWidget->setViewMode(QListView::IconMode);
        ui->listWidget->setIconSize(QSize(186, 186));
        connect(&m_pixmapLoader, SIGNAL(loaded(QPixmap)),
                this, SLOT(onPixmap_load(QPixmap)));
    }

    В функции setIcons нужно инициализировать загрузку картинок с помощью PixmapLoader:

    void Gallery::setIcons(QList<site_parser::IconData> icons) {
        for(auto const &iconData : icons) {
            m_pixmapLoader.load("https://" + iconData.urlIconSrc);
        }
    }

    А в слоте-обработчике загрузки картинки — добавлять картинку на QListWidget:

    void Gallery::onPixmap_load(QPixmap pixmap) {
        QIcon icon(pixmap);
        QListWidgetItem *item = new QListWidgetItem(icon, "", ui->listWidget, 0);
    
        ui->listWidget->addItem(item);
    }

    Все, что нам остается сделать — создать в файле main.cpp объекты галереи и парсера и соединить их с помощью механизма сигналов и слотов:

    #include <QApplication>
    
    #include "iconspagehandler.h"
    #include "gallery.h"
    
    int main(int argc, char *argv[]) {
        QApplication a(argc, argv);
    
        Gallery w;
        IconsPageHandler iconsPageHandler;
    
        w.connect(&iconsPageHandler, SIGNAL(finished(QList<site_parser::IconData>)),
                  &w, SLOT(setIcons(QList<site_parser::IconData>)));
    
        w.show();
        iconsPageHandler.parse("https://www.goodfon.ru/catalog/girls/");
    
        return a.exec();
    }

    После запуска приложение будет выводить нам в окошке иконки изображений с сайта, по крайней мере до тех пор, пока на сайте не изменится верстка.

Для ответа в этой теме необходимо авторизоваться.