Пакетная обработка на практике
При обработке большого количества HTML-документов пакетные API выполняются автоматически в параллель, в несколько раз быстрее, чем вызовы в цикле по одному. Это руководство охватывает четыре пакетных API, структуру результата и настройку параллелизма.
Обзор пакетных API
Библиотека предоставляет четыре пакетные функции, соответствующие комбинациям «ввод байтами / ввод файлами» × «без контекста / с контекстом»:
| API | Ввод | Контекст | Описание |
|---|---|---|---|
ExtractBatch | [][]byte | нет | пакетное извлечение среза байтов |
ExtractBatchFiles | []string | нет | пакетное извлечение по путям файлов |
ExtractBatchWithContext | [][]byte | да | поддержка тайм-аута/отмены |
ExtractBatchFilesWithContext | []string | да | поддержка тайм-аута/отмены |
Все функции можно вызывать как на экземпляре Processor, так и на уровне пакета:
// Уровень пакета (использует внутренний пул Processor)
br := html.ExtractBatch(pages)
// Экземпляр Processor (повторное использование кэша)
p, _ := html.New()
defer p.Close()
br := p.ExtractBatch(pages)Структура BatchResult
Пакетные операции возвращают *BatchResult, содержащий результаты по каждому элементу и сводные счётчики:
| Поле | Тип | Описание |
|---|---|---|
Results | []*Result | результат извлечения для каждого элемента; nil для неудачных или отменённых |
Errors | []error | ошибка для каждого элемента; nil для успешных; индекс соответствует вводу |
Success | int | количество успешно извлечённых |
Failed | int | количество неудачных извлечений |
Cancelled | int | количество пропущенных из-за отмены контекста |
Соответствие индексов
Results[i] и Errors[i] соответствуют i-му входному элементу. При успехе Results[i] не равно nil, а Errors[i] равно nil; при неудаче — наоборот.
Базовый пример
Пакетное извлечение 3 срезов байтов HTML:
package main
import (
"fmt"
"log"
"github.com/cybergodev/html"
)
func main() {
pages := [][]byte{
[]byte(`<html><body><article><h1>Первая страница</h1><p>Руководство по языку Go.</p></article></body></html>`),
[]byte(`<html><body><article><h1>Вторая страница</h1><p>Руководство по параллельному программированию.</p></article></body></html>`),
[]byte(`<html><body><article><h1>Третья страница</h1><p>Советы по оптимизации производительности.</p></article></body></html>`),
}
// Пакетное параллельное извлечение (функция уровня пакета)
br := html.ExtractBatch(pages)
fmt.Printf("Успешно: %d, неудачно: %d, отменено: %d\n", br.Success, br.Failed, br.Cancelled)
// Успешно: 3, неудачно: 0, отменено: 0
// Перебор результатов (индекс соответствует вводу)
for i, result := range br.Results {
if result != nil {
fmt.Printf(" [%d] Заголовок: %s\n", i+1, result.Title)
} else if br.Errors[i] != nil {
fmt.Printf(" [%d] Ошибка: %v\n", i+1, br.Errors[i])
}
}
// [1] Заголовок: Первая страница
// [2] Заголовок: Вторая страница
// [3] Заголовок: Третья страница
}Пакетное извлечение из файлов
files := []string{"page1.html", "page2.html", "page3.html"}
br := html.ExtractBatchFiles(files)
fmt.Printf("Успешно: %d, неудачно: %d\n", br.Success, br.Failed)
for i, err := range br.Errors {
if err != nil {
fmt.Printf("Сбой файла %s: %v\n", files[i], err)
}
}Функции пакета против экземпляра Processor
Поведение кэша у этих двух способов вызова различается:
| Способ вызова | Кэш | Применимый сценарий |
|---|---|---|
html.ExtractBatch(pages) | отключен (пул Processor очищает кэш при каждом возврате) | разовая пакетная задача |
p.ExtractBatch(pages) | включён (повторное использование кэша Processor) | высокочастотные пакеты, повторяющийся контент |
Функции пакета не кэшируют
Функции уровня пакета используют Processor из внутреннего sync.Pool, конфигурация которого отключает кэш (MaxCacheEntries = 0) и очищает кэш при каждом возврате. Если в пакете есть повторяющийся контент, используйте экземпляр Processor для ускорения через кэш. Подробнее см. Повторное использование Processor и кэш.
// Рекомендуется: повторное использование Processor для высокочастотных пакетов
p, _ := html.New()
defer p.Close()
for batch := range batchQueue {
br := p.ExtractBatch(batch) // кэш работает, повторяющийся контент попадает напрямую
processResult(br)
}Управление параллелизмом
WorkerPoolSize управляет количеством параллельных воркеров для пакетной обработки (по умолчанию 4, максимум 256):
cfg := html.DefaultConfig()
// Устанавливаем параллелизм по числу ядер CPU (с потолком 256)
if n := runtime.NumCPU(); n > 256 {
n = 256
}
cfg.WorkerPoolSize = n
p, _ := html.New(cfg)
defer p.Close()
br := p.ExtractBatch(pages)| Параметр | По умолчанию | Максимум | Описание |
|---|---|---|---|
WorkerPoolSize | 4 | 256 | количество параллельных воркеров, должно быть положительным целым |
Настройка WorkerPoolSize
Для CPU-интенсивных задач устанавливайте равным числу ядер CPU; для I/O-интенсивных (например, чтение файлов) можно увеличить. Значения выше 256 отклоняются при валидации конфигурации.
Предел размера пакета
Один пакет поддерживает максимум 10000 элементов. При превышении все элементы возвращают ошибку (а не частичная обработка):
huge := make([][]byte, 10001) // превышает предел
br := html.ExtractBatch(huge)
fmt.Printf("Неудачно: %d\n", br.Failed)
// Неудачно: 10001
fmt.Printf("Ошибка первого элемента: %v\n", br.Errors[0])
// Ошибка первого элемента: html: batch size 10001 exceeds maximum 10000Поведение при превышении
maxBatchSize = 10000 — жёсткий предел. При превышении ни один элемент не обрабатывается; для всех входных данных возвращается единая ошибка. Чтобы обработать больше, разбейте на несколько пакетов.
Отмена через контекст
ExtractBatchWithContext корректно завершает работу при отмене контекста:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
br := p.ExtractBatchWithContext(ctx, pages)
fmt.Printf("Успешно: %d, неудачно: %d, отменено: %d\n",
br.Success, br.Failed, br.Cancelled)| Статус элемента | Способ обработки |
|---|---|
| Завершён | результат сохраняется в Results |
| В процессе | после завершения записывается как обычно |
| Не начат | пропускается, учитывается в Cancelled, Errors[i] = ctx.Err() |
Доступность частичных результатов
После отмены контекста результаты завершённых элементов остаются в br.Results (не nil). Можно безопасно использовать уже готовые результаты, не отбрасывая весь вывод из-за отмены.
Обработка частичных неудач
Пакетная обработка допускает частичный успех — неудача одного элемента не влияет на остальные:
pages := [][]byte{
validHTML, // корректный
[]byte(""), // пустой ввод, вызывает ошибку
validHTML2, // корректный
}
br := p.ExtractBatch(pages)
// Элемент 2 неудачен, элементы 1 и 3 успешны
fmt.Printf("Успешно: %d, неудачно: %d\n", br.Success, br.Failed)
// Успешно: 2, неудачно: 1
// Постепенная обработка с пропуском неудачных элементов
for i, result := range br.Results {
if result == nil {
fmt.Printf("[%d] Неудача: %v\n", i, br.Errors[i])
continue
}
fmt.Printf("[%d] Заголовок: %s\n", i, result.Title)
}Рекомендации по производительности
| Число документов | Рекомендуемая стратегия | Описание |
|---|---|---|
| 1–10 | по одному через Extract | накладные расходы на пакет могут превысить выигрыш от параллелизма |
| 10–1000 | ExtractBatch + функция пакета | автоматический параллелизм, без управления Processor |
| 1000+ | p.ExtractBatch + экземпляр Processor | повторное использование кэша, обработка порциями во избежание пиков памяти |
| 10000+ | порциями (≤10000 на пакет) + экземпляр Processor | превышение предела одного пакета, обработка фрагментами |
// Пример крупномасштабной обработки порциями
p, _ := html.New()
defer p.Close()
const batchSize = 5000
for i := 0; i < len(allPages); i += batchSize {
end := i + batchSize
if end > len(allPages) {
end = len(allPages)
}
br := p.ExtractBatch(allPages[i:end])
// Обработка результатов текущей порции...
}Следующие шаги
- Повторное использование Processor и кэш - отличие кэша между функциями пакета и экземпляром
- Оптимизация производительности - повышение пропускной способности и тайм-ауты
- Обработка ошибок - сигнатурные ошибки и обработка пакетных ошибок
- Справочник API: пакетная обработка - полные сигнатуры API