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

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

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

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

Перехват сетевых запросов

Самое ценное на странице часто не в DOM: сайт рисует таблицу данными, которые сам получил XHR-запросом к скрытому API, и перехват запросов достаёт эти данные напрямую — с URL, статусом и телом ответа. В chromedp network-домен включается самим Run, события сети приходят в подписку через ListenTarget, а тело любого ответа достаётся по его RequestID. Разберём сбор адресов за загрузку страницы, фильтр по домену, кейс с JSON из XHR, блокировку трекеров через Fetch-домен и границы: часть механизмов зависит от версии протокола DevTools. К концу у вас будет слушатель, который собирает адреса API с живой страницы и достаёт тело ответа, — скелет, на который дальше ложатся блокировка и экономия.

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

Парсер на селекторах играет в догонялки: сайт поменял разметку — селекторы в мусор. Но данные, которые вы парсите, где-то берутся: страница делает запрос к своему API, получает JSON и красит им DOM. Запрос виден в панели Network инструментов разработчика, и тот же поток событий протокол DevTools отдаёт программе. Это меняет расстановку сил: вместо поиска карточек в разметке вы читаете эндпоинт, который страница вызывает сама, — стабильный контракт, который меняется куда реже, чем вёрстка. События сети в chromedp — не отдельный режим, а обычные события браузера: подписка, switch по типу события и доступ к полям запроса и ответа. Один нюанс стоит знать заранее: сеть — асинхронный поток, и код с ней работает как с потоком, а не как с последовательностью действий. Зато выгода симметричная: один слушатель обслуживает всю страницу, и данные приезжают сами, без повторных запросов от вас.

chromedp network: перехват запросов через ListenTarget#

Сетевые события — события вкладки, поэтому их ловит chromedp.ListenTarget: подписка принимает контекст и функцию, которой протокол отдаёт каждое событие. Внутри — классический type switch: network.EventRequestWillBeSent приходит в момент отправки и несёт RequestID, адрес, метод и тип ресурса ev.Type, где ResourceTypeXHR отличает AJAX от документов и скриптов; network.EventResponseReceived приходит с ответом и несёт статус, MIME-тип и заголовки в ev.Response. Поле ev.Type бывает Document, Script, Image, Stylesheet, Fetch — по нему отделяют API-запросы от остального потока. Включать домен руками не нужно: Run сам вызывает network.Enable при старте, поток событий идёт сразу. Колбэк выполняется синхронно в потоке обработки событий, поэтому блокировать в нём нельзя: действие запускается отдельной goroutine, иначе получите дедлок — об этом отдельный разговор ниже. Не путайте парную ListenBrowser: она ловит только браузерные события вроде появления новых целей, и target-события, включая сеть, к ней не приходят. Подписка отменяется вместе с контекстом: в цикле обхода, где контекст пересоздаётся на страницу, слушатель живёт ровно одну страницу.

Собрать все XHR за загрузку страницы

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

var xhrURLs []string
chromedp.ListenTarget(ctx, func(ev any) {
        switch ev := ev.(type) {
        case *network.EventRequestWillBeSent:
                if ev.Type == network.ResourceTypeXHR &&
                        strings.Contains(ev.Request.URL, "api.example.com") {
                        xhrURLs = append(xhrURLs, ev.Request.URL)
                }
        case *network.EventResponseReceived:
                if ev.Response.Status != 200 {
                        log.Printf("не 200: %d %s",
                                ev.Response.Status, ev.Response.URL)
                }
        }
})

err := chromedp.Run(ctx,
        chromedp.Navigate("https://example.com/reports"),
        chromedp.WaitVisible(".report-table"),
)
log.Println("перехвачено XHR:", len(xhrURLs))

Подписка ставится до Run: события навигации начнут приходить с первого байта, и ничего не потеряется. Ветвь EventRequestWillBeSent фильтрует запросы по типу XHR и домену, ветка EventResponseReceived следит за статусами. К моменту возврата Run все события загрузки обработаны, и срез готов к использованию — подписка отменится вместе с контекстом.

Таймлайн событий: запрос, ответ, финиш#

