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

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

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

Урок 3 из 25 ~18 мин +50 XP

Навигация: переходы и ожидание загрузки

Первая версия браузерного парсера почти всегда падает одинаково: переход есть, а элемента ещё нет — chromedp navigate вернул управление, JavaScript не успел дорисовать контент, и запрос упал по таймауту. Ожидание загрузки — главный навык в автоматизации браузера на Go: разберём, что именно дожидается Navigate, чем WaitReady отличается от WaitVisible и почему time.Sleep превращает парсер в лотерею.

Содержание урока · 10 разделов

Запустите парсер на странице с аналитикой и медленным соединением — и получите textbook-ошибку: Run вернул превышение дедлайна ожидания, хотя сайт в браузере открывается без проблем. Причина в том, что «страница загрузилась» — фраза с тремя разными смыслами. Смысл первый: браузер получил HTML и отработал событие load. Смысл второй: узлы, которые вам нужны, появились в DOM. Смысл третий: они отрисованы и видимы — до них можно дотянуться кликом или текстом. HTTP-парсеру достаточно первого смысла, потому что ответ сервера и есть страница. Браузерной программе мало: между load и готовностью контента живёт весь динамический слой — XHR-запросы, рендер, анимации. В этом уроке разберём, что chromedp делает на каждом уровне: что обещает Navigate, какие условия проверяют WaitReady и WaitVisible и почему сон фиксированной длины — это решение, которое работает на вашем ноутбуке и разваливается на CI.

chromedp navigate: что происходит при переходе#

chromedp.Navigate отправляет браузеру команду смены адреса и возвращает управление тогда, когда страница загрузилась — то есть дождалась события load: основная разметка приехала, отработали синхронные скрипты, браузер объявил загрузку завершённой. Это важная гарантия: после Navigate дерево документа уже содержит то, что сервер отдал, — заголовки, шапку, статический каркас. Но она же и граница: всё, что появляется после load, Navigate не дожидается. Данные, подтянутые XHR-запросами, элементы, собранные React или Vue, счётчики и виджеты — к моменту возврата из Navigate их ещё нет. Отсюда правило чтения страниц: Navigate отвечает за смену адреса, а за появление контента отвечают действия с условиями — запросы, которые переспрашивают документ, пока селектор не совпадёт. Эти два уровня часто путают в одну кучу и потом недоумевают, почему «Navigate уже вернулся, а заголовка нет». Разделение простое: переход — синхронный про load, контент — асинхронный про DOM. Полезно знать и механику: Navigate шлёт браузеру команду навигации, а затем ждёт события о завершении загрузки, поэтому «вернулся из Navigate» — факт, подтверждённый браузером, а не догадка по таймеру. Каждый переход в одном контексте заменяет текущую страницу: по сути это то, что делает человек в адресной строке, только из кода.

Переход и чтение: три уровня готовности в одном Run

GO
var title string
err := chromedp.Run(ctx,
        chromedp.Navigate("https://example.com/news/"),
        chromedp.WaitReady("body"),
        chromedp.WaitVisible("h1"),
        chromedp.Text("h1", &title),
)

Navigate дожидается события load — HTML на месте. WaitReady подтверждает, что body есть в DOM, WaitVisible — что заголовок отрисован и имеет реальные размеры, Text забирает его текст. Цепочка избыточна намеренно: она показывает, что переход и готовность контента — разные уровни, и каждый проверяется своим действием.

Два уровня готовности: WaitReady и WaitVisible#

WaitReady и WaitVisible выглядят как близнецы, но проверяют разное, и разница эта стоит часов отладки. WaitReady ждёт, пока узел по селектору появится в дереве документа: запрос переспрашивает DOM с интервалом в несколько миллисекунд, и как только элемент найден — действие исполняется. Это готовность структурная: элемент существует, с ним можно работать через DOM. WaitVisible добавляет второе условие: узел не просто есть, он виден — у него есть реальные размеры и прямоугольник на странице, то есть offsetWidth или offsetHeight отличны от нуля и CSS его не спрятал через display или visibility. Элемент может существовать в DOM и при этом быть невидимым: скрытый шаблон, вкладка, которую никто не открывал, спиннер с нулевыми габаритами. Для чтения текста хватает WaitReady, для клика нужен WaitVisible — клик идёт по координатам, а у невидимого узла координат нет. В хвосте цепочки стоит и третий нюанс: все Query-действия в chromedp уже содержат ожидание готовности по умолчанию, поэтому Text без предварительного WaitVisible всё равно дождётся узла — явное ожидание в примерах ставят ради читаемости и контроля условия. Сам поллинг дешёвый: один переспрос — это оценка крошечной функции в живой странице, а не сетевой запрос, поэтому интервал в несколько миллисекунд не грузит ни сеть, ни процессор.

sleep против событий: почему time.Sleep ломает парсер#

