Парсим HTML на C++ и Gumbo


    Gumbo — это парсер HTML5 на Си. Пока что Gumbo предоставляет только дерево, но никаких удобных функций для работы с ним. Поэтому написал парочку вспомогательных классов:
    • STL совместимый итератор обхода дерева в глубину;
    • компараторы для поиска по тегу, атрибуту;
    • пара фасадов.

    Под катом будет пример разбора страницы habrahabr.ru/all/
    Читаем из файла и разбираем HTML
    std::string readAll(const std::string &fileName);
    //...
    using namespace EasyGumbo;
    
    auto page = readAll(argv[1]);
    Gumbo parser(&page[0]);
    

    Конструктор класса Gumbo принимает указатель на буфер памяти, который завершается '\0'.

    std::find_if, компараторы и Element


    Выведем список статей и их url. Для этого найдем тег a(anchor) содержащий атрибут class со значением post_title.
        Gumbo::iterator iter = parser.begin();
        while (iter != parser.end()) {
            iter = std::find_if(iter, parser.end(),
                                And(Tag(GUMBO_TAG_A),
                                    HasAttribute("class", "post_title")));
            if (iter == parser.end()) {
                break;
            }
    
            Element titleA(*iter);
            auto text = titleA.children()[0];
            std::cout << "***\n";
            std::cout << std::setw(8) << "Title" << " : " << text->v.text.text << std::endl;
            std::cout << std::setw(8) << "Url" << " : " << titleA.attribute("href")->value << std::endl;
            ++iter;
        }

    В основе используется стандартный алгоритм std::find_if с компараторами Tag и HasAttribute. Шаблонная функция And создает экземпляр компаратора LogicalAnd. Element — это фасад над GumboNode.
    Интерфейс Element
    struct Element
    {
        typedef Vector<GumboNode*> ChildrenList;
    
        Element(GumboNode &element) noexcept :
            m_node(element)
        {
            assert(GUMBO_NODE_ELEMENT == m_node.type);
        }
    
        ChildrenList children() const noexcept
        {
            return ChildrenList(m_node.v.element.children);
        }
    
        const GumboSourcePosition& start() const noexcept
        {
            return m_node.v.element.start_pos;
        }
    
        const GumboSourcePosition& end() const noexcept
        {
            return m_node.v.element.end_pos;
        }
    
        const GumboAttribute* attribute(const char* name ) const noexcept
        {
            return gumbo_get_attribute(&m_node.v.element.attributes, name);
        }
    
        GumboNode &m_node;
    };


    В Gumbo текст хранится в узлах типа GUMBO_NODE_TEXT, поэтому обращаемся не к тегу A, а к его потомку titleA.children()[0].

    findAll и итератор


    Иногда неудобно ходить поэлементно, а хочется сразу получить список нужных узлов.
            iter = std::find_if(iter, parser.end(),
                                And(Tag(GUMBO_TAG_DIV),
                                    HasAttribute("class", "hubs")));
    
            std::cout << std::setw(8) << "Hubs" << " : ";
            auto hubs = findAll(iter.fromCurrent(), parser.end(), Tag(GUMBO_TAG_A));
            for (auto hub: hubs) {
                Element hubA(*hub);
                if (hub != hubs[0]) {
                    std::cout << ", ";
                }
                std::cout << hubA.children()[0]->v.text.text;
            }
            std::cout << std::endl;

    Тут находим узел с хабами, потом через findAll и создания нового итератора методом fromCurrent, вытаскиваем все теги A.
    Итератор спроектирован таким образом, что запоминает вершину дерева с которой начал обход. Если во время обхода натыкается на этот узел, то обход завершается. Такое поведение удобно, не нужно заботится о выходе из поддерева. Это дает возможность писать конструкции
        auto posts = findAll(parser.begin(),
                             parser.end(),
                             And(Tag(GUMBO_TAG_A),
                                 HasAttribute("class", "post shortcuts_item")));
        for(auto post : posts)
        {
            Gumbo::iterator iter(post);
            /*
             * Работаем внутри отдельного поста
             */
            ...
        }

    Это бывает удобно, но получается что по поддеревьям проходим дважды. Так же в открытый доступ вынесен метод gotoAdj, который позволяет перейти к соседнему элементу, тем самым пропускать поддерево. Остальной код прост.

    Весь разбор выглядит так:
    #include <fstream>
    #include <iomanip>
    #include <iostream>
    #include <algorithm>
    #include <gumbo.h>
    
    #include "Gumbo.hpp"
    
    using namespace std;
    
    std::string readAll(const std::string &fileName)
    {
        std::ifstream ifs;
        ifs.open(fileName);
        ifs.seekg(0, std::ios::end);
        size_t length = ifs.tellg();
        ifs.seekg(0, std::ios::beg);
        std::string buff(length, 0);
        ifs.read(&buff[0], length);
        ifs.close();
    
        return buff;
    }
    
    int main(int argc, char *argv[])
    {
        if (argc != 2) {
            return 0;
        }
    
        using namespace EasyGumbo;
    
        auto page = readAll(argv[1]);
        Gumbo parser(&page[0]);
    
        Gumbo::iterator iter = parser.begin();
        while (iter != parser.end()) {
            iter = std::find_if(iter, parser.end(),
                                And(Tag(GUMBO_TAG_A),
                                    HasAttribute("class", "post_title")));
            if (iter == parser.end()) {
                break;
            }
    
            Element titleA(*iter);
            auto text = titleA.children()[0];
            std::cout << "***\n";
            std::cout << std::setw(8) << "Title" << " : " << text->v.text.text << std::endl;
            std::cout << std::setw(8) << "Url" << " : " << titleA.attribute("href")->value << std::endl;
    
            iter = std::find_if(iter, parser.end(),
                                And(Tag(GUMBO_TAG_DIV),
                                    HasAttribute("class", "hubs")));
    
            std::cout << std::setw(8) << "Hubs" << " : ";
            auto hubs = findAll(iter.fromCurrent(), parser.end(), Tag(GUMBO_TAG_A));
            for (auto hub: hubs) {
                Element hubA(*hub);
                if (hub != hubs[0]) {
                    std::cout << ", ";
                }
                std::cout << hubA.children()[0]->v.text.text;
            }
            std::cout << std::endl;
    
            iter = std::find_if(iter, parser.end(),
                                And(Tag(GUMBO_TAG_DIV),
                                    HasAttribute("class", "views-count_post")));
            ++iter;
            std::cout << std::setw(8) << "Views" << " : " << iter->v.text.text << std::endl;
    
            iter = std::find_if(iter, parser.end(),
                                And(Tag(GUMBO_TAG_SPAN),
                                    HasAttribute("class", "favorite-wjt__counter js-favs_count")));
            ++iter;
            std::cout << std::setw(8) << "Stars" << " : " << iter->v.text.text << std::endl;
            iter = std::find_if(iter, parser.end(),
                                And(Tag(GUMBO_TAG_A),
                                    HasAttribute("class", "post-author__link")));
            Element authorA(*iter);
            std::cout << std::setw(8) << "Author" << " : " << authorA.children()[2]->v.text.text << std::endl;
    
            iter = std::find_if(iter, parser.end(),
                                And(Tag(GUMBO_TAG_DIV),
                                    HasAttribute("class", "post-comments")));
            auto comments = findAll(iter.fromCurrent(), parser.end(), Tag(GUMBO_TAG_A));
            if (comments.size() == 1) {
                Element commentsA(*comments[0]);
                std::cout << std::setw(8) << "Comments" << " : " << commentsA.children()[0]->v.text.text << std::endl;
            }
        }
    
        return 0;
    }

    Код как всегда доступен на GitHub'e
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.