Поток событий сети упорядочен, и порядок стоит выучить заранее. На каждый запрос приходят EventRequestWillBeSent — заявка с адресом и типом; затем EventResponseReceived — заголовки и статус; затем EventLoadingFinished — сигнал, что браузер получил тело целиком. Сбой вместо финиша — EventLoadingFailed с ErrorText и флагом Canceled: запрос не доехал, и ждать для него тело бессмысленно. Запрос и ответ связываются RequestID: держите карту, где ID — ключ, а значение — адрес и тип, и таймлайн каждого запроса соберётся в одну строку. Пара практических следствий. Первое: фильтруйте рано — условие по домену и типу прямо в колбэке дешевле, чем потом разгребать срез из сотни адресов. Второе: события приходят и на навигацию самого документа — Document-запросы попадают в тот же поток, поэтому ResourceTypeXHR — не украшение, а способ не смешивать страницу и её API.

Тело ответа: JSON, которого нет в DOM#

Собрать адреса — половина дела; главный приз — тело XHR-ответа с данными. Механика двухшаговая: в колбэке запоминаете RequestID интересного ответа — например, по суффиксу адреса и MIME-типу application/json, — а после Run вызываете network.GetResponseBody(id).Do(ctx): действие возвращает тело и флаг base64, потому что бинарные ответы протокол отдаёт закодированными. После Run вызов безопасен: поток событий отработал, дедлока нет. Флаг base64 — не экзотика: протокол им отвечает и на бинарные, а иногда и на большие текстовые ответы, поэтому проверять его нужно всегда. Для главного документа есть готовое действие chromedp.RunResponse: оно вернёт *network.Response страницы, на которую приехала навигация, со статусом и адресом — без ручного перехвата. Кейс собран из этих частей: страница-отчёт грузит таблицу запросом к /api/quotes, селектор WaitVisible убеждается, что данные приехали, GetResponseBody отдаёт исходный JSON, и json.Unmarshal разбирает его в структуру — минуя весь слой разметки. Кейс честно двухслойный: слушатель — для адресов и ID, а чтение тела — обычное действие после Run. Разделение выглядит дольше, чем «всё в одном колбэке», но именно оно делает перехват стабильным: слушатель не блокируется, а данные достаются в удобном месте с нормальными проверками ошибок.

Вытащить JSON из XHR-ответа

GO
var apiID network.RequestID
chromedp.ListenTarget(ctx, func(ev any) {
        switch ev := ev.(type) {
        case *network.EventResponseReceived:
                if strings.HasSuffix(ev.Response.URL, "/api/quotes") &&
                        strings.Contains(ev.Response.MimeType, "json") {
                        apiID = ev.RequestID
                }
        }
})

if err := chromedp.Run(ctx,
        chromedp.Navigate("https://example.com/quotes"),
        chromedp.WaitVisible(".quotes"),
); err != nil {
        log.Fatal(err)
}

raw, b64, err := network.GetResponseBody(apiID).Do(ctx)
if err != nil {
        log.Fatal(err)
}
if b64 { // бинарные тела протокол отдаёт в base64
        raw, _ = base64.StdEncoding.DecodeString(raw)
}
log.Println("json:", raw)

Колбэк запоминает RequestID только нужного ответа: суффикс адреса и MIME-тип отсекают всё лишнее. GetResponseBody вызывается после Run — события уже обработаны, блокировки нет. Возвращаются тело и флаг base64: текст оставляем как есть, бинарное декодируем стандартным пакетом encoding/base64, и на выходе исходный JSON со страницы.

Блокировка трекеров: Fetch-домен и паттерны#

Перехват умеет не только слушать — тот же протокол позволяет и запрещать. За это отвечает Fetch-домен: fetch.Enable с паттернами объявляет перехват по шаблону адреса и стадии запроса, на каждый подходящий запрос приходит событие RequestPaused, а решение принимается командой FailRequest — отменить — или ContinueRequest — пропустить. Классическое применение: отсечь трекеры и рекламные домены по URL-паттерну ещё до отправки, и страница грузится быстрее и дешевле — экономия трафика на картинки, аналитику и шрифты достигает десятков процентов. Подробный разбор с примерами — отдельная тема восемнадцатого урока про блокировку картинок и шрифтов. Паттерны — это строки в духе ://tracker.example/: чем уже шаблон, тем меньше событий перехвата и выше скорость, а слишком широкие шаблоны режут и нужные запросы. Здесь важна одна оговорка: Fetch-домен появился в протоколе не с рождения, и на старых версиях браузера паттерны просто не отработают — механизм зависит от версии протокола DevTools, о чём честно говорит таблица ниже. Тонкость стадий: перехват можно ставить на запрос — тогда решение принимается до отправки, — и на ответ — тогда можно переписать заголовки. Для трекеров нужна ранняя стадия: зачем загружать то, что вы всё равно отмените.

