Содержание урока · 10 разделов
- colly async: как включается параллельный режим
- Что выигрываем: сеть — узкое место, а не процессор
- Забытый c.Wait(): классика пустой выдачи
- Общий слайс: гонка данных под Async
- Async без LimitRule: всплеск нагрузки
- Вторая ловушка: дубли, которых не видно
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Скрапер из прошлых уроков ходит по сайту как добросовестный посетитель: страница пришла, распарсилась, ушла следующая. На десятке страниц разницы с браузером не заметить, но возьмите каталог из пятисот — и арифметика расстраивает. Обычный запрос к удалённому сайту живёт сотню-другую миллисекунд, и почти всё это время процессор скрапера бездельничает: соединение установлено, пакеты едут по сети, сервер думает. Пока страница в пути, программа просто стоит. Последовательный режим превращает это стояние в сумму: пятьсот страниц по двести миллисекунд ожидания — полторы минуты чистого sleep, за который источник вам ничего не обещал. Узкое место здесь сеть, а не процессор и не парсер; отсюда и лечение — не дожидаться ответа, отправляя следующий запрос. Именно для этого в Go существуют горутины, и colly их использует, если попросить.
colly async: как включается параллельный режим#
Включается режим одной опцией: colly.NewCollector(colly.Async()) — или присвоением c.Async = true, если коллектор уже собран. Поведение при этом одинаковое: опция — сахар над тем же полем. Дальше меняется сам характер Visit. В синхронном режиме он возвращается только после того, как ответ пришёл и колбэки отработали; в параллельном Visit завершается сразу после постановки запроса в очередь, а вся работа — соединение, ожидание, разбор HTML, ваши OnHTML-колбэки — уезжает в горутины. Отсюда два следствия, которые придётся держать в голове. Первое: Visit можно звать подряд сколько угодно раз — очередь разбирается параллельно, темп задают правила LimitRule. Второе: main после Visit ничего не ждёт — программа добегает до конца и закрывается вместе с недоигравшими горутинами. Для этого у коллектора есть парный метод c.Wait(): он блокирует выполнение, пока все запущенные запросы не завершатся. В синхронном режиме он не нужен — там к моменту возврата Visit всё уже случилось.
Минимальный параллельный скрапер
GOpackage main
import (
"fmt"
"github.com/gocolly/colly/v2"
)
func main() {
c := colly.NewCollector(colly.Async())
c.OnHTML("span.text", func(e *colly.HTMLElement) {
fmt.Println(e.Text)
})
for i := 1; i <= 5; i++ {
pageURL := fmt.Sprintf("https://quotes.toscrape.com/page/%d/", i)
c.Visit(pageURL)
}
c.Wait()
}Пять Visit уходят почти одновременно: каждый возвращает управление сразу, и main доезжает до c.Wait(), пока запросы ещё в полёте. Wait разбудит программу, когда отработает последний колбэк. Уберите последнюю строку — main завершится мгновенно, чаще всего до первого ответа, и в терминале не напечатается ничего. Проверка занимает десять секунд: закомментируйте Wait и запустите.
Что выигрываем: сеть — узкое место, а не процессор#
Экономика параллельного скрапинга строится на бесплатном ожидании. Разбор HTML даже крупной страницы стоит микросекунды — узкое место всегда сеть: рукопожатие, TLS, время сервера на ответ. Параллельный режим перемножает эти ожидания: пока пятьдесят запросов висят в сети, процессор почти спит, зато страницы приходят волнами, а не по одной. Правда, выигрыш обычно меньше, чем обещает деление на количество горутин: под напором сайт начинает отвечать медленнее, часть соединений упирается в лимиты операционной системы. И вежливость: всплеск из пары сотен одновременных запросов — это уже не скрапинг, а репетиция DDoS, и админ источника имеет полное право вас забанить. Поэтому параллелизм ограничивают правилами LimitRule из урока про задержки и параллелизм: Delay задаёт паузу между запросами, Parallelism — сколько запросов к одному домену живут одновременно. Вежливый параллельный скрапер — это Async с ограниченным Parallelism, а не Async в чистом виде.
Забытый c.Wait(): классика пустой выдачи#
Живое место, на котором спотыкается почти каждый первый параллельный скрапер: программа выглядит работающей — ни ошибок, ни падений, — а вывод пустой. Причина примитивна: main завершился раньше, чем пришли ответы. В параллельном режиме Visit только ставит запрос в очередь; если после него нет c.Wait(), программа доезжает до конца main и закрывается вместе со всеми горутинами, включая ваши колбэки. Colly никуда не жалуется: с её точки зрения всё в порядке, просто вселенная кончилась. Диагноз ставится за минуту: тот же код с синхронным коллектором печатает данные, а с Async — тишина; значит, ищите пропавший Wait. Я держу это правило жёстким: в файле появился colly.Async() — в этом же файле обязан появиться c.Wait(). Это пара «открыл — закрыл», а не опция на выбор, и нарушение пары дороже, чем кажется: данные в таких прогонах не потеряны — они никогда не родились.
Скрапер ходит по сети, а браузерный интерпретатор сетевых вызовов не исполняет — runner-ов в этом курсе не будет. Понадобится локальный Go: go mod init demo, затем go get github.com/gocolly/colly/v2 — и все примеры урока запускаются в терминале против живых песочниц quotes.toscrape.com.
Общий слайс: гонка данных под Async#
Печать в консоль — вершина айсберга; настоящий скрапер копит результаты в общий слайс структур. И здесь параллельный режим ломается по-взрослому: OnHTML-колбэки выполняются в горутинах, горутины бегут одновременно, а append к одному слайсу из двух горутин — учебная гонка данных, подробно разобранная в уроке про горутины. Проявляется она зловеще-мягко: чаще всего ничего не происходит, но счётчик иногда врёт — то 98 цитат, то 100 при одном и том же наборе страниц. Пара элементов теряется, файл пишется урезанным, два месяца всё чисто, пока однажды отчёт не уходит к заказчику неполным. Компилятор это не ловит: типы сходятся, логика корректна. Ловит детектор гонок — запустите скрапер с go run -race, и на первом же конкурентном append вы получите развёрнутый отчёт: две горутины, адрес памяти, стеки. Лечение стандартное: всё, что мутирует общее, — под мьютекс.
Мьютекс вокруг append
GOpackage main
import (
"fmt"
"sync"
"github.com/gocolly/colly/v2"
)
type quote struct {
Text string
Author string
}
func main() {
mu := sync.Mutex{}
quotes := []quote{}
c := colly.NewCollector(colly.Async())
c.OnHTML("div.quote", func(e *colly.HTMLElement) {
q := quote{
Text: e.ChildText("span.text"),
Author: e.ChildText("small.author"),
}
mu.Lock()
quotes = append(quotes, q)
mu.Unlock()
})
for i := 1; i <= 10; i++ {
pageURL := fmt.Sprintf("https://quotes.toscrape.com/page/%d/", i)
c.Visit(pageURL)
}
c.Wait()
fmt.Println("собрано цитат:", len(quotes))
}Структура q собирается из локальных значений e.ChildText — конфликтовать здесь не с чем: у каждой горутины свой e и свой q. Опасная зона одна — append к общему слайсу. Вход под mu.Lock(), выход под mu.Unlock(), между ними единственная строка. Обратите внимание на порядок: мьютекс захватывается после сборки структуры — всё медленное остаётся вне критической секции, и колбэки почти не ждут друг друга. c.Wait() гарантирует, что len(quotes) считается после отработки всех колбэков, а не посреди.
Критическая секция из одной строки — правильная форма: чем она короче, тем меньше горутины простаивают в очереди. Распространённая ошибка — захватить мьютекс на весь колбэк вместе с вложенными Visit: формально безопасно, фактически параллельность схлопывается в последовательную, и вы платите сложностью за ноль выгоды. Вторая тонкость — defer mu.Unlock(): внутри короткого колбэка явная пара Lock/Unlock читается проще, а defer в горячем цикле — расходы по мелочи, которые на миллионе страниц складываются во что-то заметное. Проверить себя просто: удалите мьютекс и прогоните скрапер несколько раз с go run -race — детектор покажет обе горутины и адрес, за который они дерутся. Если вы проходили каналы из смежного урока, соблазн передавать результаты через chan будет велик: схема рабочая, но в colly-колбэках удобнее мьютекс — колбэк ничего не обязан знать о том, кто и когда прочтёт данные.
WaitGroup: по коллектору на горутину
GOurls := []string{
"https://quotes.toscrape.com/page/1/",
"https://quotes.toscrape.com/page/2/",
"https://quotes.toscrape.com/page/3/",
}
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
cc := colly.NewCollector()
cc.Visit(u)
}(url)
}
wg.Wait()Паттерн для случая, когда задачи независимы и каждой нужен свой набор колбэков или свои настройки: отдельный синхронный коллектор на каждый адрес, своя горутина, sync.WaitGroup как счётчик — Add до запуска, Done через defer, финальный Wait. Внутри такой горутины Async не нужен: Visit и так живёт в вашей горутине. Один общий коллектор с Async() и c.Wait() проще — берите его, если колбэки одинаковы для всех адресов.
Async без LimitRule: всплеск нагрузки#
Темп в параллельном режиме не задаётся сам собой. В синхронном запросы идут друг за другом, и пауза из LimitRule лежит между ними естественно; в параллельном без правил все Visit уходят практически одновременно — пятьсот страниц каталога влетают в источник одной волной. LimitRule навешивает политику на домен: Delay — пауза между запросами, RandomDelay — разброс поверх неё, Parallelism — сколько запросов живут одновременно. В паре с Async это и есть вежливый параллельный скрапер: пул из четырёх одновременных запросов с паузой в секунду собирает в разы быстрее последовательного обхода и не топит чужой сервер. Если прогон начал ловить 429 или падать по таймаутам — проблема обычно не в самой параллельности, а в том, что ошибки никто не ловит: как colly показывает ошибки сети и как строить повторы вручную — отдельный урок.
Проверка: пара Async и Wait
Коллектор создан с colly.Async(). Что произойдёт, если после c.Visit(...) сразу завершить main?
Проверка: гонка на слайсе
Почему append к общему слайсу из OnHTML-колбэка в Async-режиме опасен?
Что напечатает программа?
Прочитайте код и выберите, что он выведет:
c := colly.NewCollector(colly.Async())
c.OnHTML("span.text", func(e *colly.HTMLElement) {
fmt.Println(e.Text)
})
c.Visit("https://quotes.toscrape.com/")
// c.Wait() — забыли
fmt.Println("done")Восстановите каркас параллельного скрапера
Опция параллельного режима, вход и выход из критической секции, ожидание завершения.
Соберите параллельный скрапер по строкам
От создания коллектора до завершения ожидания.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Вторая ловушка: дубли, которых не видно#
Вторая ловушка тоньше первой. В параллельном режиме вывод перемешивается: строки от разных страниц чередуются, прогресс скачет, и «странность» легко списать на сам режим. Под перемешиванием же может лежать настоящий баг: селектор оказался шире, чем вы думали, и срабатывает на вложенные элементы; страница попала в очередь дважды; OnHTML навешан и на контейнер, и на каждого ребёнка. В синхронном режиме дубль виден глазами — вывод идёт по порядку, и повтор бросается в таблицу результатов. Мой отладочный приём: снять Async на время разбора, вернуть детерминированный порядок и посмотреть, сколько раз реально срабатывает OnHTML и что именно кладётся в слайс. Async сам по себе дубли не создаёт — но и маскирует их отлично: след повторного Visit тонет в перемешанном логе. Лог URL в OnRequest решает вопрос: два одинаковых адреса в списке скажут больше, чем час гаданий.
Карточки: параллельный скрапинг
Что дальше по курсу#
Темп задан, параллельность под контролем — дальше скраперу нужно направление: как ходить не по пяти пронумерованным страницам, а по целому каталогу с пагинацией, разбирает урок про пагинацию. Ошибки сети и повторные попытки — в уроке про OnError и таймауты. А если хочется понять, что именно происходит под c.Wait(), вернитесь к уроку про горутины и перечитайте с новым опытом: теперь он читается совсем иначе.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- colly.Async() переводит Visit в постановку в очередь: запросы и колбэки живут в горутинах.
- c.Wait() обязателен в параллельном режиме: без него main завершается раньше ответов, вывод пуст.
- Сеть — узкое место скрапера; параллельный режим перемножает ожидания, а не скорость процессора.
- Общий слайс под колбэками — гонка данных: мьютекс вокруг append и проверка go run -race.
- Async без LimitRule — всплеск одновременных запросов; Parallelism и Delay держат темп вежливым.
- Дубли и перемешанный вывод в Async отлаживаются снятием Async и логом URL в OnRequest.
- Несколько независимых задач — по коллектору на горутину и sync.WaitGroup вместо c.Wait().
Домашнее задание
Возьмите скрапер с мьютексом из урока, увеличьте выборку до всех десяти страниц quotes.toscrape.com и запустите трижды: сравните длину слайса и порядок вывода. Потом удалите мьютекс и прогоните тот же код с go run -race — сохраните отчёт детектора и верните мьютекс на место. Финальный шаг: закомментируйте c.Wait() и словами опишите, что именно изменилось в выводе, — это ощущение не заменяется чтением.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем c.Wait() отличается от sync.WaitGroup?
c.Wait() знает о внутренней очереди colly и дожидается всех запущенных запросов одного коллектора. WaitGroup считает ваши собственные горутины и нужен, когда вы сами раскидываете задачи — например, по коллектору на каждый URL. Схемы не исключают друг друга.
Сколько запросов colly шлёт одновременно в Async-режиме?
Без LimitRule — столько, сколько вызовете Visit: очередь не ограничивает параллелизм сама. Чтобы не устраивать всплеск, задайте LimitRule с Parallelism и Delay — это штатный способ держать темп.
Нужен ли WaitGroup, если у меня один коллектор?
Нет: одного c.Wait() достаточно. WaitGroup появляется, когда горутин много и каждая живёт своей жизнью — например, отдельный синхронный коллектор на группу адресов со своими колбэками.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Пагинация: обходим все страницы каталога
- Формы и вход: POST, cookies, сессии
- User-Agent и заголовки: когда сайт капризничает
- Прокси в Colly: ротация и пул адресов
- LimitRule: задержки и параллелизм
- OnRequest и OnResponse: запросы под контролем
- Навигация по сайту: обход ссылок и глубина
- Структуры для спарсенного: модели и слайсы
- Все курсы по Go
Следующая ступень учебного пути
Testify: тестирование Go с assert и require
Интерактивный курс Testify для Go: assert и require, табличные тесты, suite, mock-объекты, Eventually, HTTP-тесты. 25 уроков с примерами и практикой в браузере.
- Писать читаемые проверки через assert и require, а не лестницы if
- Проверять ошибки, паники и краевые случаи без ручного кода
- Сравнивать структуры, слайсы, карты и числа с плавающей точкой
- Строить табличные тесты и сьюты с хуками подготовки