Создание веб-скрапера Go шаг за шагом: Руководство для начинающих по Colly

Go позволяет разработчикам создавать сложные проекты с более простым синтаксисом, чем C, но с почти такой же эффективностью и контролем.

Именно из-за его простоты и эффективности мы решили добавить Golang в нашу серию руководств для начинающих по веб-скрейпингу и показать вам, как использовать его для извлечения данных в больших масштабах.

Почему для веб-скрейпинга стоит использовать именно Golang, а не Python или JavaScript?

Если вы следили за нашей серией статей, вы видели, насколько простым является веб-скрейпинг с помощью таких языков, как Python и JavaScript, так почему же вам стоит попробовать Go?

Есть три основные причины, по которым стоит предпочесть Go другим языкам:

  • Go — статически типизированный язык, что облегчает поиск ошибок без запуска программы. Интегрированные среды разработки (IDE) сразу же выделяют ошибки и даже показывают вам предложения по их исправлению.
  • IDE могут быть тем полезнее, чем лучше они понимают код, а поскольку в Go мы объявляем типы данных, в коде меньше двусмысленности. Таким образом, IDE может предоставить лучшие функции автозаполнения и предложения, чем в других языках.
  • В отличие от Python или JavaScript, Go — это компилируемый язык, который выводит машинный код напрямую, что делает его быстрее Python. В эксперименте, проведенном Арнешем Агравалом, Python (с использованием библиотеки Beautiful Soup) потребовалось почти 40 минут для перебора 2000 URL-адресов, тогда как Go (с использованием пакета Goquery) — менее 20 минут.

Таким образом, Go — отличный вариант, если вам нужно оптимизировать скорость скраппинга или если вы ищете статически типизированный язык для перехода на него.

Веб-скраппинг с помощью Go

В этом проекте мы будем скрапировать страницу категории обуви Jack and Jones, чтобы извлечь названия товаров, цены и URL-адреса, а затем экспортировать данные в JSON-файл с помощью библиотеки Go для веб-скрапинга Colly.

Внимание: Мы постараемся объяснить каждый шаг как можно подробнее, так что даже если у вас нет опыта работы с Go, вы сможете следовать за нами. Однако если вы все еще чувствуете себя потерянным во время чтения, вот отличное введение в Go, которое можно посмотреть заранее.

1. Настройка нашего проекта

Сначала нам нужно зайти на сайт https://go.dev/dl/ и загрузить нужную версию Go в зависимости от нашей операционной системы. В нашем случае мы выберем версию ARM64, поскольку мы используем MacBook Air M1.

Примечание: Вы также можете использовать Homebrew в MacOS или Chocolatey в Windows для установки Go.

После завершения загрузки следуйте инструкциям, чтобы установить его на свой компьютер. После установки создадим новый каталог с именем go-scraper и откроем его в VScode или предпочитаемой вами IDE.

Примечание: Откройте терминал и введите команду go version. Если все идет хорошо, то в лог будет записана версия, как показано здесь:

В этом учебнике мы будем использовать VScode для написания кода, поэтому мы также загрузим расширение Go VScode для лучшего опыта.

Далее откройте терминал и введите следующую команду для создания или инициализации проекта: go mod init go-scraper.

В Go «модуль — это коллекция пакетов Go, хранящихся в файловом дереве, корнем которого является файл go.mod». Команда выше — go mod init — сообщает Go, что указанный нами каталог (go-scraper) является корнем модуля.

Не выходя из терминала, создадим новый файл jack-scraper.go, используя: touch jack-scraper.go и — используя команду, найденную в документации Colly — go get -u github.com/gocolly/colly/... для установки Colly.

Все загруженные зависимости были добавлены в файл go.mod, и был создан новый файл go.sum — который может содержать хэши для нескольких версий модуля.

Теперь мы можем считать наше окружение установленным!

2. Отправка HTTP-запросов с помощью Colly

В верхней части нашего файла jack-scraper.go мы добавим имя нашего пакета, который в качестве условного обозначения назовем main:

package main

И затем импортируем зависимости в проект:

import (
   "github.com/gocolly/colly"
)

Примечание: Из-за типизированной природы Go, VScode сообщит нам об ошибке при импорте. По сути, это означает, что мы импортировали зависимость, но не используем ее. Это одно из преимуществ использования Go, поскольку нам не нужно запускать наш код, чтобы найти ошибки.

Особенностью Go является то, что нам нужно обеспечить начальную точку для запуска кода. Поэтому мы создадим функцию main, и вся наша логика будет находиться внутри нее. Пока что мы выведем «Функция работает».