Задача — событие или механизм#

ЗадачаСобытие или механизмЧто несёт
адрес каждого запросаnetwork.EventRequestWillBeSentRequestID, Request.URL, метод, ev.Type: XHR или документ
статус и заголовки ответаnetwork.EventResponseReceivedev.Response.Status, .Headers, .MimeType
тело ответаnetwork.GetResponseBody(id).Do(ctx)тело плюс флаг base64; вызов после Run
ответ главного документаchromedp.RunResponse*network.Response навигации без ручного перехвата
сбой загрузкиnetwork.EventLoadingFailedev.ErrorText и флаг Canceled
отмена по шаблонуfetch.Enable + FailRequestперехват RequestPaused; зависит от версии протокола
новые цели браузераchromedp.ListenBrowserбраузерный уровень; target-события не приходят

Границы механизма: версии и потоки#

События сети честны, как протокол, и имеют те же границы. Первое — версии: cdproto повторяет протокол DevTools, протокол меняется вместе с Chrome, и механизм, работающий на свежем браузере, может отсутствовать или отличаться на старом; Fetch-домен — свежий случай, базовые network-события — старые и стабильные. Второе — полнота: поток событий отражает взгляд браузера, редиректы переписывают RequestID, кэш может схлопнуть запрос до одного события, service worker перехватывает часть трафика по своим правилам — точный подсчёт всех сетевых операций протокол не обещает. Третье — дисциплина потоков: колбэк синхронный и короткий, вся тяжёлая работа — после Run или в своей goroutine. Проверяйте перехват на той версии браузера, с которой пойдёт в бой: локальный прогон на установленном Chrome даёт правду, а отчёт о событиях полезно писать в лог — расхождение с ожиданием видно сразу. Отсюда и тестирование: прогон на той же версии Chrome, что в продакшене, снимает большинство сюрпризов заранее. И ещё одна граница — права: перехватывает не chromedp, а браузер; библиотека лишь передаёт команды протокола, и всё, чего нет в протоколе, не появится от усилий на стороне Go.

Чек-лист перехвата#

  • подписка до Run: ListenTarget ставится заранее, чтобы не потерять первые события
  • колбэк короткий: собираем ID и адреса, тяжёлое — после Run или в goroutine
  • фильтр по ev.Type и домену: XHR от документа отличает ResourceTypeXHR
  • тело ответа — GetResponseBody по RequestID с флагом base64
  • главный документ — RunResponse: статус и адрес навигации без ручных событий
  • Fetch-паттерны для отмены трекеров: механика зависит от версии протокола

Проверка: где ловить сеть

Вы подписались через chromedp.ListenBrowser и ждёте *network.EventRequestWillBeSent, но событий нет. Почему?

Проверка: дедлок в колбэке

Внутри колбэка ListenTarget вы вызываете network.GetResponseBody(...).Do(ctx) за телом только что увиденного ответа. Что произойдёт?

Сколько адресов соберёт фильтр

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

chromedp.ListenTarget(ctx, func(ev any) {
        switch ev := ev.(type) {
        case *network.EventRequestWillBeSent:
                if ev.Type == network.ResourceTypeXHR &&
                        strings.Contains(ev.Request.URL, "api.example.com") {
                        urls = append(urls, ev.Request.URL)
                }
        }
})
// страница грузит: /api/data с api.example.com,
// /logo.png со своего домена и /stats.js с tracker.io

Соберите слушатель сети

Восстановите каркас: подписка, интересующее событие и достаётся тело ответа.

chromedp.(ctx, func(ev any) { switch ev := ev.(type) { case *network.: ids[ev.RequestID] = ev.Response.URL } }) raw, b64, err := network.(id).Do(ctx)

Расставьте каркас перехвата

Соберите по порядку: контекст, подписка, сбор XHR, загрузка страницы и достаём тело.

Строки

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

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

Карточки: перехват запросов

Термин

ListenTarget против ListenBrowser

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

