Извлечение контента на практике
Это руководство поможет вам понять принципы работы и лучшие практики извлечения HTML-контента через реальные сценарии.
Обзор процесса извлечения
При вызове Extract библиотека выполняет следующие шаги:
Ввод HTML → валидация ввода → определение кодировки (автоконвертация в UTF-8) → разбор DOM → проверка глубины
→ безопасное санирование (опц.) → распознавание статьи (опц.) → извлечение содержимого → форматирование → возврат ResultПроверка глубины выполняется до санирования: сначала итеративно проверяется глубина DOM (чтобы избежать переполнения стека из-за рекурсивного обхода), а затем выполняется безопасное санирование разобранного DOM-дерева. Обе операции работают с разобранным деревом узлов, поэтому разбор DOM всегда предшествует им обеим.
Каждый шаг можно настроить через конфигурацию.
Базовое извлечение текста
Простейшее использование — извлечение контента из байтов HTML:
package main
import (
"fmt"
"log"
"github.com/cybergodev/html"
)
func main() {
data := []byte(`<html>
<head><title>Руководство по Go</title></head>
<body>
<article>
<h1>Введение в Go</h1>
<p>Go — это статически типизированный компилируемый язык со встроенной поддержкой параллелизма.</p>
<p>Он отличается быстрой компиляцией, простотой развёртывания и подходит для создания высокопроизводительных сервисов.</p>
<img src="gopher.png" alt="Талисман Gopher" />
<a href="https://go.dev">Официальный сайт Go</a>
</article>
</body>
</html>`)
result, err := html.Extract(data)
if err != nil {
log.Fatal(err)
}
fmt.Println("Заголовок:", result.Title)
// Заголовок: Руководство по Go
fmt.Println("Текст:", result.Text)
// Текст: Введение в Go
// Go — это статически типизированный компилируемый язык со встроенной поддержкой параллелизма.
// Он отличается быстрой компиляцией, простотой развёртывания и подходит для создания высокопроизводительных сервисов.
// Официальный сайт Go
fmt.Println("Слов:", result.WordCount)
// Слов: 7
fmt.Println("Время чтения:", result.ReadingTime)
// Время чтения: 2.1с (при расчёте 200 слов/мин)
fmt.Println("Изображений:", len(result.Images))
// Изображений: 1
fmt.Println("Ссылок:", len(result.Links))
// Ссылок: 1
}Понимание результата извлечения
Result содержит следующие поля:
| Поле | Тип | Описание |
|---|---|---|
Title | string | Заголовок страницы, приоритет: <title>, затем <h1>, <h2> |
Text | string | Основной текст (очищенный, без тегов и лишних пробелов) |
Images | []ImageInfo | Список извлечённых изображений |
Links | []LinkInfo | Список извлечённых ссылок |
Videos | []VideoInfo | Список извлечённых видео |
Audios | []AudioInfo | Список извлечённых аудио |
WordCount | int | Количество слов в тексте |
ReadingTime | time.Duration | Расчётное время чтения (200 слов/мин) |
ProcessingTime | time.Duration | Время обработки |
Извлечение из файла
Для обработки локальных HTML-файлов используйте ExtractFromFile:
result, err := html.ExtractFromFile("article.html")
if err != nil {
log.Fatal(err)
}
fmt.Println("Заголовок:", result.Title)Файловые операции включают встроенные проверки безопасности:
- Автоматическое обнаружение атак с обходом пути (например,
../../../etc/passwd) - Размер файла ограничен
MaxInputSize - Сообщения об ошибках скрывают полный путь через
SafePath()
Алгоритм распознавания статей
Когда ExtractArticle равен true (по умолчанию), библиотека автоматически определяет «основную область контента» на странице.
Принцип работы
- Оценка кандидатов: обход дерева DOM, вычисление оценки релевантности контента для каждого элемента
- Выбор лучшего кандидата: выбор узла с наивысшей оценкой в качестве контейнера статьи
- Механизм отката: если подходящий кандидат не найден, откат к узлу
<body>
Сигнальные измерения скорера по умолчанию
Встроенный DefaultScorer вычисляет комплексную оценку на основе многомерных сигналов и выбирает контейнер с наибольшим количеством баллов:
| Измерение | Положительные сигналы | Негативные сигналы |
|---|---|---|
| Семантика тегов | <article>(+1000), <main>(+900), <section>(+300), <body>(+100) | nav/aside/footer/header/script/style сразу возвращают 0 |
| Шаблоны class/id | content/article/post/main/entry/story (сильно положительные); blog/news/detail/page (умеренно положительные) | comment/sidebar/nav/ad/menu (сильно негативные); widget/share/social/related (умеренно негативные); promo/banner/sponsor (слабо негативные) |
| Плотность абзацев | Бонус за количество <p> в поддереве × коэффициент (больше абзацев — выше вероятность основного текста) | — |
| Длина текста | Бонус за длинный текст сверх порога; штраф за короткий текст ниже порога | — |
| Плотность контента | Высокое отношение текст/теги — усиливающий коэффициент | Низкое отношение — понижающий коэффициент |
| Плотность ссылок | — | Штраф при коротком тексте и высокой плотности ссылок (возможно, навигационная панель или карта сайта) |
| Пунктуация | Высокая плотность запятых (включая китайскую запятую ,) указывает на прозу, бонус | — |
| ARIA role | role="main"/role="article"(+500) | role="navigation"/role="complementary"(-400) |
| Скрытые элементы | — | Узлы с style="display:none"/visibility:hidden или атрибутом hidden удаляются |
Исключение для layout-обёрток
Когда class/id одновременно содержит сигналы контента (content/article) и сигналы удаления (например sidebar) — типично для CSS layout-классов вроде content-sidebar — скорер не удаляет такой узел, поскольку он обёртывает основной контент. Семантические теги <article>/<main> (или role="main"/role="article") всегда исключаются из эвристики удаления по class/id, гарантируя, что <article class="post-with-sidebar"> не будет ошибочно удалён.
Распознавание статей не всесильно
Распознавание статей лучше всего подходит для новостных сайтов, блогов, документации и других страниц с чётко выраженной «областью основного контента». Для навигационных страниц, страниц-списков, галерей и других не-статейных страниц может быть затруднено точное определение основного текста — в этом случае можно установить ExtractArticle = false для извлечения всего содержимого <body>.
Подходящие сценарии
Распознавание статей лучше всего подходит для новостных сайтов, блогов, документации и других страниц с чётко выраженной «областью основного контента». Для навигационных страниц и страниц-списков может быть затруднено точное определение основного текста.
Пользовательский скоринг
Настройка логики скоринга через реализацию интерфейса Scorer:
type myScorer struct{}
func (s myScorer) Score(node html.ContentNode) int {
// Возвращаем оценку на основе характеристик узла
class := node.AttrValue("class")
if strings.Contains(class, "article") || strings.Contains(class, "post") {
return 100
}
if strings.Contains(class, "sidebar") || strings.Contains(class, "comment") {
return -50
}
return 0
}
func (s myScorer) ShouldRemove(node html.ContentNode) bool {
// Возвращаем true для удаления узла
return node.Data() == "nav" || node.Data() == "footer"
}Примечание
Функция strings.Contains в этом примере из стандартного пакета strings. Полный работоспособный пример см. в Тестирование и пользовательские расширения.
Извлечение только текста
Когда нужен только текст без изображений, ссылок и других метаданных:
text, err := html.ExtractText(data)
if err != nil {
log.Fatal(err)
}
fmt.Println(text)Это особенно полезно для анализа текста, построения поисковых индексов и других сценариев.
Рендеринг таблиц
HTML <table> рендерится в извлечённый текст в соответствии с конфигурацией TableFormat:
cfg := html.DefaultConfig()
cfg.TableFormat = "markdown" // по умолчанию; или "html"| Формат | Результат рендеринга | Сценарий применения |
|---|---|---|
"markdown" | Markdown-таблица (со строкой-разделителем заголовка); colspan разворачивается в повторяющиеся ячейки; строки с определением ширины пропускаются | Чтение человеком, Markdown-потребление |
"html" | Сохранение исходных HTML-тегов <table> (colspan/rowspan сохраняются как есть); строки структуры сохраняются | Последующая обработка, требующая точной структуры таблицы |
Формат нечувствителен к регистру
Значение TableFormat нечувствительно к регистру ("Markdown" и "markdown" эквивалентны), пустое значение откатывается к "markdown".
Пример — извлечение HTML с таблицей:
package main
import (
"fmt"
"log"
"github.com/cybergodev/html"
)
func main() {
data := []byte(`<html><body><article>
<h1>Прайс-лист</h1>
<table>
<tr><th>Продукт</th><th>Цена</th></tr>
<tr><td>Базовая версия</td><td>Бесплатно</td></tr>
<tr><td>Профессиональная</td><td>¥99/мес</td></tr>
</table>
</article></body></html>`)
result, err := html.Extract(data)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Text)
// Вывод (при TableFormat = "markdown"):
// Прайс-лист
//
// | Продукт | Цена |
// |------------------|----------|
// | Базовая версия | Бесплатно |
// | Профессиональная | ¥99/мес |
}Обработка не-UTF-8 кодировок
Библиотека автоматически определяет 15+ кодировок (включая UTF-8, GBK, Shift_JIS, Windows-1252 и др.) и автоматически конвертирует в UTF-8.
// Автоматическое определение кодировки
result, err := html.Extract(gbkEncodedData)
// Ручное указание кодировки
cfg := html.DefaultConfig()
cfg.Encoding = "gbk"
result, err = html.Extract(gbkEncodedData, cfg)Контекст и тайм-аут
Для больших файлов или HTML из ненадёжных источников рекомендуется использовать версии с контекстом:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := html.ExtractWithContext(ctx, data)
if errors.Is(err, html.ErrProcessingTimeout) {
log.Println("Тайм-аут обработки")
}Следующие шаги
- Форматы вывода на практике - Выбор подходящего формата для вашего сценария
- Повторное использование Processor и кэш - Оптимизация производительности при высокочастотных вызовах
- Справочник API: Функции пакета - Полные сигнатуры функций