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

Colly – это фреймворк для вебскрейпинга, реализованный на языке Go, который особенно подходит для высоконнагруженных и сложных сценариев сбора данных. К его основным достоинствам относятся легкость, скорость, элегантный дизайн, простота распространения и расширения.

Эта статья, основанная на официальной документации Colly, содержит руководство по изучению Colly.

Как научиться веб-скрейпингу на Go

Когда речь заходит о фреймворках для веб-скрейпинга, Scrapy от Python, вероятно, является самым известным.

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

Для сравнения, документаци Colly, к сожалению, не такая полная и понятная.

Поначалу я не мог не попытаться применить свой опыт работы со Scrapy к Colly, но обнаружил, что такой подход не работает.

Естественным следующим шагом был поиск статей для работы с библиоткой. Однако статьи по Colly встречаются довольно редко, а те, что есть, в основном из официальных источников и выглядят неполными. Решение? Погрузиться в официальные учебные материалы, которые обычно состоят по Colly, разобраться в них и структурировать информацию.

Давайте начнем с официальной документации.

Официальная документация

Официальную документацию можно найти здесь. Я потратил некоторое время на то, чтобы изучить ее в и понять.

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

Как установить Colly

Установка Colly так же проста, как и установка любой другой библиотеки Go. Просто запустите:

go get -u github.com/gocolly/colly

Одна команда – и готово. Так просто!

Начало работы

По традиции, давайте начнем работу с Colly на простом hello world примере .

Во-первых, импортируйте Colly.

import "github.com/gocolly/colly"

Во-вторых, создайте коллектор.

c := colly.NewCollector()

Главный объект Колли – Collector. Он позволяет выполнять HTTP-запросы и парсинг сайто

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

// Найти и пройтись по всем ссылкам
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
    link := e.Attr("href")
    // Выводим ссылки
    fmt.Printf("Ссылка найдена: %q -> %s\n", e.Text, link)
    // Заходимна ссылки, найденной на странице
    // Перходим только по тем ссылкам, которые находятся в разрешенных доменах
    c.Visit(e.Request.AbsoluteURL(link))
})

c.OnRequest(func(r *colly.Request) {
    fmt.Println("Посещение", r.URL)
})

Давайте также перечислим типы событий, которые поддерживает Colly:

  • OnRequest: Вызывается перед выполнением запроса
  • OnResponse: Вызывается после получения ответа
  • OnHTML: Вызывается после OnResponse(), если сервер вернул действительный HTML-документ.  
  • OnXML: Прослушивает и выполняет селектор
  • OnHTMLDetach, прекращает прослушивание, со строкой селектора в качестве параметра
  • OnXMLDetach, прекращает прослушивание, с указанием строки селектора в качестве параметра
  • OnScraped, выполняется после завершения скрапинга, когда вся работа выполнена
  • OnError, Вызывается при возникновении ошибки

Наконец, c.Visit() запускает посещение веб-страницы.

c.Visit("https://uproger.com/")

Полный код этого примера можно найти в каталоге basic в папке _examples в исходном коде Colly.

Как настроить

Colly – это гибкий фреймворк, предоставляющий разработчикам множество вариантов конфигурации. По умолчанию для каждого параметра установлено разумное значение по умолчанию.

Вот как можно создать коллектор с настройками по умолчанию:

c := colly.NewCollector()

А вот как настроить коллектор с UserAgent и разрешить повторные посещения сайта.

Код выглядит следующим образом:

c2 := colly.NewCollector(
    colly.UserAgent("xy"),
    colly.AllowURLRevisit(),
)

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

Вы также можете изменить конфигурацию после создания коллектора.

c2 := colly.NewCollector()
c2.UserAgent = "xy"
c2.AllowURLRevisit = true

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

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

func RandomString() string {
    b := make([]byte, rand.Intn(10)+10)
    for i := range b {
        b[i] = letterBytes[rand.Intn(len(letterBytes))]
    }
    return string(b)
}