func main() {
  
   fmt.Println("Function is working")
}

Если все прошло хорошо, она должна вернуть следующее:

Вы также могли заметить, что был импортирован новый пакет. Это потому, что Go может сказать, что мы пытаемся использовать функцию .Println() из пакета fmt, поэтому он автоматически импортировал ее. Удобно, правда?

Чтобы обработать наш запрос и справиться со всеми обратными вызовами, необходимыми для извлечения данных, Colly использует объект collector. Инициализация коллектора — это просто вызов метода .NewCollector() из Colly и передача доменов, которые мы хотим разрешить Colly посетить:

c := colly.NewCollector(
    colly.AllowedDomains("www.jackjones.com"),
)

Теперь на нашем новом экземпляре c мы можем вызвать метод .Visit(), чтобы заставить colly отправить наш запрос.

c.Visit("https://www.jackjones.com/nl/en/jj/shoes/"

Однако есть еще несколько вещей, которые мы должны добавить в наш код, прежде чем отправлять наш коллектор в мир. Давайте посмотрим, что происходит за кулисами.

Сначала мы создадим обратный вызов для вывода URL, на который переходит Colly — это станет более полезным при масштабировании нашего скрепера с одной страницы на несколько страниц.

c.OnRequest(func(r *colly.Request) {
    fmt.Println("Scraping:", r.URL)
})

А затем обратный вызов для вывода статуса запроса.

c.OnResponse(func(r *colly.Response) {
    fmt.Println("Status:", r.StatusCode)
})

Как мы уже говорили, объект collector отвечает за обратные вызовы, прикрепленные к заданию collector. Поэтому каждая из этих функций будет вызываться на различных этапах цикла выполнения задания collector.

В нашем примере функция .OnRequest() будет вызвана непосредственно перед тем, как коллектор сделает HTTP-запрос, а функция .OnRespond() будет вызвана после получения ответа.

На всякий случай давайте также создадим обработчик ошибок перед запуском скрепера.

c.OnError(func(r *colly.Response, err error) {
    fmt.Println("Request URL:", r.Request.URL, "failed with response:", r, "nError:", err)
})

Чтобы запустить наш код, откройте терминал и используйте команду go run jack-scraper.go.

Потрясающе, мы получили красивый код состояния 200 (успешно)! Мы готовы к фазе 2, извлечению.

3. Инспектирование целевого веб-сайта

Чтобы извлечь данные из HTML-файла, нам нужен не только доступ к сайту. Каждый сайт имеет свою собственную структуру, и мы должны понять ее, чтобы извлечь определенные элементы, не добавляя шума в наш набор данных.

Хорошей новостью является то, что мы можем быстро просмотреть структуру HTML сайта, щелкнув правой кнопкой мыши на странице и выбрав в меню пункт Inspect. Откроется инструмент Inspector, и мы сможем навести курсор на элементы, чтобы увидеть их положение в HTML и атрибуты.

Мы используем CSS-селекторы в веб-скрейпинге (классы, идентификаторы и т.д.), чтобы указать нашим скрейперам, где располагать элементы, которые мы хотим, чтобы они извлекли для нас. К счастью для нас, Colly основан на пакете Goquery, который предоставляет Colly синтаксис, подобный JQuery, для использования этих селекторов.

Давайте попробуем извлечь название продукта первого ботинка с помощью консоли DevTools для проверки.

4. Использование DevTools для тестирования наших CSS-селекторов

Если мы осмотрим элемент дальше, то увидим, что название продукта находится в теге <a> с классом «product-tile__name__link«, обернутом между тегами <header>.

В консоли нашего браузера воспользуемся методом document.querySelectorAll(), чтобы найти этот элемент.

document.querySelectorAll("a.product-tile__name__link.js-product-tile-link")

Да! Он возвращает 44 элемента, что полностью совпадает с количеством элементов на странице.

URL продукта находится внутри того же элемента, поэтому мы можем использовать тот же селектор для него. С другой стороны, после некоторого тестирования мы можем использовать «em.value__price» для выбора цены.

5. Перебор всех названий товаров на странице

Если вы читали некоторые из наших предыдущих статей, вы знаете, что мы любим тестирование. Поэтому, прежде чем извлекать все целевые элементы, мы сначала протестируем наш скрепер, извлекая все названия продуктов со страницы.

Для этого в Colly есть функция .OnHTML() для обработки HTML из ответа (есть также функция .OnXML(), которую можно использовать, если вы не уверены, будет ли ответ содержать HTML или XML).

c.OnHTML("a.product-tile__name__link.js-product-tile-link", func(h *colly.HTMLElement) {
    fmt.Println(h.Text)
})

Вот схема происходящего:

  • Мы передаем основной элемент, с которым хотим работать a.product-tile__name__link.js-product-tile-link в качестве первого аргумента функции OnHTML().
  • Второй аргумент — это функция, которая будет выполняться, когда Colly найдет указанный нами элемент.

Внутри этой функции мы сказали Colly, что делать с объектом h. В нашем случае это печать текста элемента.

И да, пока что все работает отлично. Однако вокруг нашего текста много пустого пространства, что вносит шум в данные.

Чтобы решить эту проблему, давайте станем еще более конкретными, добавив элемент <header> в качестве основного селектора, а затем будем искать текст с помощью функции .ChildText().

c.OnHTML("header.product-tile__name", func(h *colly.HTMLElement) {
    fmt.Println(h.ChildText("a.product-tile__name__link.js-product-tile-link"))
})

6. Извлечение всех элементов HTML

Теперь мы хорошо понимаем логику работы функции OnHTML(), поэтому давайте немного расширим ее и извлечем остальные данные.

Начнем с изменения главного селектора на тот, который содержит все нужные нам данные:

Затем мы спустимся вниз по иерархии, чтобы извлечь название, цену и URL каждого продукта:

Вот как это выглядит в коде:

c.OnHTML("div.product-tile__content-wrapper", func(h *colly.HTMLElement) {
    name := h.ChildText("a.product-tile__name__link.js-product-tile-link")
    price := h.ChildText("em.value__price")
    url := h.ChildAttr("a.product-tile__name__link.js-product-tile-link", "href")
    fmt.Println(name, price, url)
})

Для этой функции мы храним каждый элемент в переменной, чтобы было легче работать с ним в дальнейшем. Например, вывести все элементы в консоль.

Примечание: При использовании функции обратного вызова .ChildAttr() нам нужно передать селекторы элемента в качестве первого аргумента (a.product-tile__name__link.js-product-tile-link) и имя атрибута в качестве второго (href).

Мы получили красивый список элементов, выведенный на консоль, но не совсем подходящий формат для анализа. Давайте решим эту проблему!

7. Экспорт данных в файл JSON в Colly

Сохранить наши данные в JSON-файл будет полезнее, чем выводить все на терминал, и сделать это не так уж сложно, благодаря встроенным в Go модулям JSON.

Создание структуры

За пределами основной функции нам нужно создать структуру (struct), чтобы сгруппировать каждый набор данных (название, цена, URL) в один тип.

type products struct {
   Name  string
   Price string
   URL   string
}

Структуры позволяют объединять различные типы данных, поэтому мы должны определить тип данных для каждого элемента. Мы также можем определить поле JSON, если хотим, но это не обязательно, так как в некоторых случаях мы не знаем, что это будет.

type products struct {
   Name  string `json:"name"`
   Price string `json:"price"`
   URL   string `json:"url"`
}

СОВЕТ: для более чистого вида мы можем сгруппировать все поля с одинаковым типом в одной строке кода, например:

type products struct {
   Name, Price, URL string
}

Конечно, в нашем коде будет сообщение об ошибке «unused», потому что мы нигде не использовали products struct. Поэтому мы назначим каждый соскобленный элемент одному из наших полей в struct.

c.OnHTML("div.product-tile__content-wrapper", func(h *colly.HTMLElement) {
    products := products{
        Name:  h.ChildText("a.product-tile__name__link.js-product-tile-link"),
        Price: h.ChildText("em.value__price"),
        URL:   h.ChildAttr("a.product-tile__name__link.js-product-tile-link", "href"),
    }
    
    fmt.Println(products)
})

Если мы запустим наше приложение, наши данные будут сгруппированы, что позволит передать каждый элемент как отдельный «элемент» в пустой список, который мы позже превратим в JSON-файл.

Добавление элементов в срез

С нашей структурой, содержащей данные о продукте, мы отправим их в пустой Slice (вместо массива, как в других языках), чтобы создать список элементов, которые мы экспортируем в JSON-файл.

Чтобы собрать пустой срез, добавьте этот код после кода инициации коллектора:

var allProducts []products

Внутри функции .OnHTML(), вместо того, чтобы распечатать нашу структуру, давайте вместо этого добавим все элементы внутри товаров в Slice.

allProducts = append(allProducts, products)

Если мы сейчас распечатаем фрагмент, то вот результат:

Вы видите, что каждый набор информации о продукте находится внутри фигурных скобок ({…}), а весь Slice находится внутри скобок ([…]).

Запись фрагмента в файл JSON

Мы уже сделали все, что нужно. Отныне в Go есть очень простой в использовании модуль JSON, который сделает все за нас:

   //We pass the data to Marshal
   content, err := json.Marshal(allProducts)
   //We need to handle someone the potential error
   if err != nil {
       fmt.Println(err.Error())
   }
   //We write a new file passing the file name, data, and permission
   os.WriteFile("jack-shoes.json", content, 0644)
}