Сеть — события вкладки: ловит ListenTarget. ListenBrowser подписан на браузерный уровень — новые цели, но не события вкладок.

1 / 6

Что дальше: перехват — источник данных и рычаг экономии. Восемнадцатый урок ставит его на боевую ногу: блокировка картинок и шрифтов — паттерны Fetch-домена, лёгкий профиль и ускорение страниц. Девятнадцатый собирает масштаб: пул браузеров поверх вкладок из шестнадцатого урока — и перехват там ездит параллельно на каждую вкладку. Практика — локально: песочница сайта работает без браузера и сетевых событий Chrome, поэтому слушатель проверяется на настоящем Chrome и своей странице с XHR. Домашняя проверка на честность: возьмите любой новостной сайт, соберите все XHR за загрузку и отсортируйте по доменам — увидите и API-эндпоинт с данными, и полдюжины трекеров, которые восемнадцатый урок научит отключать. Слушатель из этого урока переезжает в любой масштаб без изменений: подписка и фильтры не знают, один браузер в программе или десять.

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

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

Вывод

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

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

  • Сетевые события — события вкладки: их ловит ListenTarget, а ListenBrowser видит только браузерный уровень.
  • Network-домен включается самим Run: подписка ставится до Run, чтобы не потерять первые события навигации.
  • EventRequestWillBeSent даёт адрес, метод и тип ресурса; ResourceTypeXHR отличает XHR от документов и скриптов.
  • EventResponseReceived несёт статус, MIME-тип и заголовки ответа в ev.Response.
  • Тело ответа — network.GetResponseBody по RequestID после Run: тело плюс флаг base64.
  • Колбэк синхронный: не блокировать, действия — после Run или в отдельной goroutine, иначе дедлок.
  • chromedp.RunResponse возвращает ответ главного документа навигации без ручных событий.
  • Блокировка трекеров — Fetch-домен с паттернами и FailRequest; механика зависит от версии протокола DevTools.
  • Отсечённые трекеры — это ускорение и экономия трафика: страница грузит только то, что нужно парсеру, без аналитики и рекламы.

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

Соберите перехватчик на тестовом сервере: страница с двумя XHR — свой /api/quotes с JSON и внешние трекерские адреса. Слушателем соберите все запросы, отфильтруйте XHR по домену и типу, после Run достаньте тело ответа через GetResponseBody и распечатайте JSON. Затем добавьте монитор статусов: на каждый не-200 пишите адрес и код в лог. Финал — сравнение: загрузите ту же страницу без подписки и с подпиской, убедитесь, что подписка до Run не теряет первых событий, и запишите, сколько событий сети прошло за одну загрузку. Заодно запишите время загрузки с подпиской и без — цена слушателя на ваших страницах должна быть незаметной.

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

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

Почему я вижу запрос дважды или RequestID меняется по ходу?

Редиректы: каждый скачок порождает новое событие с новым RequestID, а кэш может схлопнуть операцию до одного события. Смотрите не на количество событий, а на итоговый ответ: фильтруйте по финальному URL и статусу. Для строгого учёта сопоставляйте события по RequestID в карте — так видно цепочку редиректа целиком, а дубли отсекаются при подсчёте без отдельного прохода по логу.

GetResponseBody вернул ошибку: тело недоступно. Что случилось?

Тело живёт в браузере ограниченно: запрос отменён, кэш вытеснен или вкладка ушла — протокол отдаёт тело по RequestID, пока браузер его держит. Вызывайте по горячим следам: сразу после Run в рамках того же контекста и той же вкладки. Для больших ответов включайте учёт через EventLoadingFinished и не тяните тела сотнями вкладок параллельно — буферы браузера не резиновые.

Чем перехват лучше парсинга DOM?

Контракты: API-эндпоинт — это структурированный JSON с типами, а DOM — вёрстка, которая меняется от редизайна. Данные из XHR не требуют селекторов, меньше зависят от рендеринга и часто уже содержат то, что в DOM размазано по элементам. Ограничение тоже честное: не все данные приходят XHR'ом, часть рендерится на сервере, поэтому зрелый парсер сочетает оба пути — и это тема одиннадцатого урока про [динамический контент](/courses/chromedp/lessons/11-dinamicheskij-kontent/).

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

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

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

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

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

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

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