Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 9 из 25 ~20 мин +50 XP

Ожидания: правильный сон

time.Sleep в парсере — лотерея: сайт отвечает то за двести миллисекунд, то за четыре секунды, и обе стороны ошибки одинаково дороги. Правильное chromedp ожидание строится на фактах: Poll опрашивает предикат внутри браузера, poll element go-условия дожидаются узлов, QueryAfter добавляет свой шаг после ожидания. Разберём инструменты по уровням — от условий Query до предикатов.

Содержание урока · 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. Каждый переспрос — отдельная оценка селектора в браузере, обмен по протоколу; для условий по узлам это дёшево, а вот для сложных состояний страницы нужен инструмент, который опрашивает, не выходя из браузера, — о нём дальше.

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 — про состояние внутри одной страницы.

Дождаться, что все картинки загрузились

GO
var 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, и она сразу выезжает наверх.

Дождаться двадцати карточек и забрать узлы

GO
err := 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-действий внутри RunGo: тот же механизм, настраиваемые порог и интервал
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),
// )

Соберите ожидание с аргументом

Восстановите вызовы: предикат-функция и опция, передающая ей порог количества.

var enough bool err := chromedp.Run(ctx, chromedp.( "(n) => document.querySelectorAll('.card').length >= n", &enough, chromedp.(20), ), )

Расставьте ожидание готовности

Соберите по порядку: контекст, буфер результата, Run с навигацией и предикатом, обработка ошибки.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: инструменты ожидания

Термин

chromedp.Poll

нажмите, чтобы перевернуть
Определение

Опрашивает выражение JavaScript внутри страницы до первого истинного значения; результат кладёт в res. Дефолтный тик — кадры анимации, таймаут — тридцать секунд.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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 · Ожидания: правильный сон)

Вопросы и ответы по уроку

Почему 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 — предикат пережил навигацию: перенесите ожидание после перехода или ждите готовность новой страницы штатными средствами.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Testcontainers25 уроков~8 чпрактика25 уроков уже доступны

Testcontainers в Go: интеграционные тесты с Docker

Курс testcontainers-go: Postgres, Redis, Kafka и Mongo в тестах через Docker, wait-стратегии, сети, параллельность, CI и итоговый стенд. 25 уроков.

  • Поднимать реальные зависимости в Docker из кода теста
  • Использовать модули Postgres, Redis, MongoDB, Kafka и RabbitMQ
  • Ждать готовность сервиса wait-стратегиями вместо sleep
  • Собирать стенды из нескольких контейнеров и своего образа