Мы уже рассказывали о популярных библиотеках для языка Go, помогающих с парсингом страниц. Этот материал будет о библиотеке Chromedp: о том как её использовать, какие у неё есть особенности, как её установить и настроить. Для лучшего понимания приведём примеры кода и наиболее актуальные кейсы.
Chromedp – это свободная библиотека с открытым исходным кодом, написанная на языке программирования Go, она предназначена для обеспечения взаимодействия с безголовым браузером Google Chrome.
Тут сразу нужно пояснить, что такое безголовый браузер –это браузер, к которому можно обращаться по API, то есть с помощью программных вызовов и без отображения графического интерфейса.
Максимум подробностей в материале про Headless-браузеры.
С недавних пор Google Chrome имеет свой собственный официальный headless-режим (без задействования сторонних библиотек и плагинов, таких как Selenium WebDriver).
Штатный протокол Хрома, предназначенный для управления браузером из сторонних программ, называется Chrome DevTools Protocol. Обратите внимание, его поддерживает не только Хром, но и многие браузеры, построенные на его кодовой базе. В частности, речь о чистом Chromium и о специальной docker-сборке для парсинга headless-shell.
Основная проблема при интеграции безголового Хрома с любым внешним софтом – необходимость написания специального коннектора, который бы мог конвертировать программные вызовы в команды и синтаксис протокола Chrome DevTools Protocol.
Как раз эти задачи и берёт на себя библиотека Chromedp. Она позволяет управлять headless-Хромом без задействования внешних зависимостей из скриптов.
Естественно, чаще всего Chromedp применяется при парсинге web-страниц и при организации тестирования (сайтов и веб-приложений).
Чисто технически вы можете написать свой коннектор для безголового Хрома и вызывать команды Chrome DevTools Protocol непосредственно в своём коде. Но это сильно усложняет процесс написания программы и увеличивает время на разработку. Не проще ли воспользоваться готовой библиотекой, имеющей понятный синтаксис команд и примеры кода для всех типовых задач? Проще! И именно поэтому все разработчики парсеров на Golang пользуются Chromedp.
Возможности и преимущества библиотеки можно описать примерно так:
Но хватит теории, пора переходить к практической части.
Чтобы начать работать с библиотекой, на вашем устройстве должно быть установлено окружение языка Go. Если этого ещё не сделано, перейдите на официальный сайт и скачайте среду Go для своей программной платформы (поддерживаются Linux, Windows и MacOS).
Если у вас нет совместимого браузера, то нужно скачать Google Chrome, Chromium или headless-shell (поставляется виде Docker-контейнера, поэтому обязательно нужно будет ещё и установить среду Docker).
Запустите командную строку окружения (например, терминал в Windows), можно без root-прав или без прав администратора.
Если вы хотите запустить проект в определённом каталоге, что сначала нужно создать папку и затем перейти в неё в терминале (команды переключения и создания каталогов могут отличаться в разных платформах).
В Windows для этого нужно вести (каталог будет создан в корне диска C, то есть в корне системного диска):
mkdir \My-project
cd \My-project
Теперь можно создать и инициировать свой первый модуль в Go:
go mod init My-scraper
Добавьте к своему модулю библиотеку chromedp. Это делается командой:
go get -u github.com/chromedp/chromedp
Дождитесь скачивания и установки библиотеки. Готово.
В каталоге с вашим проектом появится файл go.mod.
Если вам нужно больше примеров кода с использованием библиотеки, то выполните команду:
go get -u -d github.com/chromedp/examples
В том же каталоге создайте новый текстовый файл и переименуйте его, например, в my-parser.go.
Откройте файл в блокноте или в специальном редакторе кода и добавьте следующее содержимое:
package main
// импортируем дополнительные модули
// в нашем случае это штатные библиотеки https://pkg.go.dev/context и https://pkg.go.dev/fmt, time, а также внешняя chromedp с дополнением для чтения DOM-структуры
import (
"context"
"fmt"
"github.com/chromedp/cdproto/dom"
"github.com/chromedp/chromedp"
"log"
"time"
)
// создаём главную функцию своей программы
func main() {
// инициализируем создание нового экземпляра headless-браузера через контекст
ctx, cancel := chromedp.NewContext(
context.Background(),
)
// если браузер нам больше не нужен,
// то его инстанс выгружается, освобождая ресурсы
defer cancel()
//создаём строковую переменную yourhtml, в которую будет помещаться HTML-код страницы при парсинге
var yourhtml string
// если нет ошибки, то исполняется следующий код
err := chromedp.Run(ctx,
// указываем целевую страницу, у нас это специальные тестовый сайт с бесконечной прокруткой контента, спасибо scrapingclub
chromedp.Navigate("https://scrapingclub.com/exercise/list_infinite_scroll/"),
// добавляем паузу специально для ожидания полной прогрузки контента
chromedp.Sleep(2000*time.Millisecond),
// выгружаем HTML-код в строку, параллельно собираем и обрабатываем ошибки, помещая их в переменную err
chromedp.ActionFunc(func(ctx context.Context) error {
// для этого находим корневой элемент DOM-структуры
rootNode, err := dom.GetDocument().Do(ctx)
// если содержимое переменной err не равно нулю, то возвращаем её содержимое
if err != nil {
return err
}
// наполняем переменную yourhtml содержимым
yourhtml, err = dom.GetOuterHTML().WithNodeID(rootNode.NodeID).Do(ctx)
return err
}),
)
if err != nil {
log.Fatal("Фатальная ошибка в обработке логики парсинга:", err)
}
// Выводим в терминале содержимое переменной
fmt.Println(yourhtml)
}
Теперь осталось сохранить файл и запустить его. Для исполнения скрипта в консоли набираем команду:
go run my-parser.go
После небольшого ожидания в консоли должен отобразиться HTML-код страницы. Если что-то пойдёт не так, то отобразится ошибка.
Давайте немного усложним задачу. Нам не нужен весь код страницы, его можно посмотреть в любом браузере. Попробуем собрать со страницы только названия и цены продуктов.
Для этой задачи потребуется:
Скопируйте и вставьте код в файл my-parser.go (заменив предыдущий код):
package main
// Импортируем модули
import (
"context"
"fmt"
"github.com/chromedp/cdproto/cdp"
"github.com/chromedp/chromedp"
"log"
)
// Наша структура данных о продукте, которую будем использовать для хранения уже после извлечения (переменные name и price)
type Product struct {
name, price string
}
// создаём главную функцию своей программы
func main() {
// чтобы отслеживать все данные, создадим переменную products и массив []Product
var products []Product
// Инициализируем экземпляр headless-хрома
ctx, cancel := chromedp.NewContext(
context.Background(),
)
// закрываем его, если не используем
defer cancel()
// Тут логика автоматизации браузера
var productNodes []*cdp.Node
err := chromedp.Run(ctx,
chromedp.Navigate("https://scrapingclub.com/exercise/list_infinite_scroll/"), //целевая страница
chromedp.Nodes(".post", &productNodes, chromedp.ByQueryAll), //ищем все карточки товара, они имеют класс post
)
//заодно наполняем ошибки
if err != nil {
log.Fatal("Error:", err)
}
// логика парсинга
var name, price string
for _, node := range productNodes {
// достаём данные из HTML карточки продукта
err = chromedp.Run(ctx,
// собираем текст из тега H4, наше название
chromedp.Text("h4", &name, chromedp.ByQuery, chromedp.FromNode(node)),
// собираем текст из тега H5, наша цена
chromedp.Text("h5", &price, chromedp.ByQuery, chromedp.FromNode(node)),
)
//тут вывод ошибок, если есть
if err != nil {
log.Fatal("Ошибка:", err)
}
// запускаем новый процесс, но уже для структурирования данных парсинга
product := Product{}
product.name = name
product.price = price
products = append(products, product)
}
//Выводим массив продуктов
fmt.Println(products)
}
Так как мы не задали задержку, браузер отобразит только те карточки товаров, которые не потребовали подгрузки, то есть первые 10 штук.
Тут мы подходим к тому, что нужно либо подгружать контент страницы, если загрузка осуществляется бесконечно, либо использовать встроенный механизм пагинации.
В классических скриптах парсинга (для статичных страниц) собирается список всех URL-адресов на странице, затем они заносятся в специальную таблицу, проверяются на уникальность (на наличие копий, чтобы не обходить одни и те же страницы по второму разу) и ставятся в очередь на новые процедуры парсинга.
В библиотеке chromedp можно задействовать механизмы эмуляции поведения пользователя:
Что примечательно, chromedp позволяет использовать JavaScript для описания логики работы со страницей.
Но вернёмся к нашему примеру. Чтобы получить данные всех товаров на странице, ленту нужно прокрутить до конца не меньше 8-10 раз (в зависимости от высоты экрана). Так как страница не сразу показывает новые товары, логично добавить время задержки.
Логику скроллинга напишем на JS, добавим скрипт к переменной и уже саму переменную задействуем в языке Go.
Вот так будет выглядеть скроллинг на JS:
// прокручиваем страницу 10 раз
const numberScrolls = 10
let countScroll = 0
// После каждой итерации скроллинга добавляем паузу в 1 секунду (1000 миллисекунд)
const interval = setInterval(() => {
window.scrollTo(0, document.body.scrollHeight)
countScroll++
if (countScroll === numScrolls) {
clearInterval(interval)
}
}, 1000)
А вот так будет выглядеть полный код на Go:
package main
// Импортируем модули, обратите внимание: к предыдущим добавили модуль time
import (
"context"
"fmt"
"github.com/chromedp/cdproto/cdp"
"github.com/chromedp/chromedp"
"log"
"time"
)
// Наша структура данных о продукте, которую будем использовать для хранения уже после извлечения (переменные name и price)
type Product struct {
name, price string
}
// Создаём главную функцию своей программы
func main() {
// чтобы отслеживать все данные, создадим переменную products и массив []Product
var products []Product
// Инициализируем экземпляр headless-хрома
ctx, cancel := chromedp.NewContext(
context.Background(),
)
// закрываем его, если не используем
defer cancel()
// Описываем логику скроллинга на JavaScript, не забудьте одинарные кавычки (открывающие и закрывающие)
scriptForScrolling := `
// прокручиваем страницу 10 раз
const numberScrolls = 10
let countScroll = 0
// После каждой итерации скроллинга добавляем паузу в 1 секунду (1000 миллисекунд)
const interval = setInterval(() => {
window.scrollTo(0, document.body.scrollHeight)
countScroll++
if (countScroll === numberScrolls) {
clearInterval(interval)
}
}, 1000)
`
// логика автоматизации браузера - добавили вызов JS-скрипта и общую задержку 10 секунд
var productNodes []*cdp.Node
err := chromedp.Run(ctx,
chromedp.Navigate("https://scrapingclub.com/exercise/list_infinite_scroll/"), //целевая страница
chromedp.Evaluate(scriptForScrolling, nil), //вызов скрипта скроллинга
chromedp.Sleep(10000*time.Millisecond), //задержка в 10 секунд
chromedp.Nodes(".post", &productNodes, chromedp.ByQueryAll), //ищем все карточки товаров с классом post
)
//заодно наполняем ошибки
if err != nil {
log.Fatal("Error:", err)
}
// логика парсинга
var name, price string
for _, node := range productNodes {
// достаём данные из HTML карточки продукта
err = chromedp.Run(ctx,
// собираем текст из тега H4, наше название
chromedp.Text("h4", &name, chromedp.ByQuery, chromedp.FromNode(node)),
// собираем текст из тега H5, наша цена
chromedp.Text("h5", &price, chromedp.ByQuery, chromedp.FromNode(node)),
)
//тут вывод ошибок, если есть
if err != nil {
log.Fatal("Ошибка:", err)
}
// запускаем новый процесс, но уже для структурирования данных парсинга
product := Product{}
product.name = name
product.price = price
products = append(products, product)
}
//Выводим массив продуктов
fmt.Println(products)
}
Запустите скрипт и дождитесь его завершения. В консоли выведутся все 60 тестовых товаров из массива.
Как можно было заметить, мы поставили жёсткий лимит ожидания по времени. Но это не самый рациональный подход, так как на парсинг расходуется слишком много времени. Лучшего всего в цикле добавить событие видимости определённого элемента. Такая возможность у chromedp есть. Она вызывается функцией WaitVisible().
В нашем случае, в go-скрипте строку
chromedp.Sleep(10000*time.Millisecond),
нужно заменить на строку
chromedp.WaitVisible(".post:nth-child(60)"),
Обязательно уберите импорт модуля time, он в нашем скрипте больше не нужен.
В новом коде мы ждём загрузки шестидесятой по счёту карточки с классом «post».
Для управления логикой парсинга можно использовать и другие события:
Например, код
chromedp.Click(`.post a`, chromedp.ByQuery),
будет выполнять клик на изображении в карточке товара (`.post a` – это комбинация из CSS-класса карточки и тега <a>, в котором вставлена ссылка и изображение).
Как уже и упоминалось выше, chromedp умеет эмулировать поведение пользователей, а также заполнять формы, скачивать и загружать файлы.
Самой передовой техникой парсинга с chromedp можно назвать работу через прокси. Официальная документация по прокси для chromedp.
Ниже разберёмся с прокси в примерах кода.
Вот так будет выглядеть скрипт, который создаёт новый контекст (экземпляр браузера) и пускает его через прокси-сервер. Работа скрипта завершается выводом текущего IP-адреса (для понимания – через прокси он работает или нет):
package main
// Импортируем модули и библиотеку chromedp
import (
"context"
"fmt"
"github.com/chromedp/chromedp"
)
// Создаём главную функцию своей программы
func main() {
//объявляем переменную для хранения адреса прокси и наполняем её данными локального хоста
//формат записи прокси может быть таким: протокол://логин:пароль@IP_ADDRESS:порт
//замените данные прокси на свои
var proxyAddress string = "https://127.0.0.1:80"
//запускаем новый экземпляр браузера
ctx, cancel := chromedp.NewContext(context.Background())
//если не используется, то выгружаем ресурсы
defer cancel()
// настраиваем опции прокси и активируем флаг браузера для работы в headless-режиме
opts := []chromedp.ExecAllocatorOption{
chromedp.ProxyServer(proxyAddress),
chromedp.Flag("headless", true),
}
// запускаем контекстный поток на основе опций
ctx, cancel = chromedp.NewExecAllocator(ctx, opts...)
defer cancel()
// Создаём новый выделенный контекст
ctx, cancel = chromedp.NewContext(ctx)
defer cancel()
// переходим на сайт и извлекаем нужную нам информацию
// в нашем случае это сайт httpbin.org, на котором можно посмотреть свой IP-адрес
var ip string
err := chromedp.Run(ctx,
chromedp.Navigate("https://httpbin.org/ip"),
chromedp.WaitVisible("body"),
chromedp.Text("body", &ip),
)
if err != nil {
fmt.Println("Ошибка:", err)
return
}
// Выводим полученный IP-адрес в консоли
fmt.Println("Ваш IP-адрес:", ip)
}
Если прокси-сервер будет недоступным, то скрипт отобразит ошибку коннекта (так точно произойдёт, если вы оставите IP-адрес локального хоста – 127.0.0.1).
Но единичный прокси не всегда удобен для работы парсера.
Вот так будет выглядеть скрипт, который умеет ротировать прокси из списка:
package main
// Импортируем модули и библиотеку chromedp
import (
"context"
"fmt"
"time"
"github.com/chromedp/chromedp"
)
// Создаём главную функцию своей программы
func main() {
// определяем список прокси-серверов для ротации
proxyAddresses := []string{
"https://123.123.123.123:80",
"https://124.124.124.124:80",
"https://125.125.125.125:80",
// вам нужно внести сюда реальные рабочие прокси в формате протокол://логин:пароль@IP_ADDRESS:порт
//количество прокси может быть любым
}
// создаём новый контекст с таймаутом 20 секунд
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
// ротируем прокси-адреса и парсим данные со страницы
for _, proxyAddress := range proxyAddresses {
// устанавливаем корректный прокси адрес
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", true), // активируем флаг работы в headless-режиме
chromedp.ProxyServer(proxyAddress), // а тут определяем прокси
)
// создаём аллокатор для текущих опций прокси
allocCtx, cancel := chromedp.NewExecAllocator(ctx, opts...)
defer cancel()
// создаём новый экземпляр контекста
ctx, cancel := chromedp.NewContext(allocCtx)
// освобождаем ресурсы, если не используем
defer cancel()
// переходим на сайт и парсим данные
var ip string
err := chromedp.Run(ctx,
chromedp.Navigate("https://httpbin.org/ip"), //страница показывает текущий IP-адрес клиента
chromedp.WaitVisible("body"),
chromedp.Text("body", &ip), //копируем содержимое с IP-адресом
)
if err != nil {
fmt.Println("Error:", err)
// перемещаемся к следующему прокси в списке
continue
}
// Выводим IP-адрес, который спарсили на странице
// каждый новый цикл будет выводить новый адрес или новую ошибку коннекта
fmt.Println("IP Address:", ip)
}
}
Технически вы уже работаете через актуальную версию браузера Chrome (если он установлен в системе). Но это не всегда соответствует задачам парсинга.
Чтобы эмулировать работу через разные типы браузеров и устройств, в первую очередь нужно изменить строку юзер-агента.
За это отвечает вызов функции UserAgent().
Вот так может выглядеть пример опций настройки экземпляра браузера:
// тут сконцентрированы все настройки прокси-сервера
options := []chromedp.ExecAllocatorOption{
chromedp.DefaultExecAllocatorOptions[:],
chromedp.ProxyServer("123.123.123.123:80"), // адрес и порт прокси-сервера
chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"), //в этой строке указывается нужный user-агент
// дальше могут идти другие опции...
}
За чтение кук отвечает функция GetCookies(). Ниже пример кода, который считывает имеющиеся куки и выводит их:
chromedp.ActionFunc(func(ctx context.Context) error {
cookies, err := storage.GetCookies().Do(ctx) // здесь считываются все куки
//тут реализуется обработка ошибок
if err != nil {
return err
}
// Цикл для перебора массива с куками и их вывод в консоли
for i, cookie := range cookies {
log.Printf("Cookie %d: %+v", i, cookie)
}
return nil
}),
Библиотека chromedp умеет не только читать, но и устанавливать куки. Можно догадаться, что за такую возможность отвечает функция SetCookie(). Пример кода:
//обращение к активному контексту
chromedp.ActionFunc(func(ctx context.Context) error {
// установка кук в формате «ключ:значение»
cookie := [2]int{"cookie_name", "cookie value"}
// установка срока действия для куки на основе текущего момента + 1 год
expirationTime := cdp.TimeSinceEpoch(time.Now().Add(365 * 24 * time.Hour))
err := network.SetCookie(cookie[i], cookies[i+1]).
WithExpires(&expirationTime).
WithDomain("<YOUR_DOMAIN>"). //тут нужно указать ваш домен
WithHTTPOnly(true).
Do(ctx)
if err != nil {
return err
}
return nil
}),
Большинство сайтов работает с отправкой данных из форм через POST-запросы. Вот так будет выглядеть скрипт, который отправляет POST-запрос, содержащий логин и пароль для авторизации на сайте:
err := chromedp.Run(ctx,
chromedp.Navigate("https://your-site.com/login"), // не забудьте заменить адрес страницы входа на актуальный
chromedp.WaitVisible(sel), // ждём, когда станет видимым нужный селектор, его нужно выбрать на своё усмотрение
chromedp.SendKeys(".login", "<YOUR_USERNAME>"), // тут указываем логин
chromedp.SendKeys(".password", "<YOUR_PASSWORD>"), // а тут пароль
chromedp.Submit("button[role=submit]"), // отправляем форму, вместо сабмита можно использовать функцию клика
)
Мы привели только самые важные шаги для обхода блокировок при парсинге. Но целевые сайты могут иметь свои системы защиты.
Чтобы парсить без банов, рекомендуем ознакомиться с нашим материалом «Лучшие практики для веб-скрапинга без блокировок».
Если коротко:
Ну и помните: chromedp и headless браузер Chrome/Chromium созданы друг для друга. С другими браузерами библиотека не работает (обязательно требуется поддержка Chrome DevTools Protocol).
Если вам нужна экономия ресурсов, то рассмотрите возможность работы через headless-shell (это готовый docker-контейнер с Хромиумом).
Библиотека chromedp реально облегчает процесс создания парсеров, работающих на базе связки Golang-скриптов с headless-хромом. Вместо того, чтобы каждый раз для новой задачи писать большой объём кода, можно вызывать готовые функции.
Охват возможностей chromedp впечатляет. Можно автоматизировать что угодно: от работы с куками и юзер-агентами до загрузки файлов и эмуляции поведения пользователей.
Но ни один масштабный парсер не может работать без качественных ротируемых прокси. Купить резидентные и мобильные прокси можно у Froxy. Это 10+ млн IP с таргетингом до города и провайдера связи. Подключение к вашему парсеру может выполняться буквально одной строкой (дальнейшая ротация и подбор будет настраиваться в личном кабинете). Тарифицируется не количество прокси, а потребляемый трафик.
Воспользуйтесь недорогим триал-пакетом для полноценного тестирования возможностей наших прокси.