Go Proxy Scraping: http.Transport, аутентификация, ротация и параллелизм
В Go вся история с прокси сосредоточена в одном поле: Transport.Proxy. Настройте его правильно, и вы получите ротацию для каждого запроса, SOCKS5 и colly бесплатно. Настройте неправильно, и вы получите 'context canceled' в 2 часа ночи.
Go делает проксирование обманчиво простым: вся клиентская история сосредоточена в одном поле, http.Transport.Proxy. Настройте его правильно, и ротация для каждого запроса, SOCKS5 и интеграция с colly будут работать по одному и тому же принципу. Настройте неправильно, и вы столкнетесь с классическими ошибками Go прокси - context canceled, истощением пула соединений и переменной HTTP_PROXY, которую ваш пользовательский транспорт тихо игнорирует. Это руководство проведет вас по всему пути с исполняемым кодом.
Однострочник: http.Transport с URL прокси
Каждый запрос через прокси в Go проходит через Transport. Пакет net/http содержит помощник, http.ProxyURL, который закрепляет один статический прокси для всего клиента. Учетные данные вставляются прямо в URL userinfo - Go преобразует их в заголовок Proxy-Authorization за вас:
package main
import (
"fmt"
"net/http"
"net/url"
"time"
)
func main() {
proxyURL, _ := url.Parse("http://USER:PASS@gate.quantumproxies.io:8000")
client := &http.Client{
Timeout: 20 * time.Second,
Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
}
resp, err := client.Get("https://httpbin.org/ip")
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.Status) // body shows the exit IP, not yours
}
Всегда устанавливайте Client.Timeout. В Go по умолчанию нет тайм-аута на http.Client, поэтому один зависший выход заблокирует горутину навсегда - главная причина, по которой Go-скрапер кажется зависшим, это отсутствие тайм-аута, а не медленная цель.
Ротация прокси с помощью функции Proxy
Proxy не ограничивается фиксированным URL - это функция func(*http.Request) (*url.URL, error), которую Go вызывает один раз на каждый запрос. Это ваш крючок для ротации. Выберите случайную конечную точку из пула, и каждый запрос будет выходить через другой IP, без какой-либо логики управления списком в вашем цикле выборки:
import "math/rand"
pool := []string{
"http://USER:PASS@ip1.quantumproxies.io:8000",
"http://USER:PASS@ip2.quantumproxies.io:8000",
"http://USER:PASS@ip3.quantumproxies.io:8000",
}
transport := &http.Transport{
Proxy: func(r *http.Request) (*url.URL, error) {
return url.Parse(pool[rand.Intn(len(pool))])
},
MaxIdleConnsPerHost: 32, // reuse connections across the pool
}
Для большинства скрапинга вам не следует управлять пулом вообще. Направьте функцию Proxy на один ротационный резидентный шлюз, и шлюз предоставит вам новый IP на каждый запрос из более чем 90 миллионов адресов в более чем 200 странах - одна конечная точка, без списка, липкие сессии, когда поток требует одного выхода на несколько минут.