c := colly.NewCollector()

c.OnRequest(func(r *colly.Request) {
    r.Headers.Set("User-Agent", RandomString())
})

Как уже говорилось, конфигурация коллектора по умолчанию уже оптимизирована, но ее можно изменить с помощью переменных окружения. Конфигурация переменных окружения вступает в силу при инициализации коллектора, но ее можно отменить после официального запуска коллектора.

Поддерживаются следующие варианты конфигурации:

ALLOWED_DOMAINS (string slice), allowed domains, e.g., []string{"segmentfault.com", "zhihu.com"}
CACHE_DIR (string), cache directory
DETECT_CHARSET (y/n), whether to detect response encoding
DISABLE_COOKIES (y/n), disable cookies
DISALLOWED_DOMAINS (string slice), prohibited domains, same type as ALLOWED_DOMAINS
IGNORE_ROBOTSTXT (y/n), whether to ignore ROBOTS protocol
MAX_BODY_SIZE (int), maximum response size
MAX_DEPTH (int - 0 means infinite), visit depth
PARSE_HTTP_ERROR_RESPONSE (y/n), parse HTTP response errors
USER_AGENT (string)

Все эти параметры очень просты.

Давайте посмотрим на конфигурацию HTTP. Это все обычные настройки, такие как прокси-серверы, различные таймауты и т. д.