Самая человеческая идея при работе с динамической страницей — «подождать чуть-чуть и потом читать»: вставить Sleep на пару секунд и надеяться, что контент успеет. На одном ноутбуке это даже работает — и закрепляется как привычка. Дальше начинается то, что делает ожидание загрузки инженерной темой, а не кулинарной. Фиксированный сон не связан с фактом готовности: он либо короткий, и парсер падает на медленных страницах, либо длинный, и программа тратит секунды жизни на каждом из тысяч переходов. Хуже того — режим лотереи: на границе таймингов парсер работает через раз, и такие плавающие падения ловятся хуже всего. Правильная альтернатива — событийное ожидание: условия WaitReady, WaitVisible и компании проверяются повторно с малым интервалом, пока факт не случится, поэтому страница готова ровно тогда, когда это реально так, — ни раньше, ни позже. У chromedp есть и собственный chromedp.Sleep — действие, которое уважает отмену контекста, в отличие от голого time.Sleep, но назначение у него то же ограниченное: паузы между действиями, а не ожидание готовности. Для тонких сценариев — поллинг по своему условию — существует отдельный механизм, который разбирается в уроке про умные ожидания.

Инструменты ожидания chromedp#

ИнструментЧто дожидаетсяКогда брать
chromedp.Navigateсобытие load страницывсякая смена адреса — переходы и по клику, и по коду
chromedp.WaitReadyпоявление узла в DOMчтение текста и атрибутов скрытых и видимых узлов
chromedp.WaitVisibleузел в DOM плюс реальные размерыклики, ввод, скриншоты — всё, что требует видимости
chromedp.WaitEnabledузел без атрибута disabledкнопки, которые активируются после заполнения формы
chromedp.WaitNotPresentисчезновение узлов по селекторуспиннеры загрузки и оверлеи, мешающие следующим шагам

Несколько переходов в одном контексте#

Контекст chromedp — это сессия, а не одиночный переход: Navigate можно вызывать сколько угодно раз, страница заменяется, а вкладка и подключение живут дальше. На этом строится обход с пагинацией: переход на первую страницу, сбор, переход на вторую — тот же ctx, тот же браузер, история переходов копится штатно. Для движения по истории есть отдельные действия возврата и обновления страницы — Reload перезагружает текущий адрес, NavigateBack шагает назад, как кнопка в браузере, — они пригодятся, когда нужное состояние проще получить повторным открытием, чем репетицией кликов. Практический смысл держать один контекст экономический: браузер уже запущен и прогрет, повторный переход стоит в разы меньше, чем старт новой сессии. Единственное, за чем стоит следить, — состояние между переходами: куки и localStorage живут в сессии, поэтому переходы в одном контексте ведут себя как работа одного человека, а не отдельные визиты. Внутри одного Run действия исполняются строго по порядку, поэтому Navigate, затем ожидание, затем чтение образуют надёжный сценарий. Если же страниц десятки и сотни, последовательность упирается в скорость одного браузера — там начинается архитектура вкладок и пула, но это уже уровень, до которого нужно дойти с работающими ожиданиями.

Дедлайн контекста: таймаут на прогон#

Ожидание в chromedp опирается на контекст: пока условие не выполнено, запрос переспрашивает документ, а когда контекст отменён — всё останавливается с ошибкой дедлайна. Это делает context.WithTimeout честным предохранителем всей программы: не «подожду три секунды и хватит», а «у прогона есть бюджет времени, и любое висящее ожидание его израсходует». Дедлайн вешают на сессию целиком — переходы, ожидания, чтение; при превышении Run вернёт context deadline exceeded, и chromedp остановит браузер, потому что таймаут накрывает и его жизненный цикл. Отсюда два практических правила. Первое: бюджет закладывайте с запасом на старт браузера — истёкший в момент запуска дедлайн убивает весь процесс, а не только текущее действие. Второе: ошибку дедлайна стоит различать явно — она означает «страница или сайт не готовы», а не баг вашего кода, и обрабатывается ретраем или записью в проблемный список. Грамотная обработка таймаутов и повторов — тема отдельного урока про таймауты и устойчивость, здесь же важно само умение читать эту ошибку как сигнал о сети, а не как случайность.

Дедлайн и честная обработка ошибки

GO
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()

ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
defer cancel()

err := chromedp.Run(ctx,
        chromedp.Navigate(url),
        chromedp.WaitVisible(".price"),
)
if errors.Is(err, context.DeadlineExceeded) {
        log.Printf("не дождались цены за 30 секунд: %s", url)
        return
}

Таймаут накрывает весь прогон: если цена не отрисовалась за полминуты, Run вернёт context.DeadlineExceeded, а errors.Is отделит её от других ошибок — например, проблем с сетью. Лог с адресом страницы — материал для ретрая или проблемного списка, а не тихий сбой, который никто не заметил.

Правила ожидания одним списком#

  • Navigate дожидается только load — за контент после load отвечают условия на узлы
  • WaitReady — для существования узла в DOM, WaitVisible — для реальной видимости
  • клик и ввод требуют видимости: у невидимого узла нет координат для событий
  • time.Sleep не связан с готовностью страницы — только chromedp.Sleep как пауза между шагами
  • у каждого прогона есть дедлайн через context.WithTimeout — ошибки дедлайна различают явно
  • зеркальные ожидания тоже нужны: спиннер дожидается появления и исчезновения