Аутентификация прокси в Go
Вставка USER:PASS@ в URL прокси - это чистый путь и работает как для ProxyURL, так и для пользовательской функции Proxy. Если вы предпочитаете не включать учетные данные в URL, установите заголовок на туннеле CONNECT самостоятельно через Transport.ProxyConnectHeader. Если ваш провайдер использует аутентификацию по белому списку IP, полностью откажитесь от учетных данных и авторизуйте IP вашего сервера в панели управления - QuantumProxies поддерживает оба варианта. Отказ в CONNECT с 407 или proxyconnect tcp почти всегда означает отсутствие или неправильные учетные данные, а не блокировку на стороне цели.
Переменная окружения HTTP_PROXY (и когда Go её игнорирует)
Go's http.DefaultTransport использует http.ProxyFromEnvironment, который читает HTTP_PROXY, HTTPS_PROXY и NO_PROXY (в соответствии с пакетом golang.org/x/net/http/httpproxy). Два поведения сбивают людей с толку. Во-первых, эти переменные принимают либо полный URL, либо голый host:port, где предполагается схема http. Во-вторых, запросы к localhost или адресу обратной петли всегда обходят прокси, возвращая nil URL. Большая ловушка - это обратное: как только вы создаете свой собственный &http.Transport{Proxy: ...}, вы заменяете поведение переменной окружения - ваша явная функция побеждает, и HTTP_PROXY игнорируется. Если вы хотите и то, и другое, оберните их самостоятельно.
SOCKS5 прокси в Go
Стандартная библиотека не имеет клиента SOCKS5, поэтому подключите golang.org/x/net/proxy и настраивайте через него. Подключите дайлер в Transport.DialContext, чтобы DNS разрешался на прокси, а не на вашем компьютере (та же проблема утечки имени хоста, которую решает socks5h в других стэках). Если вы взвешиваете две схемы, наша заметка о SOCKS5 vs HTTP прокси объясняет, когда каждая из них выигрывает:
go get golang.org/x/net/proxy
import (
"context"
"net"
"net/http"
"golang.org/x/net/proxy"
)
auth := &proxy.Auth{User: "USER", Password: "PASS"}
dialer, err := proxy.SOCKS5("tcp", "gate.quantumproxies.io:1080", auth, proxy.Direct)
if err != nil {
panic(err)
}
transport := &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.Dial(network, addr)
},
}
client := &http.Client{Transport: transport}
Прокси в colly
Если вы скрапите с colly, вы получаете ротацию, не касаясь транспорта. Пакет colly/proxy поставляется с RoundRobinProxySwitcher, который циклически меняет конечные точки для каждого запроса, а LimitRule управляет темпом. Это самый быстрый способ создать вежливый, ротационный скрапер в Go:
import (
"time"
"github.com/gocolly/colly/v2"
"github.com/gocolly/colly/v2/proxy"
)
c := colly.NewCollector(colly.Async(true))
rp, err := proxy.RoundRobinProxySwitcher(
"http://USER:PASS@ip1.quantumproxies.io:8000",
"http://USER:PASS@ip2.quantumproxies.io:8000",
)
if err != nil {
panic(err)
}
c.SetProxyFunc(rp)
c.Limit(&colly.LimitRule{
DomainGlob: "*",
Parallelism: 8,
RandomDelay: 2 * time.Second,
})
Параллелизм сделан правильно (и ловушка context-canceled)
Параллелизм Go - это причина, по которой команды выбирают его для скрапинга, а также источник ошибки context canceled. Она возникает, когда контекст запроса отменяется до того, как тело полностью прочитано - обычно это истекший тайм-аут для каждого запроса или cancel(), который сработал слишком рано из-за неправильно расположенного defer. Ограничьте ваш фан-аут семафором, дайте каждому запросу свой собственный контекст тайм-аута и читайте, а затем закрывайте тело до того, как сработает отмена:
sem := make(chan struct{}, 20) // cap concurrency
var wg sync.WaitGroup
for _, u := range urls {
wg.Add(1)
sem <- struct{}{}
go func(u string) {
defer wg.Done()
defer func() { <-sem }()
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", u, nil)
resp, err := client.Do(req)
if err != nil {
return // rotate/log; do not retry the same burned exit
}
io.Copy(io.Discard, resp.Body) // drain BEFORE cancel fires
resp.Body.Close()
}(u)
}
wg.Wait()
Еще две заметки для производства. Установите MaxIdleConnsPerHost достаточно высоким, чтобы большой пул переиспользовал соединения, а не истощал файловые дескрипторы - обычная причина EOF после нескольких сотен запросов. И вращайте выход при сбое, а не повторяйте попытку с тем же; мертвый резидентный IP не восстанавливается при следующей попытке. Для более широкого дизайна на миллионах страниц наше руководство по архитектуре крупномасштабного скрапинга охватывает очереди, дедупликацию и уровни прокси.
Переиспользуйте одного клиента, настройте транспорт
Создайте http.Client один раз и используйте его во всех горутинах - он безопасен для параллельного использования, и его пул соединений делает Go-скрапер быстрым. Новый клиент для каждого запроса выбрасывает keep-alive и вынуждает каждый раз выполнять новое TLS рукопожатие. Три поля Transport важны под нагрузкой: MaxIdleConns и MaxIdleConnsPerHost задают размер пула, а IdleConnTimeout завершает устаревшие соединения, чтобы ротационный пул прокси никогда не закреплял мертвый выход. Установите их явно - стандартные настройки библиотеки настроены для браузера, а не для скрапера, выполняющего тысячи запросов в минуту.

Когда стоит прекратить ручную настройку транспортов
Код выше достаточен для чистых целей. Как только сайт добавляет Cloudflare, TLS отпечатки или рендеринг JavaScript, чистое TLS рукопожатие Go не похоже на Chrome, и никакой прокси не исправит это несоответствие. В этот момент Scraper API, который использует настоящий отпечаток браузера, вращает IP и рендерит JS за вас, будет менее кода и более высокой успешностью, чем поддержание стека вручную. Если вы выбираете язык для нового скрапера, наш пост о лучших прокси для веб-скрапинга описывает компромиссы между стеками.
Перенесите рендеринг и ротацию на Scraper API
Часто задаваемые вопросы
Как установить прокси для Go HTTP клиента?
Создайте http.Transport с полем Proxy и передайте его в http.Client. Используйте http.ProxyURL(u) для одного статического прокси или func(*http.Request) (*url.URL, error), чтобы выбрать выход для каждого запроса. Вставьте учетные данные в URL как http://user:pass@host:port и всегда устанавливайте Client.Timeout.
Почему Go игнорирует мою переменную HTTP_PROXY?
Потому что вы создали пользовательский Transport с явной функцией Proxy, которая заменяет поведение по умолчанию ProxyFromEnvironment. Переменные окружения применяются только когда вы используете http.DefaultTransport или устанавливаете Proxy: http.ProxyFromEnvironment самостоятельно. Также учтите, что запросы на loopback и localhost всегда обходят прокси.
Что вызывает 'context canceled' с Go прокси?
Контекст запроса был отменен до того, как тело ответа было прочитано - обычно это истекший тайм-аут для каждого запроса или cancel(), который сработал слишком рано через defer. Дайте каждому запросу свой собственный контекст тайм-аута, слейте и закройте тело до того, как выполнится отложенная отмена, и не делитесь одним отменяемым контекстом между многими горутинами.
Поддерживает ли Go SOCKS5 прокси?
Не в стандартной библиотеке, но golang.org/x/net/proxy добавляет дайлер SOCKS5. Создайте его с proxy.SOCKS5 и подключите в Transport.DialContext, чтобы имена хостов разрешались на прокси, а не утекали через локальный DNS-запрос.
Это вся картина на стороне клиента: одно поле Transport для настройки, функция для ротации, x/net/proxy для SOCKS5, colly для вежливого скрапинга и семафор плюс контексты для каждого запроса для параллелизма. Начните с чистого ротационного пула, и вы избежите большинства ошибок до их появления.