c := colly.NewCollector()
c.WithTransport(&http.Transport{
    Proxy: http.ProxyFromEnvironment,
    DialContext: (&net.Dialer{
        Timeout:   

 30 * time.Second,          // Таймаут
        KeepAlive:  30 * time.Second,          // keepAlive таймаут
        DualStack:  true,
    }).DialContext,
    MaxIdleConns:          100,               
    IdleConnTimeout:       90 * time.Second,  
    TLSHandshakeTimeout:   10 * time.Second,  
    ExpectContinueTimeout: 1 * time.Second,  
}

Отладка

Scrapy предоставляет очень удобную оболочку, которая помогает нам в отладке. К сожалению, в Colly нет подобной возможности.

Под отладчиком здесь понимается в основном сбор информации о времени выполнения.

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

type Debugger interface {
    // Init initializes the backend
    Init() error
    // Event receives a new collector event.
    Event(e *Event)
}

В исходном коде есть хороший пример, LogDebugger. Нам нужно только предоставить соответствующую переменную типа io.Writer. Как мы ее используем?

Вот пример:

package main

import (
    "log"
    "os"

    "github.com/gocolly/colly"
    "github.com/gocolly/colly/debug"
)

func main() {
    writer, err := os.OpenFile("collector.log", os.O_RDWR|os.O_CREATE, 0666)
    if err != nil {
        panic(err)
    }

    c := colly.NewCollector(colly.Debugger(&debug.LogDebugger{Output: writer}), colly.MaxDepth(2))
    c.OnHTML("a[href]", func(e *colly.HTMLElement) {
        if err := e.Request.Visit(e.Attr("href")); err != nil {
            log.Printf("visit err: %v", err)
        }
    })

    if err := c.Visit("http://go-colly.org/"); err != nil {
        panic(err)
    }
}

После запуска откройте файл collector.log, чтобы увидеть содержимое выходных данных.

Прокси-сервера

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

Код для реализации прокси в Colly выглядит следующим образом:

package main

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

func main() {
    c := colly.NewCollector()

    if p, err := proxy.RoundRobinProxySwitcher(
        "socks5://127.0.0.1:1337",
        "socks5://127.0.0.1:1338",
        "http://127.0.0.1:8080",
    ); err == nil {
        c.SetProxyFunc(p)
    }
    // ...
}

proxy.RoundRobinProxySwitcher – это встроенная в Colly функция, которая реализует переключение прокси.

Вот код с использованием randomProxySwitcher:

var proxies []*url.URL = []*url.URL{
    &url.URL{Host: "127.0.0.1:8080"},
    &url.URL{Host: "127.0.0.1:8081"},
}

func randomProxySwitcher(_ *http.Request) (*url.URL, error) {
    return proxies[random.Intn(len(proxies))], nil
}

// ...
c.SetProxyFunc(randomProxySwitcher)

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

Уровень исполнения

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

Среди распространенных коммуникационных протоколов – HTTP, TCP (один из них – текстовый протокол без статистики, другой – протокол, ориентированный на соединение). Кроме того, существует богатый выбор протоколов RPC, таких как Jsonrpc, Thrift от Facebook, gRPC от Google и т. д.

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

package main

import (
    "encoding/json"
    "log"
    "net/http"

    "github.com/gocolly/colly"
)

type pageInfo struct {
    StatusCode int
    Links      map[string]int
}

func handler(w http.ResponseWriter, r *http.Request) {
    URL := r.URL.Query().Get("url")
    if URL == "" {
        log.Println("missing URL argument")
        return
    }
    log.Println("visiting", URL)

    c := colly.NewCollector()

    p := &pageInfo{Links: make(map[string]int)}

    // count links
    c.OnHTML("a[href]", func(e *colly.HTMLElement) {
        link := e.Request.AbsoluteURL(e.Attr("href"))
        if link != "" {
            p.Links[link]++
        }
    })

    // extract status code
    c.OnResponse(func(r *colly.Response) {
        log.Println("response received", r.StatusCode)
        p.StatusCode = r.StatusCode
    })
    c.OnError(func(r *colly.Response, err error) {
        log.Println("error:", r.StatusCode, err)
        p.StatusCode = r.StatusCode
    })

    c.Visit(URL)

    // dump results
    b, err := json.Marshal(p)
    if err != nil {
        log.Println("failed to serialize response:", err)
        return
    }
    w.Header().Add("Content-Type", "application/json")
    w.Write(b)
}

func main() {
    // example usage: curl -s 'http://127.0.0.1:7171/?url=http://go-colly.org/'
    addr := ":7171"

    http.HandleFunc("/", handler)

    log.Println("listening on", addr)
    log.Fatal(http.ListenAndServe(addr, nil))
}

После выполнения задачи сервис возвращает соответствующую ссылку планировщику, который отвечает за отправку новых задач на рабочие узлы для выполнения.

Если необходимо принять решение об узле выполнения задачи, основываясь на нагрузке узла, необходимы дополнительные API мониторинга сервиса для получения данных о производительности узла, которые помогут планировщику принять решение.

Хранение данных

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

Colly поддерживает переключение между разнными хранилищами, если соответствующее хранилище реализует методы интерфейса colly/storage.Storage.

Также доступен RedisStorage. Возможно, потому, что Redis чаще всего используется в распределенных сценариях, на официальном сайте приведен пример использования.

import (
	"github.com/gocolly/colly"
	"github.com/gocolly/redisstorage"
)
c := colly.NewCollector()

storage := &redisstorage.Storage{
    Address:  "127.0.0.1:6379",
    Password: "",
    DB:       0,
    Prefix:   "job01",
}

err := c.SetStorage(storage)
if err != nil {
    panic(err)
}



Посмотрите другие варианты хранилища даннных включают Sqlite3Storage и MongoStorage.

Использование множества коллекторов

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

Давайте разберемся на примере.

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

Проведя исследование, мы обнаружили, что Colly не поддерживает метод Scrapy. Так что же нам делать? Это проблема, которую нам нужно решить.

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

c := colly.NewCollector(
    colly.UserAgent("myUserAgent"),
    colly.AllowedDomains("foo.com", "bar.com"),
)
// Custom User-Agent and allowed domains are cloned to c2
c2 := c.Clone()

Как правило, коллектор для дочерних страниц тот же, что и для родительских. В приведенном выше примере коллектор c2 для дочерних страниц клонирует конфигурацию родительского коллектора c.

Для передачи данных между родительскими и дочерними страницами мы можем использовать Context для передачи данных между различными коллекторами. Обратите внимание, что этот Context – всего лишь структура обмена данными, реализованная в Colly, а не Context из стандартной библиотеки Go.

c.OnResponse(func(r *colly.Response) {
    r.Ctx.Put("Custom-header", r.Headers.Get("Custom-Header"))
    c2.Request("GET", "https://foo.com/", nil, r.Ctx, nil)
})

Таким образом, мы можем получить данные, переданные от родителя, на дочерних страницах через r.Ctx. Для этого сценария вы можете обратиться к официальному примеру coursera_courses.

Оптимизация конфигурации

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

Постоянное хранилище

По умолчанию файлы cookie и URL-адреса в Colly сохраняются в памяти. Нам необходимо перейти на постоянное хранилище. Как упоминалось ранее, в Colly уже реализованы некоторые общие компоненты постоянного хранения.

Включите асинхронное выполнение, чтобы ускорить выполнение задачи

По умолчанию Colly блокирует и ждет завершения запроса, что приводит к увеличению числа ожидающих задач. Мы можем установить опцию Async коллектора в true, чтобы обрабатывать запросы асинхронно и избежать этой проблемы. Если вы используете этот метод, не забудьте добавить c.Wait(), иначе программа немедленно завершится.

Отключение или ограничение соединений KeepAlive

По умолчанию Colly включает KeepAlive, чтобы увеличить скорость поиска. Однако для этого требуются открытые дескрипторы файлов, а при длительном выполнении задач очень легко достичь максимального лимита дескрипторов.

Вот пример кода для отключения KeepAlive в HTTP:

c := colly.NewCollector()
c.WithTransport(&http.Transport{
    DisableKeepAlives: true,
})

Расширения

Colly предоставляет несколько расширений, в основном связанных с распространенными функциями веб-скрейпинга, такими как referer, random_user_agent, url_length_filter и т.д. Исходный код находится в разделе colly/extensions/.

Давайте разберемся, как их использовать, на примере:

import (
    "log"

    "github.com/gocolly/colly"
    "github.com/gocolly/colly/extensions"
)

func main() {
    c := colly.NewCollector()
    visited := false

    extensions.RandomUserAgent(c)
    extensions.Referrer(c)

    c.OnResponse(func(r *colly.Response) {
        log.Println(string(r.Body))
        if !visited {
            visited = true
            r.Request.Visit("/get?q=2")
        }
    })

    c.Visit("http://httpbin.org/get")
}

Вам просто нужно передать коллектор в функцию расширения. Это так просто.

Но можем ли мы сами реализовать расширение?

При использовании Scrapy, если вы хотите реализовать расширение, вам нужно понять довольно много концепций и внимательно прочитать документацию. Но Colly даже не упоминает об этом в документации. Так что же нам делать? Похоже, мы можем только заглянуть в исходный код.

Давайте откроем исходный код плагина referer:

package extensions

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

// Referer sets valid Referer HTTP header to requests.
// Warning: this extension works only if you use Request.Visit
// from callbacks instead of Collector.Visit.
func Referer(c *colly.Collector) {
    c.OnResponse(func(r *colly.Response) {
        r.Ctx.Put("_referer", r.Request.URL.String())
    })
    c.OnRequest(func(r *colly.Request) {
        if ref := r.Ctx.Get("_referer"); ref != "" {
            r.Headers.Set("Referer", ref)
        }
    })
}

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

Заключение

Ознакомившись с официальной документацией Colly, вы поймете, что, несмотря на рудиментарность документации, она охватывает все, что должно быть представлено. Если есть части, которые не были освещены, я дополнил их в этой статье. Ранее, при использовании пакета elastic в Go, я также обнаружил, что документация была скудной, но простое прочтение исходного кода прояснило, как его использовать.

Возможно, в этом и заключается простота Go.

Наконец, если у вас возникнут проблемы при использовании Colly, лучше всего обратиться к официальным примерам. Рекомендую потратить время на их прочтение.

+1
0
+1
0
+1
0
+1
0
+1
0

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *