Содержание урока · 10 разделов
Программа из прошлых уроков умеет ждать события загрузки и видимость узлов, но живые сценарии требуют третьего измерения: дождаться, когда на странице случится состояние — все картинки загрузились, карточек стало двадцать, счётчик в конце списка показывает готовность. Первый рефлекс — Sleep: подождать пару секунд и надеяться. Этот рефлекс стоит разобрать на части, потому что у него два режима отказа, и оба уже случались у каждого, кто писал скраперы. Медленный сайт: Sleep на две секунды, а рендер занял четыре, — программа читает пустоту и пишет мусор в отчёт. Быстрый сайт: Sleep на десять секунд «с запасом», а страница готова за полсекунды, — обход в двадцать раз длиннее нужного, и на тысяче страниц это уже часы. Ожидание по времени всегда промахивается либо в одну, либо в другую сторону, потому что время — не факт. Факт — это условие: узел появился, предикат вернул истину, счетчик дошёл до значения. Урок про chromedp ожидание элементов собирает инструменты ожидания в систему: условия Query-действий, предикаты Poll и крючок QueryAfter — плюс честный разговор о том, когда Sleep всё-таки допустим и чем он опасен.
Условия Query-действий: NodeVisible и родня#
Фундамент уже знаком по третьему и четвёртому урокам: каждое Query-действие само переспрашивает документ, пока условие не выполнится. Но условия — это отдельное семейство опций, и его стоит увидеть целиком. Дефолтное условие NodeReady: узлы найдены и получены из браузера. NodeVisible требует видимости — проверки offsetWidth, offsetHeight и клиентских прямоугольников; NodeNotVisible ждёт обратного. NodeEnabled дожидается, что у элемента нет атрибута disabled, NodeSelected — что элемент выбран. NodeNotPresent ждёт исчезновения: удобно после снятия оверлея загрузки. Обёртки над ними — знакомые WaitVisible, WaitNotVisible, WaitEnabled, WaitSelected, WaitNotPresent — это те же условия в виде отдельных действий. Два параметра настраивают сам механизм опроса: AtLeast задаёт, сколько узлов должно совпасть — по умолчанию один, для списка из двадцати карточек пишут AtLeast(20); RetryInterval — интервал переспросов, по умолчанию пять миллисекунд. Важно понимать, где живёт этот опрос: на стороне Go. Каждый переспрос — отдельная оценка селектора в браузере, обмен по протоколу; для условий по узлам это дёшево, а вот для сложных состояний страницы нужен инструмент, который опрашивает, не выходя из браузера, — о нём дальше.
Парсер ходил по каталогу год: Sleep(time.Second * 2) после каждого клика, и всё было хорошо, пока хостинг сайта не переехал. Рендер стал занимать три-четыре секунды, парсер по-прежнему читал через две — и неделю складывал в отчёт половину пустых карточек, потому что ошибки не было: узлы просто ещё не появились. Сломалось не время, а предположение, что у сайта постоянная скорость. Лечение — заменить Sleep на ожидание условия: WaitVisible на смысловом узле или Poll на предикат готовности. Спорное мнение с условием: Sleep вычёркивается из продакшен-кода совсем, но в отладке он остаётся полезен — дать анимации доиграть перед скриншотом, воспроизвести гонку руками. В исходниках библиотеки у Sleep честная пометка: временное действие для удобства, которое, скорее всего, будет удалено.
Poll: предикат внутри браузера#
Poll(expression, res, opts...) — ожидание произвольного условия JavaScript: из выражения собирается предикат, и он исполняется в самой странице, а не в Go-программе. Запросы вида poll element go описывают ровно этот механизм: предикат крутится в браузере, тикает по умолчанию в каждом кадре анимации — через requestAnimationFrame, — и ни один тик не стоит обмена по протоколу. Когда выражение впервые возвращает истинное значение, Poll завершается, а значение расфасовывается в res: логическое — в bool, текст — в строку, живой объект — в низкоуровневый RemoteObject; res может быть и nil, если значение не нужно. Это делает Poll универсальным: он умеет ждать не только узлы, но и данные — длину списка, значение переменной, флаг, который выставил код страницы. Таймаут по умолчанию тридцать секунд, ошибка при нём — ErrPollingTimeout. Один суровый предел: предикат не переживает навигацию — контекст исполнения страницы уничтожается, и Poll вернёт ошибку «Execution context was destroyed». Дожидаться конца загрузки новой страницы — работа Navigate и WaitReady, а Poll — про состояние внутри одной страницы.
Дождаться, что все картинки загрузились
GOvar ready bool
err := chromedp.Run(ctx,
chromedp.Navigate(url),
chromedp.Poll(
`Array.from(document.images).every(
i => i.complete && i.naturalWidth > 0)`,
&ready,
chromedp.WithPollingTimeout(15*time.Second),
),
)
if err != nil {
panic(err)
}
fmt.Println("картинки готовы:", ready)Предикат собирает все картинки страницы и требует, чтобы каждая закончила загрузку с ненулевой шириной — иначе битые изображения прошли бы проверку. Опрос идёт в браузере, в Go возвращается итоговое логическое значение. Таймаут задан явно: пятнадцать секунд вместо дефолтных тридцати, чтобы медленная страница не подвешивала обход.
PollFunction: аргументы и режимы опроса#
Когда условие сложнее одного выражения, его оформляют функцией: PollFunction(pageFunction, res, opts...) оборачивает её в вызов с аргументами — return (функция)(...args). Аргументы передаются опцией WithPollingArgs: предикат вида (n) => document.querySelectorAll('.card').length >= n вместе с WithPollingArgs(20) ждёт, пока карточек станет не меньше двадцати; значения едут как есть — числа, строки, структуры, закодированные в JSON. Вторая степень свободы — режим опроса. Дефолтный тик по кадрам анимации — самый частый и самый прожорливый по CPU страницы; он нужен, когда условие завязано на стили и размеры. WithPollingInterval(interval) переводит опрос на таймер: раз в двести пятьдесят миллисекунд — разумная точка для медленных состояний вроде загрузки данных. WithPollingMutation ждёт событий DOM: предикат проверяется один раз, а дальше — на каждое изменение дерева; это самый экономный режим, и у него есть поучительная история: ранние версии мутирующего опроса умели пропускать таймаут, если страница молчала, поэтому сниппет исполнения доработали страховкой, а тесты библиотеки проверяют случай «таймаут без мутаций». Привычка задавать WithPollingTimeout явно от этого только надёжнее. Опция WithPollingInFrame переносит предикат в указанный фрейм — узлы фрейма достают действием Nodes, и это единственный путь, когда условие живёт внутри iframe.
QueryAfter: свой шаг после ожидания#
Условия и предикаты закрывают вопрос «когда продолжать», но иногда в сам момент готовности нужно что-то сделать: посчитать узлы, вынуть из них данные, запустить свою функцию ровно тогда, когда страница доехала. Для этого у Query есть крючок After, а у библиотеки — обёртка QueryAfter(sel, f, opts...): селектор опрашивается, как обычно, условия выполняются, и затем вызывается ваша функция f с контекстом, идентификатором контекста исполнения и срезом найденных узлов. Сигнатура строгая: func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error — вернуть ошибку значит уронить весь Run, как от любого действия. Именно на этом крючке построены все Query-действия библиотеки: Text, Click и SetValue — это QueryAfter со своей функцией внутри, и теперь вы можете собрать в том же стиле собственное действие. Типовые применения: дожидаться AtLeast(20) карточек и одним махом собрать их узлы для дальнейшей работы; выполнить условие NodeVisible и сразу прокрутить узел; совместить своё ожидание с существующими опциями Query — ByQueryAll, AtLeast, RetryInterval. Это низкоуровневый инструмент, и брать его стоит, когда готовых действий уже не хватает, — но знание, что весь слой Query стоит на нём, делает библиотеку прозрачной. Крючок After многоразовый: функций можно повесить несколько, и исполнятся они по порядку добавления, а QueryAfter — лишь обёртка над одной из них. Ещё две детали из недр Query: порог AtLeast проверяется до условий видимости, поэтому с AtLeast(20) прогон честно крутится, пока карточек меньше двадцати; а битый селектор, скажем со случайным двоеточием, не уходит в вечный переспрос — браузер отвечает ошибкой DOM, и она сразу выезжает наверх.
Дождаться двадцати карточек и забрать узлы
GOerr := chromedp.Run(ctx,
chromedp.QueryAfter(".product-card",
func(ctx context.Context, execCtx runtime.ExecutionContextID,
cards ...*cdp.Node) error {
fmt.Println("карточек в DOM:", len(cards))
return nil
},
chromedp.ByQueryAll, chromedp.AtLeast(20),
chromedp.NodeVisible,
),
)QueryAfter дожидается, пока селектор совпадёт минимум с двадцатью видимыми карточками, и только затем вызывает функцию с узлами. Идентификатор execCtx нужен для низкоуровневых вызовов в контексте страницы, срез cards — сами узлы. Те же опции Query управляют поиском: ByQueryAll берёт все совпадения, AtLeast задаёт порог готовности.
Инструменты ожидания по уровню#
| Инструмент | Что ждёт | Где крутится опрос |
|---|---|---|
| WaitVisible и родня | узел и его состояние: видимость, включённость, выбор | Go: переспрос селектора, дефолт каждые 5 мс |
| NodeVisible, AtLeast, RetryInterval | условия Query-действий внутри Run | Go: тот же механизм, настраиваемые порог и интервал |
| Poll | выражение JavaScript: состояние, данные, флаги | браузер: тик на каждый кадр анимации |
| PollFunction | функцию с аргументами и режимом interval или mutation | браузер: таймер, события DOM или кадры |
| QueryAfter | готовность узлов плюс ваш шаг с этими узлами | Go: условие Query, затем ваш код |
| chromedp.Sleep | ничего: фиксированная пауза | Go: чистый сон — только для отладки |
Сколько ждать: таймауты и дедлайны#
У каждого ожидания есть верхняя граница, и её выбор — отдельное решение. У Poll и PollFunction — WithPollingTimeout: по умолчанию тридцать секунд, ноль отключает таймаут совсем. У дедлайна контекста — context.WithTimeout, знакомый по третьему уроку: он отсекает весь прогон, включая навигацию и действия. Правило их совместной жизни: таймаут предиката должен быть заметно меньше дедлайна контекста, иначе внешний дедлайн ударит первым, и вы получите не внятную ошибку ErrPollingTimeout, а безликую context deadline exceeded, из которой не видно, какое из условий не доехало. Разумная связка для обхода: контексту — бюджет страницы, скажем двадцать секунд, предикату — пятнадцать; тогда при отказе ошибка укажет на конкретное ожидание. Второе правило — таймауты задаются явно даже там, где есть дефолт: тридцать секунд — значение для человека за консолью, а не для обхода в тысячу страниц, где три зависших предиката стоят минуту времени воркера. Ноль, отключающий таймаут, — инструмент осознанный: он уместен в интерактивной сессии, когда вы смотрите на страницу глазами и готовы прервать сами, и опасен в автоматике, где превращает зависание в вечное.
Как выбрать ожидание за пять секунд#
- ждёте узел — WaitVisible или условие NodeVisible прямо в действии
- ждёте количество — AtLeast(20) на Query или предикат с querySelectorAll
- ждёте состояние или данные — Poll с выражением и явным WithPollingTimeout
- условие требует параметров или сложной логики — PollFunction с WithPollingArgs
- страница меняется редко и опрос дорог — режим WithPollingMutation
- нужен свой шаг в момент готовности — QueryAfter с функцией над узлами
- хочется Sleep — сначала спросите себя, какой факт вы на самом деле ждёте
Что дальше: ожидания — фундамент под оставшимся курсом. Одиннадцатый урок применит их к самой сложной цели — SPA и динамическому контенту, где разметку собирает фреймворк и без предикатов не обойтись; пятнадцатый урок про таймауты и устойчивость соберёт дедлайны, ретраи и падения браузера в систему. Практика — локально, с Go и Chrome: песочница сайта показывает механику без браузера, а настоящие гонки предикатов вы увидите только на живой странице. Кстати, половина багов ожидания воспроизводится в консоли браузера: перед тем как переносить предикат в Go, исполните его выражение в DevTools — совпало там, совпадёт и здесь.
Проверка: где живёт предикат
Где исполняется предикат, переданный в chromedp.Poll?
Проверка: таймаут опроса
Какой таймаут у Poll по умолчанию и как его отключить?
Что вернёт Poll после навигации
Прочитайте код и выберите, что он выведет:
// err := chromedp.Run(ctx,
// chromedp.Navigate(url),
// chromedp.Click("#next"), // кнопка уводит страницу переходом
// chromedp.Poll("window.__done === true", &ok),
// )Соберите ожидание с аргументом
Восстановите вызовы: предикат-функция и опция, передающая ей порог количества.
Расставьте ожидание готовности
Соберите по порядку: контекст, буфер результата, Run с навигацией и предикатом, обработка ошибки.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: инструменты ожидания
Предикат Poll — обычное выражение JavaScript, и его можно исполнить в консоли DevTools до переноса в Go: если выражение в консоли возвращает false там, где страница, по-вашему, готова, — вопрос к условию, а не к коду. Эта одна проверка экономит циклы «запустил — увидел ошибку — поправил строку», особенно для выражений с querySelectorAll и приведением типов.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Ожидание по времени всегда промахивается: ждать нужно факт — узел, количество, предикат, значение.
- Условия Query-действий: NodeReady, NodeVisible и родня, порог AtLeast и интервал RetryInterval; опрос крутится на стороне Go.
- Poll исполняет предикат в браузере: тик на каждый кадр анимации, ноль обменов по протоколу на тик, результат — в res.
- PollFunction добавляет аргументы через WithPollingArgs; режимы — таймер WithPollingInterval и события DOM WithPollingMutation.
- Таймаут предиката — WithPollingTimeout, дефолт тридцать секунд, ноль отключает; держите его меньше дедлайна контекста.
- QueryAfter — крючок после готовности узлов: ваша функция получает контекст исполнения и срез узлов; на нём стоит весь слой Query.
- Sleep — отладочный костыль с пометкой о будущем удалении: в продакшене его место занимает условие или предикат.
- Предикат не переживает навигацию: ожидание состояния — внутри одной страницы, переход — работа Navigate и WaitReady.
Домашнее задание
Возьмите страницу с ленивой подгруздкой и соберите три версии ожидания готовности списка: WaitVisible на контейнере, Poll с выражением document.querySelectorAll('.item').length >= 20 и PollFunction с предикатом-функцией и WithPollingArgs. Замерьте время каждого подхода, затем намеренно сломайте условие — задайте порог, которого страница не достигнет, — и посмотрите на ErrPollingTimeout. Финал: замените один Sleep в своём старом коде на предикат и сравните стабильность десяти прогонов.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему chromedp.Poll, а не WaitVisible — у меня и так узлы ждут?
WaitVisible ждёт узел и его видимость; Poll ждёт любое состояние страницы: длину списка, значение переменной, завершение загрузки картинок, флаг готовности, который выставляет код сайта. Если условие сводится к «элемент есть и виден», достаточно WaitVisible. Если условие про данные и числа — это территория Poll, потому что Query-условия про узлы, а не про состояние.
Есть ли в chromedp отдельное действие вроде Visibility?
Нет: видимость в библиотеке — это условие, а не действие. NodeVisible вешается на любое Query-действие и дожидается, что узел имеет ненулевые размеры и клиентские прямоугольники; WaitVisible — обёртка того же условия в виде отдельного шага; NodeNotVisible ждёт обратного. Проверка живёт в маленьком сниппете JavaScript и исполняется браузером — отдельное имя для неё не понадобилось.
Poll завис на медленной странице — что читать в ошибке?
Три типовых исхода. ErrPollingTimeout — условие честно не выполнилось за WithPollingTimeout: проверьте предикат в консоли. context deadline exceeded — раньше ударил дедлайн контекста: разнесите таймауты, предикату поменьше, контексту побольше. Execution context was destroyed — предикат пережил навигацию: перенесите ожидание после перехода или ждите готовность новой страницы штатными средствами.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Testcontainers в Go: интеграционные тесты с Docker
Курс testcontainers-go: Postgres, Redis, Kafka и Mongo в тестах через Docker, wait-стратегии, сети, параллельность, CI и итоговый стенд. 25 уроков.
- Поднимать реальные зависимости в Docker из кода теста
- Использовать модули Postgres, Redis, MongoDB, Kafka и RabbitMQ
- Ждать готовность сервиса wait-стратегиями вместо sleep
- Собирать стенды из нескольких контейнеров и своего образа