Пройдите краулер-колли от начала до руководства

Go

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

В последнее время я обнаружил, что интересующих вопросов о Zhihu становится все меньше и меньше, поэтому я готов объединить другие технические вопросы и ответы по платформе, такие как segmentfault, stackoverflow и т. д.

Чтобы выполнить эту работу, она определенно неотделима от рептилий. Между прочим, я нашел время, чтобы изучить colly, краулер-фреймворк для Go.

Обзор

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

как научиться

Самым известным фреймворком для краулеров должен быть Python scrapy, который является первым фреймворком для краулеров, с которым сталкиваются многие люди, и я не исключение. Его документация очень полная, и компоненты расширения также очень богаты. Когда мы хотим спроектировать фреймворк сканера, мы часто обращаемся к его дизайну. До этого я видел несколько статей о том, что в Go есть похожие реализации scrapy.

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

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

Сегодня давайте начнем с официальной документации на данный момент! Текст начинается.

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

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

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

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

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

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

go get -u github.com/gocolly/colly

Одна строка команды, чтобы сделать это. Так легко!

быстрый старт

Давайте быстро попробуем использовать слово «колли» в приветственном регистре. Действуйте следующим образом:

Первый шаг — импорт колли.

import "github.com/gocolly/colly"

Второй шаг — создание коллектора.

c := colly.NewCollector()

Третий шаг, мониторинг событий, выполняет обработку событий посредством обратного вызова.

// Find and visit all links
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
	link := e.Attr("href")
	// Print link
	fmt.Printf("Link found: %q -> %s\n", e.Text, link)
	// Visit link found on page
	// Only those links are visited which are in AllowedDomains
	c.Visit(e.Request.AbsoluteURL(link))
})

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

Кстати, давайте перечислим типы событий, поддерживаемые Colly, следующим образом:

  • OnRequest вызывается перед выполнением запроса
  • OnResponse вызывается после возврата ответа
  • Селектор выполнения монитора OnHTML
  • Селектор выполнения монитора OnXML
  • OnHTMLDetach, отменить прослушивание, параметр представляет собой строку выбора
  • OnXMLDetach, отменить прослушивание, параметр представляет собой строку выбора
  • OnScraped, выполняется после очистки, выполняется после завершения всей работы
  • OnError, обратный вызов ошибки

Последний шаг, c.Visit(), официально запускает посещение веб-страницы.

c.Visit("http://go-colly.org/")

Код завершения кейса находится в каталоге _example исходного кода Colly.basicпредоставлено в.

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

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

Ниже приведен сборщик, созданный по умолчанию.

c := colly.NewCollector()

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

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

Мы также можем создать, а затем изменить конфигурацию.

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

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

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{"segmentfault.com", "zhihu.com"}
CACHE_DIR (string) 缓存目录
DETECT_CHARSET (y/n) 是否检测响应编码
DISABLE_COOKIES (y/n) 禁止 cookies
DISALLOWED_DOMAINS (字符串切片),禁止的域名,同 ALLOWED_DOMAINS 类型
IGNORE_ROBOTSTXT (y/n) 是否忽略 ROBOTS 协议
MAX_BODY_SIZE (int) 响应最大
MAX_DEPTH (int - 0 means infinite) 访问深度
PARSE_HTTP_ERROR_RESPONSE (y/n) 解析 HTTP 响应错误
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,  // TLS 握手超时
	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)
	}
}

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

распределенный

Распределенные сканеры можно рассматривать с нескольких уровней, а именно уровня агента, уровня выполнения и уровня хранилища.

Уровень агентства

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

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

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, реализующая переключение прокси путем опроса. Конечно, мы также можем полностью настроить.

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

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-интерфейсы мониторинга для получения данных о производительности узла, чтобы помочь планировщику принимать решения.

уровень хранения

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

Мы можем реализовать обмен данными между узлами, сохраняя данные в Redis, Mongo и других хранилищах. colly поддерживает переключение между любыми хранилищами, если реализовано соответствующее хранилищеcolly/storage.Storageметоды в интерфейсе.

На самом деле, Colly имеет встроенные реализации хранения, см.storage. Эта тема также будет рассмотрена в следующем разделе.

место хранения

Я только что упоминал эту тему ранее, давайте посмотрим на хранилище, которое уже поддерживает Colly.

InMemoryStorage, то есть память, хранилище колли по умолчанию, мы можем заменить его на collect.SetStorage() .

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

разноеSqlite3StorageиMongoStorage.

Несколько коллекторов

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

Как понять эту фразу? Возьмите пример.

Если вы писали поисковые роботы какое-то время, вы, должно быть, сталкивались с проблемой сканирования родительских и дочерних страниц.Обычно логика обработки родительских страниц отличается от логики дочерних страниц, и обычно требуется обмен данными между родительской и дочерней страницами. дочерние страницы. Если вы использовали scrapy, то должны знать, что 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 дочерней страницы копирует конфигурацию родительского сборщика через clone.

Передача данных между родительской и дочерней страницами может передаваться между разными сборщиками через 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 по умолчанию оптимизирована для небольшого количества сайтов. Если вы сканируете большое количество сайтов, необходимо некоторое улучшение.

постоянное хранение

По умолчанию куки и урлы в колли хранятся в памяти, приходится переключаться на постоянное хранилище. Как упоминалось ранее, в 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, если мы хотим реализовать расширение, нам нужно заранее понять множество концепций и внимательно прочитать его документацию. Но у Колли вообще нет соответствующих инструкций в документации. Что я делаю? Похоже, вы можете смотреть только исходный код.

Мы открываем исходный код плагина реферера следующим образом:

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, вы обнаружите, что, хотя документация очень проста, содержание, которое следует представить, в основном сложное. Если есть какой-то контент, который не охвачен, я также сделал соответствующие дополнения в этой статье. ранее использовал GoelasticЧто касается пакетов, то документация тоже скудная, но просто прочитав исходный код, можно сразу понять, как им пользоваться.

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

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


波罗学的微信公众号