Сохраните его, и необходимые зависимости обновятся:

import (
   "encoding/json"
   "fmt"
   "os"
  
   "github.com/gocolly/colly"
)

Давайте запустим наш код и посмотрим, что он вернет:

Примечание: Когда вы открываете свой файл, все данные в нем расположены в одну строку. Чтобы отобразить документ в виде картинки выше, щелкните правой кнопкой мыши на окне и выберите пункт format document.

Мы также могли бы вывести длину allProducts после создания JSON-файла в целях тестирования:

fmt.Println(len(allProducts))

Но мы должны получить все, что хотим.

Учитывая это, одной страницы недостаточно для большинства проектов. На самом деле, почти во всех проектах, подобных этому, нам потребуется скрести несколько страниц, чтобы собрать как можно больше информации. К счастью, мы можем масштабировать наш проект с помощью всего нескольких строк кода.

8. Скраппинг нескольких страниц

Если мы прокрутим список товаров вниз, то увидим, что J&J использует нумерованную пагинацию на странице категории.

Мы могли бы попытаться выяснить, как они строят свои URL, и посмотреть, сможем ли мы имитировать это с помощью цикла, но у Colly есть более элегантное решение, похожее на то, как Scrapy осуществляет навигацию по пагинациям.

c.OnHTML("a.paging-controls__next.js-page-control", func(p *colly.HTMLElement) {
    nextPage := p.Request.AbsoluteURL(p.Attr("data-href"))
    c.Visit(nextPage)
})

