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

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

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

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

Ошибки и отладка chromedp

Ошибки chromedp узнаваемы, как отпечаток: один и тот же набор сообщений проходит через чужие issue на GitHub и через ваши ночные логи — и чем раньше вы выучите их в лицо, тем быстрее чините обходы. chromedp отладка тоже имеет свою технику: программа пишет в лог до и после каждого шага, при падении оставляет скриншот и дамп HTML, а подозрительные случаи разбираются в режиме с окном, где браузер видно глазами. Половина материала уже встречалась по пути: дедлайны из пятнадцатого урока, JS-рендер из одиннадцатого, сотни вкладок из девятнадцатого. Здесь всё собирается в одну дисциплину: типовые ошибки, их причины и лечения, обёртка с логированием шагов и таблица, которую стоит держать под рукой, когда парсер падает в два часа ночи.

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

Ошибка — не повод переписывать парсер, а повод прочитать её текст целиком. Половина материала этого урока уже встречалась по пути: дедлайны из пятнадцатого, рендер из одиннадцатого, сотни вкладок из девятнадцатого — но там они были техникой, здесь они становятся диагнозами: что именно сломалось, почему и что делать в первую минуту. Ценность дисциплины отладки измеряется ночью, когда обход молча встал: у кого-то в терминале одна строка «panic» и ноль версий, а у вас — лог шагов с временами, дамп HTML и скриншот того, что видел браузер в момент сбоя. Ниже — четыре семейства ошибок с причинами и лечениями, обёртка с логированием шагов, которую стоит вставить в парсер сегодня, и таблица для быстрых разборов.

Ошибки chromedp: четыре частых гостя#

Реальные сбои браузерных парсеров на Go сводятся к четырём семействам, и в логе они выглядят узнаваемо. Первое — context deadline exceeded: дедлайн истёк, страница не успела загрузиться или сайт медленный. Второе — «no node set» или пустой результат query: селектор не нашёл узел, чаще всего потому, что страница рендерится JavaScript'ом позже, чем код пытается читать. Третье — connection refused: Chrome не поднялся или порт отладки занят, программы вообще не с чем разговаривать. Четвёртое — tab crashed и context canceled без вашей вины: вкладка или весь браузер погибли, обычно от памяти при сотнях вкладок. Четыре семейства — не потому, что chromedp беден на ошибки, а потому, что архитектура проста: процесс Chrome, DevTools-протокол поверх вебсокета, контекст Go — и ломаться может только одно из трёх звеньев. Разбор всегда идёт сверху вниз: сначала текст ошибки, потом звено, потом причина — и только после этого код. Дальше — каждое семейство отдельно, с причиной и лечением, а в конце урока таблица для быстрых разборов.

ctx deadline exceeded: дедлайн истёк#

context deadline exceeded — самый частый гость, и почти всегда это не баг библиотеки, а арифметика из пятнадцатого урока: дедлайн шага меньше реального времени страницы. Медленный сайт, тяжёлая страница, сеть на другом краю страны — действие честно не уложилось в потолок. Классификация проста: перишаговый дедлайн истёк — ретрай с бэкоффом и свежим контекстом часто проходит; дедлайн сгорел на первом Run, который аллоцирует браузер, — погибли все последующие Run, и это уже авария конфигурации, а не сеть. Проверяйте причину через errors.Is(err, context.DeadlineExceeded), а не сравнением строк: обёртки и ретраи наворачивают слои, и только errors.Is видит сквозь них. Отдельный случай — context canceled при живом браузере и вашей невиновности: контекст отменила сама библиотека, потеряв соединение с браузером, — ретрай в мёртвый контекст бессмыслен, нужен новый NewContext от аллокатора. И помните про отсчёт: дедлайн стартует от вызова WithTimeout, а не от начала Run, — минуты между созданием контекста и Run съедаются молча. Отдельно тренируйте привычку записывать затраченное время каждого шага: потолок, выведенный из замеров, лечит таймауты навсегда, а угаданный — воспроизводит их еженедельно.

Дамп HTML и скриншот при падении шага

GO
if err := chromedp.Run(stepCtx,
        chromedp.Navigate(url),
        chromedp.WaitVisible(".list"),
); err != nil {
        // что видит браузер в момент сбоя
        var html string
        if derr := chromedp.Run(ctx,
                chromedp.Evaluate(
                        `document.documentElement.outerHTML`, &html),
        ); derr == nil && html != "" {
                _ = os.WriteFile("dump.html", []byte(html), 0o644)
        }
        var shot []byte
        if serr := chromedp.Run(ctx,
                chromedp.FullScreenshot(&shot, 90),
        ); serr == nil {
                _ = os.WriteFile("dump.png", shot, 0o644)
        }
        log.Printf("%s: %v (html=%d, png=%d)",
                url, err, len(html), len(shot))
}

После падения шага дедлайна браузер жив под родительским контекстом — этим пользуются: тем же Run выгружают outerHTML и полный скриншот страницы. Дамп сохраняется с внешним именем, чтобы потом открыть его глазами и увидеть, что же отдал сайт: капчу, заглушку, вёрстку без вашего класса. Errors двух Run независимы: сбой дампа не затирает главную причину.

«No node set» и пустой query: рендер позже кода#

Второе семейство ловит гонку «код быстрее страницы». Query-механика умеет ждать: по умолчанию стоит условие NodeReady — узлы должны существовать, и хелпер внутри крутит проверку с миллисекундными паузами до готовности или дедлайна. Поэтому Text по селектору, которого нет вообще, будет молча ждать до конца контекста — и вернёт таймаут, а не ошибку селектора. Но между «узлы появились» и «данные дописаны» проходит время, и при перерисовке SPA узел, который вы уже прочитали, заменяется новым: действие получает пустой или протухший набор узлов — в логах это всплывает как «no node set» или пустая строка без ошибки. Evaluate добавляет третью ловушку: он ничего не дожидается сам, и мгновенный Evaluate по ещё не отрисованному DOM вернёт null или пустой массив, хотя страница через полсекунды отрисуется. Лечение одно и то же: WaitVisible на устойчивый якорь контента вместо мгновенных чтений, poll-предикаты из девятого урока для изменчивых данных, повтор после перерисовки. Это ровно механика динамического контента из одиннадцатого урока — здесь она проявляется как ошибка, там — как техника. Диагностика простая: тот же селектор через пару секунд находит узел — значит, дело во времени, а не в разметке.

chromedp отладка: окно и скриншоты#

chromedp отладка начинается с простого шага: выключите невидимость. В DefaultExecAllocator из второго урока прописан headless-режим — флаг Headless в списке опций, — и выключается он добавлением своей опции Flag("headless", false) к срезу аллокатора: браузер запустится окном, и вы увидите всё своими глазами — где кликает программа, что рисует сайт, не вылезла ли капча. Это самый быстрый отладчик из существующих и его недооценивают: пятнадцать секунд наблюдения отвечают на вопросы, на которые лог не ответит и за вечер. Второй инструмент — FullScreenshot(res, quality): полное изображение страницы целиком, включая то, что за пределами вьюпорта, — «что видит браузер» в отличие от того, что видит curl: скрипты исполнены, стиль применён, картинка честная. Третий — дамп HTML после падения из примера выше, открываемый офлайн. У тройки есть общее правило: собирать их надо в момент сбоя, а не после — перезапуск уже изменит состояние сайта, капча переедет, и разбор станет гаданием. Окно с браузером на рабочем столе выглядит несерьёзно рядом с профайлером, но за пятнадцать секунд наблюдения оно отвечает на вопросы, на которые профайлер не ответит никогда.

Обёртка с логом шагов: до и после

GO
func logged(step string, act chromedp.Action) chromedp.Action {
        return chromedp.ActionFunc(func(ctx context.Context) error {
                start := time.Now()
                log.Printf("-> %s", step)
                if err := act.Do(ctx); err != nil {
                        log.Printf("xx %s: %v (%s)",
                                step, err, time.Since(start))
                        return err
                }
                log.Printf("ok %s (%s)", step, time.Since(start))
                return nil
        })
}

// chromedp.Run(ctx,
//         logged("навигация", chromedp.Navigate(url)),
//         logged("ожидание", chromedp.WaitVisible(".list")),
//         logged("сбор", chromedp.Evaluate(expr, &rows)),
// )

ActionFunc — адаптер, позволяющий обычной функции притворяться действием: обёртка получает шаг, пишет его имя до исполнения, ошибку с длительностью при провале и время при успехе. Act.Do(ctx) исполняет вложенное действие в том же контексте — дедлайны и отмена работают без изменений. Лог превращает падение в отчёт: видно последний успешный шаг и затраченные времена.

