Веб-скрейпинг на Go с goquery
Что такое веб-скрейпинг
Веб-скрейпинг, или извлечение данных с веб-страниц, — это автоматический сбор информации с сайтов. Скрепер отправляет HTTP-запрос, получает HTML и не отображает его пользователю, а обрабатывает по заданным правилам и сохраняет результат в структуру данных или базу.
Краткое определение: веб-скрейпинг — автоматическое извлечение структурированных данных из веб-страниц.
Важно: не все сайты разрешают скрейпинг. Перед запуском проверьте robots.txt, условия использования и законы о защите данных.
Когда скрейпинг удобен и когда он не работает
- Работает хорошо: сайты без API, публичные каталоги, страницы с предсказуемой HTML-структурой.
- Падает: сайты, генерируемые исключительно JavaScript (SPA) без серверной отдачи HTML; страницы с антибот-защитой (CAPTCHA, детекторы поведения); сайты с частыми изменениями верстки.
Альтернатива: если есть официальное API — используйте его. Для динамических сайтов используйте браузерный контроллер (ChromeDP) или сервисы рендеринга.
Инструменты для скрейпинга на Go
- goquery — парсер, вдохновлённый jQuery, работает с net/html и Cascadia (CSS-селекторы).
- Colly — специализированная библиотека для скрейпинга с удобным API и очередями.
- ChromeDP — управление браузером через DevTools Protocol; полезен для страниц, требующих рендеринга JavaScript.
Выбор: goquery хорош для быстрого парсинга HTML; Colly добавляет удобства и управления очередями; ChromeDP подходит для сложного JS.
Установка goquery
В терминале выполните:
go get github.com/PuerkitoBio/goqueryЕсли возникают ошибки — обновите версию Go и проверьте настройки GOPATH / модулей.
Общий процесс скрейпинга
- Создать HTTP-запрос к странице.
- Получить и распарсить HTML.
- Найти нужные элементы с помощью CSS-селекторов.
- Извлечь текст/атрибуты и сохранить в структуру или базу.
- Обрабатывать ошибки, ждать, кэшировать и соблюдать правила сайта.
Делать HTTP-запросы правильно
Стандартный пакет для запросов — net/http. Всегда задавайте таймауты, закрывайте response.Body и устанавливайте корректный User-Agent.
Пример простого запроса (исходный пример, сохранённый):
package main
import "net/http"
import "log"
import "fmt"
func main() {
webUrl := "https://news.ycombinator.com/"
response, err:= http.Get(webUrl)
if err != nil {
log.Fatalln(err)
} else if response.StatusCode == 200 {
fmt.Println("We can scrape this")
} else {
log.Fatalln("Do not scrape this")
}
}Совет: используйте http.Client с таймаутом и заголовками:
client := &http.Client{Timeout: 15 * time.Second}
req, _ := http.NewRequest("GET", webUrl, nil)
req.Header.Set("User-Agent", "MyGoScraper/1.0 (+https://example.com)")
resp, err := client.Do(req)
if err != nil {
// обработка
}
defer resp.Body.Close()Получение и парсинг HTML с goquery
Создание документа из ответа
Пример создания документа (сохранённый):
document, err := goquery.NewDocumentFromReader(response.Body)
if err != nil {
log.Fatalln(err)
}После этого document содержит дерево DOM, с которым можно работать через CSS-селекторы.
Выбор элементов по селекторам
Нужно посмотреть структуру страницы и составить селектор. Пример из исходного материала:
Пример поиска одного элемента:
document.Find("tr.athing")Примечание: Find возвращает набор элементов; методы, которые применяются к набору, работают для первого совпадения или для всех в зависимости от используемой функции.
Обработка множества элементов
Чтобы пройтись по всем совпадениям, используйте Each:
document.Find("tr.athing").Each(func(index int, selector *goquery.Selection) {
/* Process selector here */
})Пример извлечения заголовков и ссылок (сохранённый):
document.Find("tr.athing").Each(func(index int, selector *goquery.Selection) {
title := selector.Find("td.title").Text()
link, found := selector.Find("a.titlelink").Attr("href")
})Метод Text() возвращает текст, Attr возвращает значение атрибута и флаг наличия.
Сохранение данных: структуры и базы
Простой способ — сохранить результат в слайс структур, а затем записать в базу.
Пример структуры (исходный):
type Information struct {
link string
title string
}
info := make([]Information, 0)Рекомендация: экспортируйте поля и добавьте json-теги, если планируете сериализацию:
type Information struct {
Title string `json:"title"`
Link string `json:"link"`
}
var info []Information
// Внутри Each:
info = append(info, Information{
Title: title,
Link: link,
})Печать слайса покажет результат:
fmt.Println(info)Практические советы и лучшие практики
- Уважайте robots.txt и условия сайта. Если сайт запрещает автоматический сбор, лучше найти легальный способ доступа.
- Ограничивайте частоту запросов (rate limiting) и используйте backoff при ошибках.
- Кэшируйте результаты локально, чтобы уменьшить нагрузку на целевой сервер.
- Обрабатывайте редиректы и коды ошибок (429, 503 и т.д.).
- Используйте заголовки (User-Agent, Referer, Accept-Language) разумно.
- Закрывайте response.Body с defer сразу после проверки ошибки.
- Не храните секреты в коде и не публикуйте свои прокси/ключи.
Критическая запись: всегда тестируйте скрейпер на тестовой среде или с ограниченной частотой, прежде чем запускать в прод.
Анти-скрейпинг: как его обнаруживают и как с этим быть
Анти-скрейпинг меры включают:
- CAPTCHA и интерактивные проверки.
- Анализ поведенческих паттернов (скорость кликов, последовательность запросов).
- Блокировка по IP или ограничение по сессиям.
Когда скрейпинг «ломается»: если HTML меняется динамически, селекторы становятся невалидны. Решения:
- Переход на ChromeDP (рендеринг JS).
- Работа с API или договор с владельцем данных.
- Добавление мониторинга изменений страницы и тестов селекторов.
Примеры альтернативных подходов
- Коллекторы и очереди: Colly для управления параллелизмом и очередями задач.
- Браузерный рендеринг: ChromeDP для SPA.
- Серверы рендеринга/прокси: внешние сервисы, которые возвращают уже отрендеренный HTML.
Мини-методология для проекта скрейпинга
- Определите цель и поля данных.
- Проверьте легальность и robots.txt.
- Найдите стабильные CSS-селекторы.
- Напишите прототип с goquery и локальным кэшем.
- Добавьте таймауты, ретраи и логирование.
- Тестируйте на изменениях верстки.
- Переходите к масштабированию и хранению в БД.
Чек-лист по ролям
Разработчик
- Написал клиент с таймаутами и заголовками.
- Закрыл response.Body.
- Добавил retry и backoff.
DevOps
- Настроил прокси/балансировщик при необходимости.
- Добавил мониторинг быстрых ошибок и задержек.
QA
- Описал тесты селекторов и проверки кейсов с отсутствующими полями.
- Проверил корректную обработку кодов 4xx/5xx.
Продукт
- Подтвердил законность и соответствие политике конфиденциальности.
- Определил SLA для обновления данных.
Критерии приёмки
- Скрейпер корректно извлекает поля, указанные в ТЗ, в 95% тестовых страниц (без изменения верстки).
- Скрейпер корректно обрабатывает недоступность страницы и не падает.
- Запросы идут с заданной частотой и не нарушают robots.txt.
Тестовые случаи и приемочные тесты
- Позитивный: страница доступна, все селекторы возвращают значения.
- Негативный: страница возвращает 404 — скрейпер логирует и продолжает.
- Негативный: атрибут отсутствует — скрейпер пропускает элемент без паники.
Пример продвинутого кода: клиент с таймаутом и парсером
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/PuerkitoBio/goquery"
)
func fetch(url string) (*goquery.Document, error) {
client := &http.Client{Timeout: 15 * time.Second}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "MyGoScraper/1.0 (+https://example.com)")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("bad status: %d", resp.StatusCode)
}
doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
return nil, err
}
return doc, nil
}
func main() {
doc, err := fetch("https://news.ycombinator.com/")
if err != nil {
log.Fatal(err)
}
doc.Find("tr.athing").Each(func(i int, s *goquery.Selection) {
title := s.Find("td.title").Text()
link, _ := s.Find("a.titlelink").Attr("href")
fmt.Println(i, title, link)
})
}Decision flowchart для выбора инструмента
flowchart TD
A[Нужен скрейпинг] --> B{Страница статическая?}
B -- Да --> C[goquery]
B -- Частично --> D[ChromeDP]
B -- Нет --> D
C --> E{Нужен масштаб?}
E -- Да --> F[Colly или очередь]
E -- Нет --> G[Прототип и кэш]Безопасность и приватность
- Не собирайте персональные данные без согласия.
- Храните данные в зашифрованном виде, если это требуется политикой безопасности.
- Логируйте только то, что нужно для отладки; избегайте логирования чувствительных заголовков.
Совместимость и миграция
- goquery совместим с Go-модулями. Следите за версиями зависимостей.
- Если сайт переходит на heavy JS, подумайте о миграции на ChromeDP.
Контроль качества и мониторинг
- Пишите тесты селекторов с фиктивными HTML-файлами.
- Настройте оповещения на рост числа ошибок 4xx/5xx.
Частые ошибки и как их избежать
- Забыл defer resp.Body.Close() — утечка дескрипторов.
- Нет таймаута у http.Client — висящие запросы.
- Жёстко захардкоженные селекторы без наблюдения за изменениями верстки.
Краткое резюме
- goquery — быстрый способ парсинга HTML в Go.
- Для динамических или защищённых сайтов используйте ChromeDP или API.
- Соблюдайте robots.txt, лимиtы и законы о данных.
- Структурируйте результат в экспортируемые структуры и храните в БД с кэшем.
Важно: начните с прототипа, добавьте логику повторных попыток и мониторинг перед масштабированием.
Ключевые выводы:
- Начинайте с проверки robots.txt и юрисдикции данных.
- Используйте http.Client с таймаутами и корректными заголовками.
- goquery удобен для статических страниц; для динамических — ChromeDP.
- Добавляйте кэширование, ретраи и мониторинг для стабильной работы.