В новой функции OnHTML() мы нацеливаемся на следующую кнопку в меню.

Во внутренней функции мы захватываем значение внутри data-href (которое содержит URL), сохраняем его в новой переменной (nextPage), а затем говорим нашему скраперу посетить страницу.

Запустив код сейчас, мы получим все данные о продукте с каждой страницы в пагинации.

Как вы можете видеть, наш фрагмент теперь содержит 102 товара.

9. Избегайте блокировки вашего скрепера Colly

При работе с веб-скраппером всегда нужно учитывать одну вещь: большинство сайтов не любят веб-скрапперы, потому что многие разработчики не заботятся о сайтах, с которых они извлекают данные.

Представьте себе, что вы хотите соскрести тысячи страниц с сайта. Каждый отправленный вами запрос отнимает ресурсы у реальных пользователей, создавая дополнительные расходы для владельца сайта и, возможно, ухудшая пользовательский опыт из-за медленного времени загрузки.

Вот почему мы всегда должны использовать лучшие практики веб-скрейпинга, чтобы не навредить нашим целевым сайтам.

Однако есть и другая сторона истории. Чтобы предотвратить доступ скреперов к сайту, все больше и больше сайтов внедряют системы защиты от скрепинга, предназначенные для выявления и блокирования скреперов.

Хотя скраппинг нескольких страниц не вызовет никаких вопросов — в большинстве случаев — скраппинг нескольких страниц определенно поставит под угрозу ваш IP и скраппера.

Чтобы избежать этих мер, нам придется создать функцию, которая меняет наш IP-адрес, иметь доступ к пулу IP-адресов для нашего скрипта, чтобы чередовать их, создать какой-то способ борьбы с CAPTCHA и обрабатывать страницы с javascript, которые становятся все более распространенными.

Или мы можем просто послать наш HTTP-запрос через сервер ScraperAPI и позволить им обрабатывать все автоматически:

1. Во-первых, нам нужно будет только создать бесплатный аккаунт ScraperAPI, чтобы использовать 5000 бесплатных кредитов API и получить доступ к нашему ключу API с приборной панели.
2. Для простоты мы удалим параметр colly.AllowedDomains("www.jackjones.com") из collector.
3. Добавим конечную точку ScraperAPI в нашу начальную функцию .Visit() следующим образом:

c.Visit("http://api.scraperapi.com?api_key={yourApiKey}&url=https://www.jackjones.com/nl/en/jj/shoes/")

4. И сделаем аналогичное изменение для того, как мы посещаем следующую страницу:

c.Visit("http://api.scraperapi.com?api_key={yourApiKey}&url=" + nextPage)