Ошибка, причина, лечение#

ОшибкаПричинаЛечение
context deadline exceededдедлайн меньше реального времени страницы; медленный сайтerrors.Is на тип, ретрай с бэкоффом, потолок шага по замерам
context canceled без виныchromedp потерял браузер и сам отменил контекстне ретраить: новый NewContext от аллокатора
no node set / пустоJS-рендер позже чтения; узел перерисовалсяWaitVisible на якорь, poll-предикат, повтор
connection refusedChrome не поднялся; порт отладки занят зомбиуборка процессов, окружение запуска, не сеть
tab crashedOOM браузера: сотни вкладок, тяжёлые страницылимит вкладок, pool, меньше вкладок на браузер

tab crashed и утечки вкладок: pprof на помощь#

Tab crashed — диагноз памяти: вкладка или браузер погибли, когда одной из сотен вкладок девятнадцатого урока не хватило памяти. Chrome падает по-разному — иногда гаснет вкладка, иногда весь процесс, — но для программы это выглядит одинаково: действия возвращают context canceled, а новый Run в том же контексте молчит. Симптом надёжнее причины: обход с пулом из девятнадцатого урока начал ронять задания через сорок минут — считайте это сигналом OOM, пока не доказано иное. Лечение — снижение потребления: меньше вкладок на браузер, меньше одновременно живых контекстов, обязательно defer cancel() на каждую вкладку — забытый cancel живёт до конца программы и держит вкладку. Для поиска утечек вкладок подходит стандартный арсенал Go: импорт net/http/pprof поднимает профиль — счётчик горутин, растущий от задания к заданию, почти всегда означает контексты без отмены, а heap-профиль показывает, что именно держит память. Снимайте профили на живом обходе, а не после падения: утечка видна в динамике, где две кривые расходятся. И держите лимит вкладок как константу рядом с пулом: растить параллелизм дешевле, чем лечить OOM на проде.

Чек-лист разбора падения#

  • прочитать текст ошибки целиком, классифицировать errors.Is, не глазами на строку
  • deadline exceeded — проверить, где стоит дедлайн и что в нём учитывается
  • context canceled при живой программе — умер браузер, восстанавливать контекст
  • пустой результат — ожидание на якорь до чтения, Evaluate не ждёт ничего
  • connection refused — процессы и порт, не сеть: ps и уборка зомби
  • tab crashed — память: лимит вкладок, cancel на каждую, pprof по горутинам
  • дамп HTML и скриншот — в момент сбоя, потом окно headless=false и глазами

Проверка: context canceled без вины

Ночной обход вернул context canceled, хотя программа ничего не отменяла. Обход в этом же контексте продолжать?

Проверка: Evaluate вернул пусто сразу

Evaluate по селектору таблицы вернул пустой массив мгновенно, а тот же Text на том же селекторе висит до дедлайна. Почему разные?

Что напишет лог

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

// обёртка logged из урока:
//   "-> шаг" до, "ok шаг (время)" после,
//   "xx шаг: err (время)" при ошибке
err := chromedp.Run(ctx,
        logged("навигация", chromedp.Navigate(url)),      // ok, 1.2s
        logged("ожидание", chromedp.WaitVisible(".zzz")), // дедлайн 3s
        logged("сбор", chromedp.Evaluate(expr, &rows)),
)

Соберите обёртку с логом

Восстановите адаптер ActionFunc и исполнение действия.

func logged(step string, act chromedp.Action) chromedp.Action { return chromedp.(func(ctx context.Context) error { start := time.Now() log.Printf("-> %s", step) if err := act.(ctx); err != nil { log.Printf("xx %s: %v (%s)", step, err, time.(start)) return err } return nil }) }

Расставьте разбор падения по шагам

Порядок действий, когда ночной обход упал: от чтения ошибки до воспроизведения глазами.

Строки

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

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

Карточки: ошибки и отладка

Термин

context deadline exceeded

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

Дедлайн истёк: страница медленнее потолка. Проверка errors.Is, ретрай с бэкоффом, потолок шага по замерам. Отсчёт — от WithTimeout, а не от начала Run.

1 / 6

