Содержание урока · 10 разделов
- chromedp evaluate: выражение и указатель
- Возврат значений: undefined, null и сырые байты
- js в браузере go: границы уместности
- Пять способов забрать значение
- EvaluateAsDevTools: контекст консоли
- Аргументы, исключения и порядок работы
- Практики безопасной оценки
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
У чтения из шестого урока есть экономический предел: каждое действие — отдельный обмен с браузером, и на больших страницах счёт идёт на сотни. Но у страницы под рукой есть исполнитель, который видит весь DOM целиком, — JavaScript. Один цикл Array.map по селектору собирает все заголовки за один проход, и именно это делает chromedp evaluate: отправляет выражение в контекст страницы, где доступны document, переменные скриптов сайта и всё состояние, которое фреймворк уже посчитал, и привозит результат в Go. Это не обход библиотеки, а её нижний этаж: сам протокол DevTools исполняет выражения той же командой Runtime.evaluate, и chromedp лишь даёт ей удобную обёртку. Отсюда и место действия в курсе: селекторы отвечают за поиск, чтение — за узлы, а Evaluate — за всё, что проще посчитать на месте, чем привозить по кусочкам. Скролл страницы из FAQ седьмого урока, агрегаты по таблицам, чтение данных SPA до того, как они попали в DOM, — всё это один и тот же механизм с разными выражениями. В этом уроке — как устроен возврат значений, какие ошибки он приносит и где заканчивается здравый смысл писать скрапер на JS-строках.
chromedp evaluate: выражение и указатель#
Сигнатура короткая: chromedp.Evaluate(expression, res). Первый аргумент — строка с выражением JavaScript, второй — указатель на приёмник результата; передадите nil, и результат будет проигнорирован, действие только исполнит код — так выглядит прокрутка window.scrollTo из седьмого урока. Механика возврата для обычных приёмников такая: результат выражения кодируется в JSON по значению, и стандартный json.Unmarshal раскладывает его в вашу переменную. Число ложится в int или float64, строка — в string, массив — в срез, объект — в карту или структуру с json-тегами. Это значит, что тип приёмника — ваша ответственность: выражение вернёт массив, а приёмник объявлен строкой — получите ошибку распаковки, и она будет честной ошибкой Go с описанием. Отсюда привычка подбирать пару «выражение плюс тип»: соберёте на JS массив строк — берите []string, посчитаете сумму — берите float64 или int, разберёте объект — карту map[string]any или структуру. Evaluate не дожидается ничего: он исполняет выражение в текущем состоянии страницы, поэтому перед ним стоят те же ожидания готовности, что и перед чтением узлов. Ошибка внутри выражения — обращение к несуществующему свойству, ошибка синтаксиса — возвращается как ошибка действия, и Run останавливается: тихого прохождения мимо не бывает.
Все заголовки карточек одним обменом
GOvar titles []string
err := chromedp.Run(ctx,
chromedp.WaitVisible(".cards"),
chromedp.Evaluate(
`Array.from(document.querySelectorAll('.card h3'))
.map(e => e.textContent)`,
&titles,
),
)
if err != nil {
panic(err)
}
fmt.Println("карточек:", len(titles))Выражение собирает все узлы по селектору и возвращает массив текстов; результат кодируется в JSON и распаковывается в срез строк. WaitVisible дожидается контейнера — Evaluate сам ничего не ждёт. Один обмен с браузером вместо сотни действий Text: на больших страницах разница видна в секундах, а не миллисекундах.
Возврат значений: undefined, null и сырые байты#
Три особые формы приёмника закрывают края случая, и о них стоит знать заранее. Первая — ошибка undefined: если выражение вернуло undefined или null, а приёмник не умеет быть nil — строка, число, структура, — Evaluate вернёт ошибку: encountered an undefined value или encountered a null value. Приёмники, которые nil умеют, — указатели, срезы, карты — примут undefined молча: срез останется nil, и это штатный путь выразить «данных нет». Вторая особая форма — сырые байты: приёмник []byte получает JSON-кодировку результата как есть, без распаковки в тип; это способ забрать значение сложной формы и разобрать его у себя, когда писать JS-разбор не хочется, а промежуточная структура не нужна. Третья — низкоуровневый приёмник *runtime.RemoteObject: вы получаете объект протокола без попытки конвертации — класс объекта, идентификатор, тип; живые объекты браузер держит у себя, пока страница не закрыта или не переезжает, и освобождать их можно командами runtime.ReleaseObject, если сессия длинная и объектов много. Для повседневных задач хватает первого пути; RemoteObject нужен, когда результат — функция, узел DOM или большой объект, который вы хотите исследовать по частям, а не тащить целиком в Go.
js в браузере go: границы уместности#
Сила Evaluate провоцирует: раз можно всё — почему не перенести в JS-строку половину парсера? Здесь полезна граница, которую стоит провести заранее. Уместное: агрегаты одним обменом, прокрутка, чтение состояния, которое не попало в DOM — переменные скрипта, объекты аналитики, данные в памяти фреймворка; проверка сложной готовности, которую селектором не выразить. Неуместное: валидация, ветвления и маршрутизация парсера внутри JS-строк — код без типов, без отладчика Go, без тестов, и каждая правка — редактирование строки в кавычках. Спорное мнение с условием: логика должна жить в Go, а JS — только читать и трогать страницу; исключение, при котором мнение можно перевернуть, одно — когда страница сама держит данные в недоступном селекторам виде, и короткий JS-запрос к её состоянию экономит десятки действий. Хороший маркер уместности: если выражение умещается в одну-две строки и не содержит ветвлений, это работа для Evaluate; если третье — сигнал вернуться к действиям библиотеки. Второй практический предел — аргументы: у Evaluate нет параметра для значений, аргументы подставляются в строку выражения. Подставляйте через JSON-кодирование и экранирование, а не конкатенацией: сырая склейка строк с данными с сайта — прямой путь к сломанному выражению, а в худшем случае к исполнению чужого кода в вашей сессии. Для предикатов с аргументами есть PollFunction с WithPollingArgs из девятого урока — там подстановка безопасна и встроена.
Пять способов забрать значение#
| Способ | Что возвращает | Когда брать |
|---|---|---|
| Text, AttributeValue, Value | одно значение одного узла | точечное чтение по селектору |
| Evaluate с []string или структурой | агрегат по значению: JSON в Go-тип | массивы, суммы, объекты одним обменом |
| Evaluate с *[]byte | сырую JSON-кодировку результата | сложная форма, разбор на своей стороне |
| Evaluate с **runtime.RemoteObject | живой объект протокола: класс, ObjectID | функции, узлы, большие объекты |
| EvaluateAsDevTools | то же плюс Command Line API консоли | скрипты с $ и $$, работа как в DevTools |
EvaluateAsDevTools: контекст консоли#
Вторая обёртка над той же командой протокола — chromedp.EvaluateAsDevTools: выражение исполняется так, как его исполняет консоль панели разработчика. Разница в двух деталях: выражение живёт в контексте объекта группы console, и ему доступен Command Line API — те самые сокращения консоли, включая выбор элементов dollar-скриптами $ и $$. Для задач «сделай, как я сделал в DevTools» это прямой путь: выражение, проверенное в консоли, переносится в код без переписывания dollar-сокращений на чистый querySelector. У библиотеки на этом механизме стоят привычные действия: Title и Location из третьего урока — обёртки над EvaluateAsDevTools с выражениями document.title и document.location.toString(), так что вы уже пользовались этой обёрткой, не зная о ней. Цена за удобство обозначена в исходниках честно: не используйте EvaluateAsDevTools с недоверенным JavaScript — Command Line API расширяет возможности исполняемого кода, и запускать в этой песочнице стоит только то, что вы написали сами или проверили. Рядом живут и опции нижнего уровня: EvalIgnoreExceptions заставляет оценку не падать на исключениях, EvalAsValue принудительно кодирует результат по значению — те же рычаги, которыми настроена обычная обёртка.
Живой объект протокола
GOvar res *runtime.RemoteObject
err := chromedp.Run(ctx,
chromedp.Evaluate(`window`, &res),
)
if err != nil {
panic(err)
}
fmt.Println(res.ClassName)Приёмник **runtime.RemoteObject получает объект протокола без конвертации: здесь это глобальное окно страницы с ClassName Window. У объекта есть идентификатор ObjectID, по которому к нему можно обращаться следующими командами протокола; живые объекты браузер держит до закрытия или навигации страницы, при длинной сессии их освобождают ReleaseObject.
Типичная авария: в выражение конкатенацией подставили данные — часть пути, номер страницы, слово из запроса. На тестовых данных работало, на реальном товаре с кавычкой в названии выражение сломалось, а на злонамеренной строке исполнило бы что угодно в контексте страницы. Сломался не Evaluate, а склейка строк там, где нужна кодировка значений. Лечение: значения в выражение — через JSON-кодирование с экранированием, аргументные предикаты — через PollFunction с WithPollingArgs, а всё, что можно выразить селектором, — селектором. Спорное мнение с условием: держать логику в Go, JS — только чтение; переворачивается оно, когда данные живут в памяти фреймворка и не видны селекторам — тогда короткий JS-запрос к состоянию честнее двадцати действий.
Аргументы, исключения и порядок работы#
Соберём дисциплину оценки в правила, которые проверены на живых страницах. Выражение сначала живёт в консоли DevTools: написали, исполнили, увидели результат — и только потом переехало в Go; между консолью и Evaluate почти нет разницы, а цикл отладки в консоли в десять раз короче. Приёмник подбирается под форму результата и объявляется рядом с выражением: массив текстов — []string, сумма — число, объект — карта или структура; расхождение формы и типа даёт ошибку распаковки с понятным текстом. Undefined и null обрабатываются сознательно: если «нет данных» — норма, берите приёмники, умеющие nil, и проверяйте на nil, а не на ошибку. Для страницы, где результат появляется позже, Evaluate окружается ожиданиями: WaitVisible на узле или Poll с предикатом из девятого урока — оценка сама ждать не умеет. Крупные вычисления по сотням узлов делаются одним выражением с map и reduce: на стороне браузера это микросекунды, а расписывать ту же работу десятками действий — тратить обмены и время обхода. И держите выражения короткими: трёхэтажная JS-программа в строке — сигнал, что пора выносить её в отдельный файл или возвращаться к действиям библиотеки.
Практики безопасной оценки#
- отлаживайте выражение в консоли DevTools, потом переносите в Go без изменений
- тип приёмника объявляйте под форму результата: срез для массива, число для суммы
- undefined и null принимайте приёмниками, умеющими nil, и проверяйте на nil
- значения в выражение подставляйте через JSON-кодирование, не конкатенацией
- аргументные предикаты собирайте на PollFunction с WithPollingArgs
- перед оценкой дожидайтесь готовности: WaitVisible или Poll с предикатом
- EvaluateAsDevTools — только для своих и проверенных скриптов
JavaScript-выражение — часть кодовой базы: держите его в именованной константе рядом с селекторами, а не в строке внутри Run. Это даёт три выгоды: выражение видно при ревью, его можно переиспользовать в нескольких местах и легко подменить в тесте. Рядом же комментируйте форму результата — с каким приёмником выражение согласовано, чтобы правки не расходились.
Что дальше: оценка JavaScript — последний инструмент низкого уровня перед большой темой данных. Одиннадцатый урок применит всё это к SPA и динамическому контенту, где разметку собирает фреймворк, а состояние живёт в его памяти; четвёртый урок про селекторы вспомнится в связи с ByJSPath — путём элемента, который тоже исполняется оценкой. Практика — локально: понадобится Go и Chrome, песочница сайта исполняет чистый Go без браузера, поэтому настоящие JSON-обмены вы увидите только в своём терминале. Первый честный замер для домашней работы: страница с сотней узлов, время цикла Text против одного Evaluate — цифры убедят надёжнее слов.
Проверка: undefined в приёмник
Выражение вернуло undefined, приёмник — строка var s string. Что произойдёт?
Проверка: nil вместо приёмника
Зачем в Evaluate передают nil вместо указателя на результат?
Проверка: контекст консоли
Чем EvaluateAsDevTools отличается от Evaluate?
Что окажется в sum
Прочитайте код и выберите, что он выведет:
var sum int
// err := chromedp.Run(ctx,
// chromedp.Evaluate(`1+2`, &sum),
// )Соберите пару чтений
Восстановите приёмник агрегата и обёртку с контекстом консоли.
Расставьте агрегат по порядку
Соберите по порядку: приёмник, Run с ожиданием и оценкой, обработка ошибки.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: выполнение JavaScript
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Evaluate исполняет выражение в контексте страницы: доступны document, состояние скриптов и всё, что видит консоль.
- Результат возвращается по значению: JSON-кодирование и json.Unmarshal в приёмник; тип подбирается под форму результата.
- nil вместо приёмника — исполнить код ради побочного эффекта: прокрутка, вызов функции, флаг.
- undefined и null в приёмнике без nil — ошибки ErrJSUndefined и ErrJSNull; срезы, карты и указатели принимают их молча.
- *[]byte даёт сырой JSON, **runtime.RemoteObject — живой объект протокола с ClassName и ObjectID.
- EvaluateAsDevTools — контекст консоли и Command Line API: Title и Location из третьего урока — обёртки над ним.
- Аргументы подставляются в строку через JSON-кодирование; для предикатов — PollFunction с WithPollingArgs.
- Граница уместности: JS читает и считает, решает Go; выражение сначала проверяется в консоли DevTools.
Домашнее задание
Возьмите страницу с сотней однотипных узлов и сделайте два чтения одного и того же: цикл Text по селектору и один Evaluate с Array.from и map. Замерьте время обоих подходов и запишите разницу. Затем верните сумму числового поля одним выражением с reduce в float64, а объект window прочитайте через **runtime.RemoteObject и напечатайте ClassName. Финал — сломайте приёмник: прочитайте массив в строку и изучите текст ошибки распаковки.
Личные заметкиПусто
Вопросы и ответы по уроку
Evaluate вернул encountered an undefined value — что это значит?
Выражение вернуло undefined или null, а приёмник не умеет быть nil: строка, число, структура. Проверьте выражение в консоли и решите, откуда отсутствие данных: если это норма, возьмите приёмник, умеющий nil — указатель, срез или карту, — и проверяйте на nil. Если отсутствие неожиданно, ищите причину на странице: неверный селектор или данные, которые ещё не приехали.
Можно ли передать аргументы в Evaluate?
Отдельного параметра нет: значения подставляются в строку выражения. Делайте это через JSON-кодирование с экранированием — конкатенация ломает выражение на кавычках и открывает дорогу инъекции. Для предикатов с аргументами есть готовый путь: PollFunction из девятого урока принимает значения через WithPollingArgs и передаёт их функции безопасно.
Зачем EvaluateAsDevTools, если есть Evaluate?
Когда скрипт написан и проверен в консоли DevTools с dollar-сокращениями: EvaluateAsDevTools даёт тот же Command Line API, и выражение переезжает в код без переписывания. Сюда же относятся задачи, где удобно работать как в консоли. Для недоверенного кода обёртка не предназначена: API расширяет возможности исполняемого скрипта, поэтому её область — ваши собственные и проверенные выражения.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Testcontainers в Go: интеграционные тесты с Docker
Курс testcontainers-go: Postgres, Redis, Kafka и Mongo в тестах через Docker, wait-стратегии, сети, параллельность, CI и итоговый стенд. 25 уроков.
- Поднимать реальные зависимости в Docker из кода теста
- Использовать модули Postgres, Redis, MongoDB, Kafka и RabbitMQ
- Ждать готовность сервиса wait-стратегиями вместо sleep
- Собирать стенды из нескольких контейнеров и своего образа