
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