Что дальше: после разбора сбоев остаётся два шага до эксплуатации. Двадцать четвёртый урок поставит вежливость и закон на службу — этику скрапинга: паузы, robots.txt и границы, за которыми парсер превращается в проблему. Двадцать третий уедет с вами на сервер — Docker и серверное окружение: там connection refused станет главной ошибкой первых дней. Домашняя проверка — локально: заведите обёртку logged в парсере каталога из предыдущего урока, затем воспроизведите три аварии подряд — убитый Chrome, занятый порт, селектор мимо рендера — и убедитесь, что по одному логу с дампами вы можете рассказать, что случилось, не глядя в код.

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

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

Вывод

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

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

  • Четыре семейства сбоев: deadline exceeded, no node set, connection refused, tab crashed — по звену на каждый: дедлайн, рендер, процесс, память.
  • errors.Is вместо сравнения строк: обёртки наворачивают слои, и только errors.Is видит первопричину.
  • context canceled без вашей вины — библиотека потеряла браузер: ретрай бессилен, нужен новый NewContext от аллокатора.
  • Text по несуществующему селектору ждёт до дедлайна, Evaluate ничего не ждёт и возвращает пусто мгновенно.
  • Лечение гонки рендера одно: WaitVisible на якорь перед чтением, poll-предикат для изменчивых данных.
  • Дамп HTML и FullScreenshot собираются в момент сбоя: перезапуск изменит состояние сайта.
  • headless=false — Flag("headless", false) поверх DefaultExecAllocator: браузер окном отвечает быстрее любого лога.
  • Обёртка на ActionFunc пишет шаг до и после: по логу виден последний успешный шаг и затраченные времена.
  • pprof на живом обходе: растущий счётчик горутин — это контексты и вкладки без cancel.
  • Разбор всегда сверху вниз: текст ошибки, звено, причина — и только потом код; замеры времени шага лечат таймауты навсегда.

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

Вооружите парсер каталога из предыдущего урока: обёртка logged на каждом шаге, дамп HTML и FullScreenshot при падении, паузы по-прежнему на месте. Затем устройте аварии по расписанию: убейте Chrome посреди обхода и убедитесь, что по логу видно мёртвый контекст; запустите второй экземпляр на том же порту и прочитайте connection refused; поставьте селектор мимо фактического рендера и посмотрите дамп — на нём должна быть видна страница без вашего класса. Снимите pprof-профиль горутин до и после обхода из тридцати вкладок и объясните разницу. Всё — на локальном Chrome, без внешних сайтов. Финал: вставьте в лог замер времени каждого шага и решите по замерам, какой из дедлайнов в вашем парсере стоит слишком щедро, а какой — впритык.

Личные заметкиПусто
Хранятся только в вашем браузере (Chromedp · Ошибки и отладка chromedp)

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

Как отличить таймаут своей программы от смерти браузера?

По типу ошибки и по вашей роли: errors.Is(err, context.DeadlineExceeded) — истёк дедлайн, браузер жив, ретрай уместен. errors.Is(err, context.Canceled) при том, что программа сама не вызывала cancel, — контекст отменила библиотека, потеряв браузер: контекст мёртв, восстанавливайте NewContext от аллокатора. Помогает и лог обёртки: таймаут наступает в конце ожидания с длительностью около потолка, смерть браузера — внезапно посреди шага.

Дамп HTML бесполезен: там одна заглушка без контента. Это плохой дамп?

Нет, это самый информативный дамп: заглушка вместо каталога означает, что сайт отдал антибот-страницу, капчу или просит JavaScript-навигацию — парсер попал не на контент, а на фильтр. Сравните дамп с той же страницей в обычном браузере: если глазами контент есть, а в дампе нет, вы читаете слишком рано или не ту вкладку; если заглушка и у людей — вопрос к источнику трафика и лимитам, а не к коду.

Нужен ли pprof постоянно или только при аварии?

Достаточно лёгкой формы: счётчик горутин и память в лог раз в несколько минут по runtime.NumGoroutine и runtime.ReadMemStats — это почти бесплатно и сразу видно утечку вкладок и контекстов. Полный pprof-сервер с heap-профилями включайте точечно на время разбора: он пишет из себя память и открывает порт, что на сервере с внешним доступом само по себе риск. Динамика важнее снимка: утечка — это расходящиеся кривые, а не высокое значение.

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

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

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

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

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

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

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