Содержание урока · 10 разделов
- chromedp text: видимый текст узла
- TextContent, InnerHTML и OuterHTML: сырой уровень
- AttributeValue и Attributes: атрибуты из разметки
- Атрибут и свойство: Value против AttributeValue
- Шесть читающих действий урока
- Где данные прячутся чаще всего
- Чтение в цикле: цена переспросов
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Страница, открытая chromedp, — уже не HTML-ответ, а живое дерево: браузер собрал его, отрисовал и продолжает обновлять по мере работы скриптов. Colly на этом шаге закончила бы работу, а chromedp только начинает: всё, что видно посетителю, доступно программе. Извлечение данных go-программой устроено одинаково для всех действий этого урока: действие дожидается узлов по селектору, берёт первый совпавший, исполняет на нём браузерную функцию и расфасовывает результат в переменную, адрес которой вы передали указателем. Указатель здесь не украшение, а канал доставки: в него записывается значение, поэтому в Run передают &name, &html, &ok. Как и в четвёртом уроке про селекторы, Query-действия сами переспрашивают документ с коротким интервалом, и отдельное «подождать» перед чтением не нужно. Отсюда и стиль кода: чтение — это список действий внутри одного Run, где каждая строка кладёт свой кусок данных в свою переменную, а ошибка любой строки останавливает весь прогон. Плата за удобство — типизация: каждая переменная объявляется до Run, и тип её должен совпадать с тем, что вернёт действие: строка для текста, bool для ok, карта для атрибутов.
chromedp text: видимый текст узла#
chromedp.Text возвращает innerText — то, что посетитель видит на экране, со всеми переносами и регистром, как это отрисовал браузер. Внутри действия лежит маленький JavaScript-сниппет, и у него есть условие: у узла должны быть ненулевые offsetWidth, offsetHeight или непустые клиентские прямоугольники. Если элемент скрыт, сниппет возвращает пустую строку — и действие завершается успешно, без ошибки. Это решение библиотеки ломает ожидания у половины новичков: ошибка сигнализирует о неточном селекторе или истёкшем дедлайне, а про скрытый элемент текст молчит. Вторая особенность innerText — зависимость от рендера: содержимое display:none-детей в результат не попадёт, текст из script и style тоже, а строки перенесутся примерно так, как они выглядят на экране. Для витрин и карточек это ровно то, что нужно: заголовок, цена, статус — всё это текст, который пользователь реально видит. Для скрытых заглушек и аккордеонов нужен TextContent, о котором ниже.
TextContent, InnerHTML и OuterHTML: сырой уровень#
Три действия снимают зависимость от рендера и отдают сырьё. TextContent читает textContent поддерева: весь текст всех потомков подряд, независимо от видимости — включая содержимое скрытых блоков и тегов script и style. Плата за полноту — отсутствие форматирования: колонки таблицы слипнутся в одну строку, и разбирать их придётся вручную, зато ничего не потеряется. InnerHTML возвращает разметку поддерева без самого узла, OuterHTML — узел целиком вместе с открывающим и закрывающим тегом. Практика простая: текст на экране — Text; данные из скрытых областей — TextContent; вложенная структура вроде таблицы позиций, которую хочется разобрать дома, — InnerHTML, а дальше goquery или стандартная библиотека encoding/html, те же приёмы, что и в HTTP-скрапинге. Разбор разметки у себя дешевле, чем десять чтений по ячейкам: один обмен с браузером — и таблица у вас в строке, дальше она режется без участия Chrome. OuterHTML в продакшен-извлечении редкость, зато незаменим при отладке: сохранили узел в файл, открыли глазами, увидели, что селектор ловит не тот узел, который вы имели в виду. Так находят и вторую половину багов чтения: не «узел не нашёлся», а «узел нашёлся не тот» — класс, который встречается четыре раза, и первый из них не карточка.
Карточка товара: текст, скрытая спецификация, ссылка
GOvar name, spec, href string
var ok bool
err := chromedp.Run(ctx,
chromedp.WaitVisible(".product-card", chromedp.ByQuery),
chromedp.Text(".product-card h1", &name),
chromedp.TextContent(".product-card .specs", &spec),
chromedp.AttributeValue(".product-card a", "href", &href, &ok),
)Каждая строка кладёт своё значение в свою переменную: Text берёт видимый заголовок, TextContent вытаскивает текст блока спецификаций, AttributeValue читает href первой ссылки и ставит ok в true, потому что атрибут на месте. WaitVisible на контейнере дисциплинирует прогон: чтение начинается только после того, как карточка отрисована целиком.
AttributeValue и Attributes: атрибуты из разметки#
AttributeValue читает один атрибут из карты атрибутов узла — той, что собрана из HTML-разметки. Сигнатура с двумя указателями выглядит непривычно: chromedp.AttributeValue(sel, "href", &href, &ok), но именно ok несёт смысл: атрибут есть — true, атрибута нет — false, и ошибки в обоих случаях не будет. Отсутствие атрибута — нормальный ответ, а не авария: у большинства ссылок не бывает title, у картинок без описания нет alt. Проверяйте ok, а не стройте догадки. Когда нужны сразу все атрибуты узла — витрины часто держат в data-свойствах идентификатор товара, цену в копейках и флаги наличия — есть Attributes, заполняющая map[string]string одним обменом с браузером. Для коллекций то же самое делает AttributesAll с опцией ByQueryAll: на выходе срез карт, по одной на узел, порядок совпадает с порядком совпадений селектора. Это первый инструмент экономии, когда карточек много: одна команда вместо дюжины. Один нюанс расфасовки: карта в Go не сохраняет порядок атрибутов, поэтому для отчётов, где колонки важны, значения вынимают по ключам явно, а не перебирают карту.
Атрибут и свойство: Value против AttributeValue#
Тонкое место, на котором спотыкаются даже опытные скраперы: у полей ввода value существует в двух лицах. Атрибут value — это снимок из разметки, начальное значение, которое браузер подставил при отрисовке. Свойство value — текущее содержимое поля, живущее в JavaScript-объекте элемента и меняющееся с каждым вводом. chromedp.Value читает именно свойство: универсальный механизм JavascriptAttribute исполняет на узле функцию this[n] и возвращает то, что лежит в свойстве n. AttributeValue с именем value читает атрибут, и после того, как программа или пользователь что-то напечатали, эти два значения расходятся. То же касается checked у чекбоксов и disabled у кнопок и почти любого состояния, которое страница меняет на лету: атрибут в разметке мог остаться прежним, а свойство уже новое. Типовой сценарий — фильтр каталога: страница загрузилась с дефолтным регионом в поле, скрипт региональной переустановки тут же подменил его на ваш город, и атрибут остался со старым значением. Правило выбора короткое: актуальное состояние — свойство, декларация из HTML — атрибут. Стоимость проверки — один обмен с браузером, дешевле обойтись без неё не получится.
Расхождение в цифрах
GO// в разметке: <input id="coupon" value="ГОД2023">
var fromHTML, current string
var ok bool
err := chromedp.Run(ctx,
chromedp.AttributeValue("#coupon", "value", &fromHTML, &ok),
chromedp.SendKeys("#coupon", "-7", chromedp.ByID),
chromedp.Value("#coupon", ¤t),
)AttributeValue вернёт ГОД2023 и ok в true — так написано в HTML. SendKeys допечатает дефис и семёрку к текущему содержимому поля, и Value честно вернёт ГОД2023-7. Разница между двумя чтениями и есть разница между снимком разметки и живым свойством; во всех сценариях с формами и фильтрами актуальное значение берут через Value.
Парсер каталога ходит по вкладкам товара; у второй вкладки блок цены свёрстан с display:none до клика. Text возвращает пустую строку, действие проходит успешно, программа пишет в отчёт цену «пусто» — и так сотнями строк. Сломалось не извлечение, а предположение, что успех означает данные. Лечение в три слоя: WaitVisible на читаемом узле перед Text, TextContent для областей, которые обязаны быть скрытыми, и проверка длины результата с отдельной веткой лога, куда попадает и адрес страницы, и название вкладки. Спорное мнение с условием: читать вслепую быстрее, но цена одного тихого прогона с пустыми ценами — неделя недоверия к отчётам.
Шесть читающих действий урока#
| Действие | Что возвращает | Когда брать |
|---|---|---|
| chromedp.Text | innerText видимого узла; пустая строка, если элемент скрыт | текст, как его видит посетитель: заголовки, цены, статусы |
| chromedp.TextContent | textContent поддерева целиком, видимость не важна | скрытые блоки, аккордеоны, содержимое script и style |
| chromedp.InnerHTML | разметку поддерева без внешнего тега | вложенные структуры, которые хочется разобрать у себя |
| chromedp.OuterHTML | узел целиком с тегами | сохранение фрагмента для отладки и отчёта |
| chromedp.AttributeValue | один атрибут из разметки плюс флаг ok | href, alt, data-свойства; отсутствие атрибута — не ошибка |
| chromedp.Value | текущее JS-свойство value | актуальное содержимое полей после ввода |
Где данные прячутся чаще всего#
- data-атрибуты витрин: идентификатор товара, цена в копейках, флаги наличия — читаются AttributeValue и Attributes
- href ссылок: относительные пути дорасширяются до полного адреса уже в Go
- value полей фильтров после ввода — только Value, атрибут врёт
- innerHTML таблицы позиций: одна команда вместо десятка селекторов по ячейкам
- alt и title картинок: пояснения, которые авторы оставляют в атрибутах
- текст бейджей состояния — «Товар заканчивается», «Наличие уточняйте» — только Text по видимому узлу
Чтение в цикле: цена переспросов#
Каждое читающее действие — это обмен с браузером: селектор уехал, узел найден, функция исполнена, результат вернулся. На десятке полей это незаметно, но цикл по двумстам карточкам, где на каждую читается пять значений, превращается в тысячу обменов, и время обхода растёт линейно. Первый уровень экономии — группировать: AttributesAll вынимает атрибуты всех карточек одним действием, InnerHTML карточки снимает сразу всё поддерево, и разбор идёт уже без браузера, чистым Go. Второй уровень — JavaScript: одно выражение chromedp.Evaluate соберёт массив значений со всей страницы за один обмен — об этом десятый урок. Третий — работа с таблицами целиком: когда страница и есть большая таблица, приёмы из урока про скрапинг таблиц сокращают число действий на порядок. Экономить стоит тогда, когда обход реально медленный: переписывать чтение двух полей ради двух миллисекунд — работа в минус. И не путайте группировку с халтурой: InnerHTML вместо пяти Text — это не «сложнее», это другая точка разбора, переносимая в тесты, где браузера нет вовсе.
Отдельная дисциплина — отличать пустые данные от пустой выдачи. Если селектор не нашёл ни одного узла, Run вернёт ошибку: действие честно признаёт, что искать нечего. Если узел нашёлся, но данных в нём нет, ошибки не будет — будет пустая строка или false от ok. Это разные состояния каталога: первая ситуация означает, что страница не та, сломался селектор или сайт вёл себя иначе; вторая — что товар без цены или спецификации, и это данные тоже, только пугающие. В логе они должны выглядеть по-разному: первая — ошибка прогона с адресом страницы, вторая — строка в отчёте с пометкой. Соблазн свалить всё в одну кучу велик, пока не наступает день, когда сайт поменял вёрстку, и нужно за минуту понять, сломался парсер или кончился товар.
Проверка: Text на скрытом узле
Блок цены свёрстан с display:none. Что вернёт chromedp.Text по этому блоку?
Проверка: отсутствующий атрибут
У ссылки нет атрибута title. Что вернёт chromedp.AttributeValue(sel, "title", &v, &ok)?
Что окажется в переменных
Прочитайте код и выберите, что он выведет:
// <input id="q" value="тапочки">
// err := chromedp.Run(ctx,
// chromedp.AttributeValue("#q", "value", &attr, &ok),
// chromedp.SetValue("#q", "носок", chromedp.ByID),
// chromedp.Value("#q", ¤t),
// )Соберите чтение карточки
Восстановите действия: видимый текст, текст скрытого блока, атрибут ссылки и текущее значение поля.
Расставьте извлечение по порядку
Соберите программу чтения карточки: переменные, запуск, три действия, обработка ошибки.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: чтение данных
Чтение — такой же кандидат на проверку, как ввод из пятого урока: длина строки, ok от атрибута, число прочитанных элементов. Пара лишних строк в цикле превращает тихую пустоту в громкую ошибку с номером карточки в логе. Для InnerHTML-разбора тот же принцип работает после: ячейка не распарсилась — это тоже аномалия, а не повод молча пропустить строку.
Данные прочитаны — пора снимать артефакты рендера. Седьмой урок про скриншоты страниц и элементов покажет три уровня захвата: окно браузера, вся страница и отдельный узел. Девятый урок про умные ожидания закроет случаи, когда данных ещё нет и ждать нужно условие, а не узел. Оговорка про практику: всё это проверялось локально — понадобится локальный Go и установленный Chrome; интерактивы сайта и тематическая программа в песочнице механику подскажут, но живой DOM они не заменят.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Чтение в chromedp — Query-действия: сами дожидается узлов, берут первый совпавший и расфасовывают результат в указатели.
- Text возвращает innerText видимого узла; скрытый элемент даёт пустую строку без ошибки.
- TextContent читает всё поддерево независимо от видимости; InnerHTML и OuterHTML отдают разметку для разбора в Go.
- AttributeValue читает атрибут из разметки и ставит ok; отсутствие атрибута — штатный ответ, а не ошибка.
- Value читает текущее свойство value: после ввода оно расходится с атрибутом из HTML.
- Attributes и AttributesAll снимают все атрибуты узлов и режут число обменов в цикле.
- Агрегаты дешевле собирать одним выражением Evaluate — тема десятого урока.
- Нет узлов — ошибка Run; узел есть, данных нет — пустая строка: эти состояния каталога различают в логах.
- Проверка длины и ok превращает тихую пустоту в ошибку, которую видно в логе.
Домашнее задание
Возьмите страницу с карточкой товара и соберите полное извлечение: название и цену через Text, скрытую спецификацию через TextContent, href и два data-атрибута через AttributeValue с проверкой ok, содержимое поля фильтра через Value после SetValue. Затем спрячьте цену стилем display:none и убедитесь, что Text вернёт пустую строку без ошибки, — добавьте проверку длины и отдельную ветку лога для этого случая.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему Text вернул пустую строку, хотя элемент на странице есть?
Элемент скрыт: сниппет Text проверяет offsetWidth, offsetHeight и клиентские прямоугольники и у невидимых узлов возвращает пустую строку, завершаясь успешно. Дождитесь видимости WaitVisible или читайте TextContent, если скрытость нормальна. Пустой результат без ошибки — сигнал про видимость, а не про селектор: неточный селектор даёт ошибку отсутствия узлов. Третий вариант — элемент пуст по данным, и это тоже проверяется длиной.
Чем Attributes отличается от AttributeValue?
AttributeValue читает один атрибут и возвращает пару строка плюс ok; Attributes снимает всю карту атрибутов первого узла в map[string]string одним обменом. В цикле по карточкам карта на карточку часто дешевле, чем пять отдельных AttributeValue, а AttributesAll распространяет тот же подход на все совпадения селектора сразу. Помните про порядок: карта его не сохраняет, для отчётов значения вынимают по ключам.
Как читать значение поля, которое пользователь или скрипт уже изменил?
Только chromedp.Value: он исполняет на узле чтение свойства value, то есть актуального состояния. Атрибут value в разметке остаётся исходным снимком и после ввода врёт. То же правило для checked и disabled: состояние живёт в свойствах, разметка хранит декларацию. Проверяется это элементарно: прочитайте оба значения после ввода и сравните — расхождение станет наглядным.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Testcontainers в Go: интеграционные тесты с Docker
Курс testcontainers-go: Postgres, Redis, Kafka и Mongo в тестах через Docker, wait-стратегии, сети, параллельность, CI и итоговый стенд. 25 уроков.
- Поднимать реальные зависимости в Docker из кода теста
- Использовать модули Postgres, Redis, MongoDB, Kafka и RabbitMQ
- Ждать готовность сервиса wait-стратегиями вместо sleep
- Собирать стенды из нескольких контейнеров и своего образа