5. Чтобы все работало правильно и без ошибок, нам нужно изменить 10-секундный тайм-аут по умолчанию в Colly по крайней мере на 60 секунд, чтобы дать нашему скреперу достаточно времени для обработки любых заголовков, CAPTCHA и т.д. Мы воспользуемся примером кода из документации Colly, но изменим таймаут с 30 на 60:

c.WithTransport(&http.Transport{
    DialContext: (&net.Dialer{
        Timeout:   60 * time.Second,
        KeepAlive: 30 * time.Second,
        DualStack: true,
    }).DialContext,
    MaxIdleConns:          100,
    IdleConnTimeout:       90 * time.Second,
    TLSHandshakeTimeout:   10 * time.Second,
    ExpectContinueTimeout: 1 * time.Second,
})

Добавьте этот код после создания коллектора.

Запуск нашего кода вернет те же данные, что и раньше. С той разницей, что ScraperAPI будет вращать наш IP-адрес для каждого отправленного запроса, искать наилучшую комбинацию «прокси + заголовки» для обеспечения успешного запроса и обрабатывать любые другие сложности, с которыми может столкнуться наш скрепер.

Colly сохранит все данные в отформатированный JSON-файл, который мы сможем использовать в других приложениях или проектах.

С помощью всего лишь нескольких небольших изменений в коде вы можете скреативить любой сайт, который вам нужен, при условии, что информация находится в HTML-файле.

Однако мы также можем настроить конечную точку ScraperAPI на рендеринг содержимого JavaScript перед возвратом HTML-документа. Таким образом, если только содержимое не находится за событием (например, нажатием кнопки), вы сможете без проблем получить динамическое содержимое.

Подведение итогов: Полный код веб-скрапера Colly

Поздравляем, вы создали свой первый веб-скрапер Colly! Если вы следили за развитием событий, то вот как должна выглядеть ваша кодовая база:

package main
  
import (
   "encoding/json"
   "fmt"
   "net"
   "net/http"
   "os"
   "time"
  
   "github.com/gocolly/colly"
)
  
type products struct {
   Name  string `json:"name"`
   Price string `json:"price"`
   URL   string `json:"url"`
}
  
func main() {
   c := colly.NewCollector()
   c.WithTransport(&http.Transport{
       DialContext: (&net.Dialer{
           Timeout:   60 * time.Second,
           KeepAlive: 30 * time.Second,
           DualStack: true,
       }).DialContext,
       MaxIdleConns:          100,
       IdleConnTimeout:       90 * time.Second,
       TLSHandshakeTimeout:   10 * time.Second,
       ExpectContinueTimeout: 1 * time.Second,
   })
  
   var allProducts []products
  
   c.OnRequest(func(r *colly.Request) {
       fmt.Println("Scraping:", r.URL)
   })
  
   c.OnResponse(func(r *colly.Response) {
       fmt.Println("Status:", r.StatusCode)
   })
  
   c.OnHTML("div.product-tile__content-wrapper", func(h *colly.HTMLElement) {
       products := products{
           Name:  h.ChildText("a.product-tile__name__link.js-product-tile-link"),
           Price: h.ChildText("em.value__price"),
           URL:   h.ChildAttr("a.product-tile__name__link.js-product-tile-link", "href"),
       }
  
       allProducts = append(allProducts, products)
   })
  
   c.OnHTML("a.paging-controls__next.js-page-control", func(p *colly.HTMLElement) {
       nextPage := p.Request.AbsoluteURL(p.Attr("data-href"))
       c.Visit("http://api.scraperapi.com?api_key={yourApiKey}&url=" + nextPage)
   })
  
   c.OnError(func(r *colly.Response, err error) {
       fmt.Println("Request URL:", r.Request.URL, "failed with response:", r, "nError:", err)
   })
  
   c.Visit("http://api.scraperapi.com?api_key={yourApiKey}&url=https://www.jackjones.com/nl/en/jj/shoes/")
  
   content, err := json.Marshal(allProducts)
   if err != nil {
       fmt.Println(err.Error())
   }
   os.WriteFile("jack-shoes.json", content, 0644)
   fmt.Println("Total products: ", len(allProducts))
}

Веб-скрейпинг — один из самых мощных инструментов в вашем арсенале сбора данных. Однако помните, что каждый сайт устроен по-своему. Сосредоточьтесь на основах структуры сайта, и вы сможете решить любую проблему, которая встанет на вашем пути.

До следующего раза, счастливого скраппинга!

Оцените статью
devanswers.ru
Добавить комментарий