Skip to content

Извлечение контента на практике

Это руководство поможет вам понять принципы работы и лучшие практики извлечения HTML-контента через реальные сценарии.

Обзор процесса извлечения

При вызове Extract библиотека выполняет следующие шаги:

text
Ввод HTML → валидация ввода → определение кодировки (автоконвертация в UTF-8) → разбор DOM → проверка глубины
    → безопасное санирование (опц.) → распознавание статьи (опц.) → извлечение содержимого → форматирование → возврат Result

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

Каждый шаг можно настроить через конфигурацию.

Базовое извлечение текста

Простейшее использование — извлечение контента из байтов HTML:

go
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 содержит следующие поля:

ПолеТипОписание
TitlestringЗаголовок страницы, приоритет: <title>, затем <h1>, <h2>
TextstringОсновной текст (очищенный, без тегов и лишних пробелов)
Images[]ImageInfoСписок извлечённых изображений
Links[]LinkInfoСписок извлечённых ссылок
Videos[]VideoInfoСписок извлечённых видео
Audios[]AudioInfoСписок извлечённых аудио
WordCountintКоличество слов в тексте
ReadingTimetime.DurationРасчётное время чтения (200 слов/мин)
ProcessingTimetime.DurationВремя обработки

Извлечение из файла

Для обработки локальных HTML-файлов используйте ExtractFromFile:

go
result, err := html.ExtractFromFile("article.html")
if err != nil {
    log.Fatal(err)
}
fmt.Println("Заголовок:", result.Title)

Файловые операции включают встроенные проверки безопасности:

  • Автоматическое обнаружение атак с обходом пути (например, ../../../etc/passwd)
  • Размер файла ограничен MaxInputSize
  • Сообщения об ошибках скрывают полный путь через SafePath()

Алгоритм распознавания статей

Когда ExtractArticle равен true (по умолчанию), библиотека автоматически определяет «основную область контента» на странице.

Принцип работы

  1. Оценка кандидатов: обход дерева DOM, вычисление оценки релевантности контента для каждого элемента
  2. Выбор лучшего кандидата: выбор узла с наивысшей оценкой в качестве контейнера статьи
  3. Механизм отката: если подходящий кандидат не найден, откат к узлу <body>

Сигнальные измерения скорера по умолчанию

Встроенный DefaultScorer вычисляет комплексную оценку на основе многомерных сигналов и выбирает контейнер с наибольшим количеством баллов:

ИзмерениеПоложительные сигналыНегативные сигналы
Семантика тегов<article>(+1000), <main>(+900), <section>(+300), <body>(+100)nav/aside/footer/header/script/style сразу возвращают 0
Шаблоны class/idcontent/article/post/main/entry/story (сильно положительные); blog/news/detail/page (умеренно положительные)comment/sidebar/nav/ad/menu (сильно негативные); widget/share/social/related (умеренно негативные); promo/banner/sponsor (слабо негативные)
Плотность абзацевБонус за количество <p> в поддереве × коэффициент (больше абзацев — выше вероятность основного текста)
Длина текстаБонус за длинный текст сверх порога; штраф за короткий текст ниже порога
Плотность контентаВысокое отношение текст/теги — усиливающий коэффициентНизкое отношение — понижающий коэффициент
Плотность ссылокШтраф при коротком тексте и высокой плотности ссылок (возможно, навигационная панель или карта сайта)
ПунктуацияВысокая плотность запятых (включая китайскую запятую ) указывает на прозу, бонус
ARIA rolerole="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:

go
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. Полный работоспособный пример см. в Тестирование и пользовательские расширения.

Извлечение только текста

Когда нужен только текст без изображений, ссылок и других метаданных:

go
text, err := html.ExtractText(data)
if err != nil {
    log.Fatal(err)
}
fmt.Println(text)

Это особенно полезно для анализа текста, построения поисковых индексов и других сценариев.

Рендеринг таблиц

HTML <table> рендерится в извлечённый текст в соответствии с конфигурацией TableFormat:

go
cfg := html.DefaultConfig()
cfg.TableFormat = "markdown" // по умолчанию; или "html"
ФорматРезультат рендерингаСценарий применения
"markdown"Markdown-таблица (со строкой-разделителем заголовка); colspan разворачивается в повторяющиеся ячейки; строки с определением ширины пропускаютсяЧтение человеком, Markdown-потребление
"html"Сохранение исходных HTML-тегов <table> (colspan/rowspan сохраняются как есть); строки структуры сохраняютсяПоследующая обработка, требующая точной структуры таблицы

Формат нечувствителен к регистру

Значение TableFormat нечувствительно к регистру ("Markdown" и "markdown" эквивалентны), пустое значение откатывается к "markdown".

Пример — извлечение HTML с таблицей:

go
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.

go
// Автоматическое определение кодировки
result, err := html.Extract(gbkEncodedData)

// Ручное указание кодировки
cfg := html.DefaultConfig()
cfg.Encoding = "gbk"
result, err = html.Extract(gbkEncodedData, cfg)

Контекст и тайм-аут

Для больших файлов или HTML из ненадёжных источников рекомендуется использовать версии с контекстом:

go
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("Тайм-аут обработки")
}

Следующие шаги