Часто задаваемые вопросы
В чём разница между функциями пакета и Processor?
Функции пакета (например, html.Extract) внутри используют sync.Pool для повторного использования Processor и подходят для редких, однократных вызовов. После каждого вызова Processor возвращается в пул.
Processor (например, p := html.New()) подходит для высокочастотных вызовов, повторно используя кэш и внутренние ресурсы. Также поддерживает сбор статистики и журнал аудита.
// Редкие вызовы: функции пакета
result, _ := html.Extract(data)
// Высокочастотные вызовы: Processor
p, _ := html.New(html.DefaultConfig())
defer p.Close()
for _, page := range pages {
p.Extract(page)
}Как обрабатывать проблемы с кодировкой?
Библиотека HTML автоматически определяет 15+ кодировок (UTF-8, GBK, Shift_JIS, Windows-1252 и др.), обычно ручное указание не требуется.
Если необходимо принудительно указать кодировку:
cfg := html.DefaultConfig()
cfg.Encoding = "gbk"Каков лимит размера ввода?
По умолчанию максимум 50 МБ (DefaultMaxInputSize = 52428800). Можно изменить через конфигурацию:
cfg.MaxInputSize = 10 * 1024 * 1024 // 10 МБКак получить вывод в формате Markdown?
md, err := html.ExtractToMarkdown(data)Или с использованием Processor:
p, _ := html.New()
md, _ := p.ExtractToMarkdown(data)Сколько элементов можно обработать в пакетном режиме?
Максимум 10 000 элементов за один вызов. Для больших наборов данных обрабатывайте пакетами.
Почему извлечённый текст пуст?
Возможные причины:
- Проблема структуры HTML — содержимое находится внутри тегов
<script>или<style> - Пустое содержимое после санирования — если основной текст существует только в тегах, удаляемых при санировании (например
<iframe>,<object>), результат может быть пустым; для доверенного ввода можно временно установитьEnableSanitization = falseдля диагностики - Пустой ввод — проверьте, что входной массив байтов не пуст (пустое содержимое возвращает пустой
Result) - Распознавание статьи — попробуйте отключить
ExtractArticleи проверить, извлекается ли содержимое
Различайте ошибки и пустые результаты
Превышение глубины вложенности DOM относительно MaxDepth даёт не пустой текст, а ошибку ErrMaxDepthExceeded. Если вызов возвращает error, сначала определите тип ошибки через errors.Is, а не проверяйте, пуст ли текст.
cfg := html.DefaultConfig()
cfg.ExtractArticle = false // Отключить распознавание статьиКак отслеживать статистику обработки?
p, _ := html.New(html.DefaultConfig())
defer p.Close()
// После обработки некоторого контента
stats := p.GetStatistics()
fmt.Printf("Обработано: %d\n", stats.TotalProcessed)
fmt.Printf("Попаданий в кэш: %d\n", stats.CacheHits)
fmt.Printf("Среднее время: %v\n", stats.AverageProcessTime)
fmt.Printf("Ошибок: %d\n", stats.ErrorCount)Как включить аудит?
cfg := html.DefaultConfig()
cfg.Audit = html.DefaultAuditConfig()
cfg.Audit.Enabled = true
cfg.Audit.Sink = html.NewLoggerAuditSink()Подробнее в Система аудита.
Безопасны ли пути к файлам?
FileError автоматически усекает полный путь, предотвращая утечку серверных путей в сообщениях об ошибках:
var fileErr *html.FileError
if errors.As(err, &fileErr) {
fmt.Println(fileErr.SafePath()) // Только имя файла, без полного пути
}Как реализовать пользовательский скоринг контента?
Реализуйте интерфейс Scorer:
type MyScorer struct{}
func (s *MyScorer) Score(node html.ContentNode) int {
// Пользовательская логика скоринга
return 0
}
func (s *MyScorer) ShouldRemove(node html.ContentNode) bool {
// Пользовательская логика удаления
return false
}
cfg := html.DefaultConfig()
cfg.Scorer = &MyScorer{}Подробнее в Определение интерфейсов.
Должен ли пользовательский Scorer быть потокобезопасным?
Да. Когда один Processor используется несколькими concurrent-вызовами Extract, методы Score/ShouldRemove вызываются одновременно из нескольких goroutine. Если пользовательский Scorer хранит изменяемое состояние (кэш, счётчики), он должен самостоятельно использовать блокировки для синхронизации. Встроенный DefaultScorer доступен только для чтения и изначально потокобезопасен.
Предпочтительно отсутствие состояния
Рекомендуется проектировать пользовательский Scorer без состояния (вычислять только на основе переданного ContentNode), что позволяет избежать накладных расходов на блокировку и полностью исключает проблемы конкурентного доступа. При необходимости агрегирования статистики записывайте результаты в статистический канал Processor, а не в сам Scorer.
Как генерируется ключ кэша?
Ключ кэша основан на UTF-8 содержимом после преобразования кодировки и вычисляется с использованием алгоритма в стиле xxHash, создающего 128-битный (16 байт) хэш:
- До 64 КБ включительно (
maxCacheKeySize): вычисляется хэш полного содержимого - Более 64 КБ: используется 5-точечная выборка (голова, хвост, 3 равномерно распределённые точки), общий бюджет выборки 4096 байт (
cacheKeySample, около 819 байт на точку), дополнительно смешивается общая длина содержимого для повышения уникальности - Одинаковое UTF-8 содержимое (независимо от исходной кодировки: GBK, Shift_JIS или Windows-1252) генерирует один и тот же ключ
Преимущества нормализации кодировки
Поскольку ключ вычисляется после определения кодировки и преобразования в UTF-8, один и тот же документ, сохранённый в разных байтовых кодировках, попадает в одну и ту же запись кэша. Процент попаданий в кэш не зависит от кодировки ввода.
Почему результаты ExtractAllLinks и Extract различаются?
Они предназначены для разных целей, их пути обработки и возвращаемые типы различаются:
Extractсначала применяет санирование HTML (удаляет<script>,<iframe>и др.), затем извлекает ссылки из санированного DOM, результат вResult.Links, типLinkInfo(с полямиPosition,IsExternalи др.)ExtractAllLinksне применяет санирование, перечисляет все ссылки на ресурсы (включая<script src>,<iframe>,<link>,<embed>), возвращает[]LinkResource(с классификацией поType, например сценарий, стиль, медиа)
Кратко: Extract даёт «ссылки из основного текста», ExtractAllLinks даёт «все ресурсы, на которые ссылается страница».
Использует ли пул при передаче Config в функцию пакета?
Нет. Логика resolveConfig:
- Без Config → используется
DefaultConfig(), применяется пулsync.Pool - 1 Config → используется этот Config, создаётся временный Processor (без повторного использования пула)
Поэтому html.Extract(data, cfg) каждый раз создаёт и уничтожает новый Processor. При высокочастотных вызовах с пользовательской конфигурацией следует использовать html.New(cfg) и повторно использовать Processor для получения преимуществ кэша и статистики.
Как влияют внутренние panic?
Все операции извлечения обёрнуты в recoverPanic, panic не распространяется до вызывающего, а преобразуется в ошибку ErrInternalPanic. Гранулярность изоляции:
- Однократное извлечение: panic →
ErrInternalPanic - Пакетная обработка: panic отдельного элемента изолированно восстанавливается, влияет только на данный элемент (записывается в
Failed), не влияет на другие элементы - Подсистема аудита: panic в
Write/CloseAuditSinkизолируется (аудит работает по принципу best-effort, см. SEC-003), не прерывает основной процесс извлечения - Goroutine тайм-аута: внутренние panic также восстанавливаются независимо
Что делать при ErrInternalPanic
ErrInternalPanic означает, что ввод мог вызвать внутреннюю ошибку библиотеки. Следует записать исходные данные (или минимальный воспроизводимый пример) и сообщить об этом, а не просто повторять попытку — тот же ввод, скорее всего, снова вызовет panic.
Как отключить кэш для экономии памяти?
cfg := html.DefaultConfig()
cfg.MaxCacheEntries = 0 // Отключить кэш, пропустить генерацию ключа (нулевые накладные расходы)После отключения каждое извлечение выполняется полностью, но позволяет избежать накладных расходов памяти на записи кэша. Подходит для сценариев обработки большого количества различного контента (например, однократный скрапинг огромного числа разных страниц).
Пул Processor-ов по умолчанию уже отключает кэш
Пул Processor-ов, используемый функциями пакета (например html.Extract), настраивается с MaxCacheEntries = 0, CacheTTL = 0 — поскольку при каждом возврате в пул кэш очищается, включение кэша лишь добавляет накладные расходы на хэширование и операции с map. Для использования кэша явно вызывайте html.New(cfg).
В чём разница между ProcessingTimeout и тайм-аутом пользовательского context?
Внутренний тайм-аут библиотеки и context вызывающего работают совместно, тип ошибки зависит от источника и конфигурации:
| Сценарий | Тип ошибки | Источник |
|---|---|---|
Настроен ProcessingTimeout и истекает первым | ErrProcessingTimeout | Внутренний тайм-аут |
Пользовательский context истекает раньше ProcessingTimeout | ErrProcessingTimeout (нормализован) | Тайм-аут вызывающего |
ProcessingTimeout не настроен, пользовательский context истекает | context.DeadlineExceeded | Тайм-аут вызывающего |
Пользователь вызывает cancel() | context.Canceled | Ручная отмена |
Механизм: когда ProcessingTimeout > 0, библиотека создаёт производный deadline через context.WithTimeout(parentCtx, ProcessingTimeout), беря более ранний из двух; независимо от того, какой истёк, возвращается ErrProcessingTimeout. Только context.Canceled от ручного cancel() возвращается как есть. Если ProcessingTimeout не настроен, ошибки пользовательского context передаются напрямую.
Использует ли ExtractToMarkdown кэш?
Нет. ExtractToMarkdown внутри создаёт временный Processor через buildFormatProcessor, который явно отключает кэш (MaxCacheEntries = 0 + NewCache(0, 0)), не читая и не записывая кэш основного Processor-а.
Почему так спроектировано
Преобразование в Markdown — это лишь изменение формата вывода, результат самого извлечения не должен загрязнять основной кэш (иначе одно и то же содержимое кэшировалось бы в нескольких экземплярах для разных форматов). Временный Processor повторно использует Scorer основного Processor-а, переопределяя только InlineImageFormat/InlineLinkFormat, конфигурация изолируется копированием значения, что исключает конкурентное изменение общего состояния.
Почему тег <form> не удаляется при санировании?
Многие серверные фреймворки (ASP.NET WebForms, JSF, JSP) оборачивают весь <body> в один <form>. Удаление <form> приведёт к потере почти всего видимого содержимого. Текстовое извлечение не рендерит и не отправляет формы, поэтому причина удаления <form> для защиты от CSRF/UI-redress не применима к самому контейнеру. Однако элементы управления формы, такие как <input> и <button>, по-прежнему удаляются.
Какие ограничения применяются к data URL?
Санитизатор выполняет множественную проверку data: URL:
- Разрешены только MIME-типы из белого списка: изображения (gif/jpeg/png/webp/bmp/avif и др.), шрифты (woff/woff2/ttf/otf), PDF
- Блокируется
image/svg+xml(SVG может содержать встроенный JavaScript) - Блокируются пустые медиа-типы (например
data:;base64,...) - Ограничение размера
MaxDataURILength(100 КБ) - Проверка корректности символов base64-части
Заблокированные URL записываются через AuditRecorder с указанием причины (например malformed data URL, unsafe media type).
Что произойдёт при превышении 10000 элементов в пакетной обработке?
Весь пакет завершится неудачей (частичная обработка не выполняется). Лимит maxBatchSize — 10000, при превышении каждая запись в Errors заполняется строкой html: batch size N exceeds maximum 10000, Failed равен количеству входных данных, Results полностью nil.
// BatchResult при превышении: Failed == len(inputs), частичного успеха нет
br := html.ExtractBatch(hugeSlice) // len(hugeSlice) > 10000
fmt.Println(br.Failed) // == len(hugeSlice)Вызывающему необходимо самостоятельно разбивать данные на пакеты (например по 5000 элементов) для обработки больших наборов.
Что произойдёт при вызове после закрытия Processor?
Возвращается ErrProcessorClosed. Processor внутренне использует atomic.Bool для отметки состояния закрытия, все методы извлечения/форматирования проверяют его на входе. Ключевые моменты поведения:
Close()идемпотентен, многократный вызов безопасен- Пул Processor-ов после закрытия не возвращается в пул (чтобы следующий
Getне получил закрытый экземпляр с остановленным goroutine очистки кэша), а напрямую отбрасывается;sync.Poolсоздаст новый при следующемGet - При вызове пакетных методов на закрытом Processor каждая ошибка в
BatchResultбудетErrProcessorClosed
Каков алгоритм скоринга интеллектуального распознавания статей (ExtractArticle)?
Скорер по умолчанию (DefaultScorer) вычисляет оценку релевантности контента для каждого узла-элемента на основе многомерных сигналов и выбирает узел с наибольшим количеством баллов в качестве контейнера статьи. Измерения скоринга включают:
| Измерение | Положительные сигналы | Негативные сигналы |
|---|---|---|
| Семантика тегов | <article>(+1000), <main>(+900), <section>(+300) | nav/aside/footer/header сразу возвращают 0 |
| Шаблоны class/id | content/article/post/main/entry (сильно положительные); blog/news/detail (умеренно положительные) | comment/sidebar/nav/ad/menu (сильно негативные); widget/share/social (умеренно негативные) |
| Плотность абзацев | Бонус за количество <p> в поддереве × коэффициент | — |
| Длина текста | Бонус за длинный текст сверх порога; штраф за короткий текст ниже порога | — |
| Плотность ссылок | — | Штраф при коротком тексте и высокой плотности ссылок (вероятно, навигационная панель) |
| Пунктуация | Высокая плотность запятых (, или ,) указывает на прозу, бонус | — |
| Плотность контента | Высокое отношение текст/теги — усиливающий коэффициент | Низкое отношение — понижающий коэффициент |
| ARIA role | role="main"/role="article"(+500) | role="navigation"/role="complementary"(-400) |
Особая обработка layout-обёрток
Когда class/id одновременно содержит сигналы контента (content/article) и сигналы удаления (например sidebar) — типично для CSS layout-классов вроде content-sidebar — скорер не удаляет такой узел, поскольку он обёртывает основной контент. Семантические теги <article>/<main> всегда исключаются из эвристики удаления по class/id.
Если скорер по умолчанию не подходит для вашего целевого сайта, реализуйте собственный интерфейс Scorer для замены. Подробнее см. Тестирование и пользовательские расширения.
Как TableFormat влияет на вывод таблиц?
TableFormat управляет способом отображения HTML <table> в извлечённом тексте/Markdown:
| Значение формата | Эффект | Сценарий применения |
|---|---|---|
"markdown" (по умолчанию) | Рендеринг как Markdown-таблица (со строкой-разделителем заголовка), colspan разворачивается в повторяющиеся ячейки, строки с определением ширины пропускаются | Чтение человеком, Markdown-потребление |
"html" | Сохранение исходных HTML-тегов <table> (colspan/rowspan сохраняются как есть), строки структуры не пропускаются | Последующая обработка, требующая точной структуры таблицы |
cfg := html.DefaultConfig()
cfg.TableFormat = "html" // Сохранить HTML-таблицыСтрока формата нечувствительна к регистру ("Markdown" и "markdown" эквивалентны), пустое значение откатывается к "markdown".
Одинаково ли поведение AllowedBaseDir на разных платформах?
Да, базовая безопасность семантически единообразна на всех платформах, но механизм разрешения путей различается:
| Платформа | Способ разрешения | Покрываемые перенаправления |
|---|---|---|
| Linux | Чтение link из /proc/self/fd/<fd> | Символические ссылки (race-free) |
| macOS / BSD | Чтение link из /dev/fd/<fd> | Символические ссылки (race-free) |
| Другие Unix | Откат к filepath.EvalSymlinks | Символические ссылки (незначительный остаточный TOCTOU) |
| Windows | GetFinalPathNameByHandleW | Символические ссылки + junction + все reparse points |
Ключевая особенность дизайна: библиотека разрешает реальный путь из уже открытого дескриптора файла ОС (а не из строки пути), что закрывает окно TOCTOU-состояния гонки — проверка и чтение используют один и тот же файловый дескриптор, замена пути между ними не повлияет на результат. На Windows junction/reparse points создаются без каких-либо привилегий, и filepath.EvalSymlinks не может их разрешить, поэтому библиотека использует GetFinalPathNameByHandleW.
При попадании в кэш возвращается исходный объект?
Нет. При попадании в кэш возвращается глубокая копия через cloneResult — для срезов Images/Links/Videos/Audios выполняется copy. Это необходимо: записи в кэше могут одновременно читаться несколькими goroutine, прямое возвращение указателя привело бы к загрязнению кэша через псевдоним при изменении результата вызывающей стороной.
При промахе путь аналогичен: сначала запись в кэш, затем возврат копии, поэтому кэш-запись и возвращаемое значение не являются псевдонимами.
Почему один и тот же видео-URL одновременно появляется в Videos и Audios?
.ogg — это формат-контейнер, который может содержать видео (кодек Theora) или аудио (кодек Vorbis/Opus). При резервном регулярном сканировании URL с .ogg одновременно совпадает со списком видео- и аудио-расширений, поэтому он появляется отдельно в Result.Videos и Result.Audios. Только аудио-вариант .oga появляется только в списке аудио.
В чём разница между ProcessingTimeout=0 и отсутствием настройки?
Разницы нет. Нулевое значение Config нельзя использовать напрямую (нужно начинать с DefaultConfig()), а DefaultConfig() устанавливает ProcessingTimeout в 30 секунд. Установка вручную в 0 эквивалентна «без лимита» — Extract не запускает goroutine тайм-аута и не занимает квоту maxTimeoutGoroutines. Это позволяет избежать ненужных накладных расходов на goroutine при обработке заведомо корректных очень больших документов.
Можно ли использовать Extract и ExtractAllLinks совместно?
Да, они работают независимо:
Extractвозвращает*Result, гдеResult.Links— это ссылки<a>из санированного DOM (типLinkInfo, с полямиPosition/IsExternalи др.)ExtractAllLinksвозвращает[]LinkResource, перечисляя все ссылки на ресурсы в несанированном HTML (включая<script src>,<iframe>,<link>и др.), с классификациейType
Оба можно вызывать последовательно, не влияя друг на друга. Типичный сценарий: сначала Extract для извлечения основного контента, затем ExtractAllLinks для сбора всех ресурсов, на которые ссылается страница.