Skip to content

Пакетная обработка на практике

При обработке большого количества HTML-документов пакетные API выполняются автоматически в параллель, в несколько раз быстрее, чем вызовы в цикле по одному. Это руководство охватывает четыре пакетных API, структуру результата и настройку параллелизма.

Обзор пакетных API

Библиотека предоставляет четыре пакетные функции, соответствующие комбинациям «ввод байтами / ввод файлами» × «без контекста / с контекстом»:

APIВводКонтекстОписание
ExtractBatch[][]byteнетпакетное извлечение среза байтов
ExtractBatchFiles[]stringнетпакетное извлечение по путям файлов
ExtractBatchWithContext[][]byteдаподдержка тайм-аута/отмены
ExtractBatchFilesWithContext[]stringдаподдержка тайм-аута/отмены

Все функции можно вызывать как на экземпляре Processor, так и на уровне пакета:

go
// Уровень пакета (использует внутренний пул Processor)
br := html.ExtractBatch(pages)

// Экземпляр Processor (повторное использование кэша)
p, _ := html.New()
defer p.Close()
br := p.ExtractBatch(pages)

Структура BatchResult

Пакетные операции возвращают *BatchResult, содержащий результаты по каждому элементу и сводные счётчики:

ПолеТипОписание
Results[]*Resultрезультат извлечения для каждого элемента; nil для неудачных или отменённых
Errors[]errorошибка для каждого элемента; nil для успешных; индекс соответствует вводу
Successintколичество успешно извлечённых
Failedintколичество неудачных извлечений
Cancelledintколичество пропущенных из-за отмены контекста

Соответствие индексов

Results[i] и Errors[i] соответствуют i-му входному элементу. При успехе Results[i] не равно nil, а Errors[i] равно nil; при неудаче — наоборот.

Базовый пример

Пакетное извлечение 3 срезов байтов HTML:

go
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] Заголовок: Третья страница
}

Пакетное извлечение из файлов

go
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 и кэш.

go
// Рекомендуется: повторное использование Processor для высокочастотных пакетов
p, _ := html.New()
defer p.Close()

for batch := range batchQueue {
    br := p.ExtractBatch(batch) // кэш работает, повторяющийся контент попадает напрямую
    processResult(br)
}

Управление параллелизмом

WorkerPoolSize управляет количеством параллельных воркеров для пакетной обработки (по умолчанию 4, максимум 256):

go
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)
ПараметрПо умолчаниюМаксимумОписание
WorkerPoolSize4256количество параллельных воркеров, должно быть положительным целым

Настройка WorkerPoolSize

Для CPU-интенсивных задач устанавливайте равным числу ядер CPU; для I/O-интенсивных (например, чтение файлов) можно увеличить. Значения выше 256 отклоняются при валидации конфигурации.

Предел размера пакета

Один пакет поддерживает максимум 10000 элементов. При превышении все элементы возвращают ошибку (а не частичная обработка):

go
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 корректно завершает работу при отмене контекста:

go
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). Можно безопасно использовать уже готовые результаты, не отбрасывая весь вывод из-за отмены.

Обработка частичных неудач

Пакетная обработка допускает частичный успех — неудача одного элемента не влияет на остальные:

go
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–1000ExtractBatch + функция пакетаавтоматический параллелизм, без управления Processor
1000+p.ExtractBatch + экземпляр Processorповторное использование кэша, обработка порциями во избежание пиков памяти
10000+порциями (≤10000 на пакет) + экземпляр Processorпревышение предела одного пакета, обработка фрагментами
go
// Пример крупномасштабной обработки порциями
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])
    // Обработка результатов текущей порции...
}

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