Отдельного слова заслуживают SPA-страницы, где после load начинается самое интересное: фреймворк собирает вёрстку, данные едут отдельными запросами, и полсекунды разницы решают, увидит парсер карточки или пустой каркас. Здесь связка Navigate плюс условия на узлы показывает силу: программа ждёт не время, а факт — появление конкретного элемента, пусть даже через пять секунд после перехода. Как такие страницы устроены изнутри и какие ловушки в них живут — тема урока про SPA и динамический контент. Напоминание про песочницу: все примеры этого урока ходят по сети и требуют локального Go с установленным Chrome — запускайте их у себя в терминале; интерактивы сайта проверяют понимание уровней готовности, а не исполняют браузер.

Проверка: что обещает Navigate

chromedp.Navigate вернул управление. Что уже точно произошло?

Проверка: разница ожиданий

Чем WaitVisible отличается от WaitReady?

Медленная страница и три секунды сна

Прочитайте код и выберите, что он выведет:

// ctx, cancel := chromedp.NewContext(context.Background())
// ctx, cancel = context.WithTimeout(ctx, 10*time.Second)
// defer cancel()
// err := chromedp.Run(ctx,
//   chromedp.Navigate(url),
//   chromedp.Sleep(3*time.Second),
//   chromedp.Text(".price", &price),
// )
// Рендер цены занял 5 секунд. Что произойдёт?

Соберите цепочку готовности

Восстановите действия: переход, готовность дерева, видимость заголовка и чтение текста.

var title string err := chromedp.Run(ctx, chromedp.("https://example.com/news/"), chromedp.("body"), chromedp.("h1"), chromedp.Text("h1", &title), )

Расставьте каркас прогона

Соберите по порядку программу с дедлайном: от создания контекста до перехода и ожидания заголовка.

Строки

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

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

Карточки: ожидания загрузки

Термин

Что обещает Navigate

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

Дожидается события load: HTML получен и обработан. Контент после load — XHR и рендер — в обещание не входит.

1 / 6

Что дальше: переходы и ожидания — двигатель, а следующая ступень — прицел. Четвёртый урок про селекторы даст систему поиска элементов; здесь же отметим два направления. Если захочется довести ожидания до инженерной точности — поллинг по своему условию и редкие условия — держите в плане урок про умные ожидания. А когда парсер начнёт ходить по десяткам страниц, вернитесь к пятому уроку о кликах и вводе — действия пользователя и ожидания всегда идут парой.

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

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • «Страница загрузилась» имеет три уровня: событие load, узел в DOM, узел отрисован — chromedp даёт инструменты на каждый.
  • Navigate дожидается load: разметка от сервера на месте, но XHR-данные и рендер фреймворков — позже.
  • WaitReady — структурная готовность (узел в DOM), WaitVisible добавляет реальные размеры и не-скрытость.
  • Query-действия переспрашивают документ с малым интервалом, пока селектор не совпадёт или не истечёт дедлайн.
  • time.Sleep не связан с фактом готовности: на быстрых страницах — потерянные секунды, на границе таймингов — плавающие падения.
  • Дедлайн через context.WithTimeout накрывает прогон целиком; DeadlineExceeded различают через errors.Is и решают ретраем.
  • Контекст — сессия: Navigate можно звать многократно, а спиннеры дожидаются появления и исчезновения.

Домашнее задание

Возьмите любую страницу с динамическим блоком — например, ленту новостей, где карточки подтягиваются после открытия. Напишите программу с Navigate, дедлайном и WaitVisible на заголовок, затем намеренно воспроизведите обе ошибки урока: уберите ожидание и оставьте только Navigate перед чтением, а после — замените WaitVisible на Sleep. Зафиксируйте в терминале, как ведёт себя каждая версия, сформулируйте, почему событийное ожидание выиграло, и запишите, на каком уровне готовности ваша страница ломается без ожидания.

Личные заметкиПусто
Хранятся только в вашем браузере (Chromedp · Навигация: переходы и ожидание загрузки)

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

Нужно ли ставить WaitVisible перед каждым действием?

Строго говоря, нет: Query-действия и так дожидаются готовности узла по умолчанию. Явное WaitVisible ставят, когда важно именно видимое состояние — перед кликом, вводом или скриншотом, — и ради читаемости: по коду видно, какое условие программа считает фактом готовности. Если условий несколько, цепочку можно разбить на несколько Run — каждый вернёт свою ошибку, и по логу будет видно, какое именно условие не сбылось.

Почему нельзя просто поставить большой Sleep и не думать?

Потому что сон не проверяет факт: на медленной странице он окажется коротким и парсер упадёт, а на быстрых — съест лишние секунды на каждом из тысяч переходов. Плюс режим лотереи на границе таймингов: плавающие падения, которые ловятся хуже всего. Условия на узлы дешёвые и точные.

Navigate вернулся, а элемента нет — это баг chromedp?

Нет, это штатная ситуация динамического сайта: элемент появляется после события load — его подтянул XHR или собрал фреймворк. Решение — условное ожидание: WaitVisible или другое Query-действие, которое переспрашивает документ, пока узел не появится.

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

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

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

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

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

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

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