Scraping de proxy en Go : http.Transport, Auth, Rotation et Concurrence

En Go, toute l'histoire du proxy repose sur un seul champ : Transport.Proxy. Configurez-le correctement et vous obtenez la rotation par requête, SOCKS5 et colly gratuitement. Configurez-le mal et vous obtenez 'context canceled' à 2h du matin.

Go rend le proxying trompeusement simple : toute l'histoire côté client repose sur un seul champ, http.Transport.Proxy. Configurez-le correctement et la rotation par requête, SOCKS5 et l'intégration de colly découlent toutes du même design. Configurez-le mal et vous rencontrez les échecs classiques des proxies Go - context canceled, épuisement du pool de connexions, et une variable HTTP_PROXY que votre transport personnalisé ignore silencieusement. Ce guide parcourt tout le chemin avec du code exécutable.

La phrase clé : http.Transport avec une URL de proxy

Chaque requête proxy en Go passe par un Transport. Le package net/http propose un assistant, http.ProxyURL, qui fixe un proxy statique pour tout le client. Les identifiants vont directement dans l'URL userinfo - Go les transforme en en-tête Proxy-Authorization pour vous :

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
}

Toujours définir Client.Timeout. Go n'a pas de délai d'attente par défaut sur http.Client, donc une seule sortie morte bloquera la goroutine pour toujours - la raison numéro un pour laquelle un scraper Go semble se bloquer est un délai d'attente manquant, pas une cible lente.

Rotation des proxies avec une fonction Proxy

Proxy n'est pas limité à une URL fixe - c'est une fonction func(*http.Request) (*url.URL, error) que Go appelle une fois par requête. C'est votre crochet de rotation. Choisissez un point de terminaison aléatoire dans un pool et chaque requête sort par une IP différente, sans logique de gestion de liste dans votre boucle de récupération :

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
}

Pour la plupart des scrapers, vous ne devriez pas gérer de pool du tout. Pointez la fonction Proxy vers un seul passerelle résidentielle rotative et la passerelle vous fournit une nouvelle IP à chaque requête parmi plus de 90 millions d'adresses dans plus de 200 pays - un point de terminaison, pas de liste, sessions collantes lorsque le flux nécessite la même sortie pendant quelques minutes.

Diagramme montrant un Go http.Client déléguant une requête à http.Transport, qui appelle la fonction Proxy pour acheminer à travers une passerelle résidentielle rotative vers le site cible
Un Transport, une fonction Proxy : la fonction s'exécute par requête, donc un seul client effectue une rotation sur tout le pool.

Authentification de proxy en Go

Mettre USER:PASS@ dans l'URL du proxy est le chemin propre et fonctionne pour ProxyURL et une fonction Proxy personnalisée. Si vous préférez garder les identifiants hors de l'URL, définissez l'en-tête sur le tunnel CONNECT vous-même via Transport.ProxyConnectHeader. Si votre fournisseur utilise l'authentification par liste blanche IP à la place, supprimez complètement les identifiants et autorisez l'IP de votre serveur dans le tableau de bord - QuantumProxies prend en charge les deux. Un CONNECT refusé avec 407 ou proxyconnect tcp signifie presque toujours des identifiants manquants ou incorrects, pas un blocage côté cible.

La variable d'environnement HTTP_PROXY (et quand Go l'ignore)

Le http.DefaultTransport de Go utilise http.ProxyFromEnvironment, qui lit HTTP_PROXY, HTTPS_PROXY et NO_PROXY (selon le package golang.org/x/net/http/httpproxy). Deux comportements piègent les gens. Premièrement, ces variables acceptent soit une URL complète, soit un simple host:port, où le schéma http est supposé. Deuxièmement, les requêtes vers localhost ou une adresse de boucle locale contournent toujours le proxy, renvoyant une URL nulle. Le plus gros piège est l'inverse : dès que vous construisez votre propre &http.Transport{Proxy: ...}, vous avez remplacé le comportement de la variable d'environnement - votre fonction explicite l'emporte et HTTP_PROXY est ignoré. Si vous voulez les deux, enveloppez-les vous-même.

Proxies SOCKS5 en Go

La bibliothèque standard n'a pas de client SOCKS5, donc intégrez golang.org/x/net/proxy et composez à travers elle. Alimentez le composeur dans Transport.DialContext pour que les DNS se résolvent au niveau du proxy, et non sur votre machine (le même problème de fuite de nom d'hôte que socks5h résout dans d'autres piles). Si vous pesez les deux schémas, notre note sur SOCKS5 vs HTTP proxies couvre quand chacun gagne :

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}

Proxies dans colly

Si vous scrapez avec colly, vous obtenez la rotation sans toucher au transport. Le package colly/proxy propose un RoundRobinProxySwitcher qui fait tourner les points de terminaison par requête, et LimitRule gère le rythme. C'est le moyen le plus rapide pour un crawler poli et rotatif en 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,
})

Concurrence bien faite (et le piège du context-canceled)

La concurrence de Go est la raison pour laquelle les équipes le choisissent pour le scraping, et aussi d'où vient l'erreur context canceled. Elle se déclenche lorsqu'un contexte de requête est annulé avant que le corps ne soit entièrement lu - généralement un délai d'attente par requête qui a expiré, ou un cancel() qui s'est exécuté trop tôt à cause d'un defer mal placé. Limitez votre fan-out avec un sémaphore, donnez à chaque requête son propre contexte de délai d'attente, et lisez puis fermez le corps avant que l'annulation ne se déclenche :

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()

Deux autres notes de production. Réglez MaxIdleConnsPerHost suffisamment haut pour qu'un grand pool réutilise les connexions au lieu d'épuiser les descripteurs de fichiers - la cause habituelle de EOF après quelques centaines de requêtes. Et faites tourner la sortie en cas d'échec plutôt que de réessayer la même ; une IP résidentielle morte ne se rétablit pas à la tentative suivante. Pour le design plus large à des millions de pages, notre guide sur l'architecture de scraping à grande échelle couvre les files d'attente, la déduplication et le tiering de proxy.

Réutilisez un client, ajustez le transport

Créez le http.Client une fois et partagez-le entre chaque goroutine - il est sûr pour une utilisation concurrente, et son pool de connexions est ce qui rend un scraper Go rapide. Un client frais par requête jette le keep-alive et force une nouvelle poignée de main TLS à chaque fois. Trois champs Transport importent sous charge : MaxIdleConns et MaxIdleConnsPerHost dimensionnent le pool, et IdleConnTimeout retire les connexions obsolètes pour qu'un pool de proxies rotatifs ne fixe jamais une sortie morte. Définissez-les explicitement - les valeurs par défaut de la bibliothèque standard sont ajustées pour un navigateur, pas un crawler exécutant des milliers de requêtes par minute.

Tableau à deux colonnes mappant les erreurs de proxy Go - proxyconnect refusé, context canceled, x509, EOF, HTTP_PROXY ignoré - à leurs causes et solutions
Presque chaque échec de proxy Go se rapporte à l'un de ces cinq - vérifiez la colonne des causes avant de réessayer la même sortie.

Quand arrêter de rouler les transports à la main

Le code ci-dessus est suffisant pour des cibles propres. Une fois qu'un site ajoute Cloudflare, l'empreinte TLS ou le rendu JavaScript, une poignée de main TLS brute en Go ne ressemble en rien à celle de Chrome, et aucun proxy ne corrige ce décalage. À ce stade, une Scraper API qui porte une véritable empreinte de navigateur, fait tourner les IPs et rend le JS pour vous est moins de code et un taux de succès plus élevé que de maintenir la pile à la main. Si vous choisissez un langage pour un nouveau scraper, notre article sur les meilleurs proxies pour le scraping web cartographie les compromis entre les piles.

Déchargez le rendu et la rotation sur l'API Scraper

Questions fréquemment posées

Comment définir un proxy pour le client HTTP Go ?

Construisez un http.Transport avec un champ Proxy et passez-le à http.Client. Utilisez http.ProxyURL(u) pour un proxy statique unique, ou une func(*http.Request) (*url.URL, error) pour choisir la sortie par requête. Mettez les identifiants dans l'URL sous la forme http://user:pass@host:port et définissez toujours Client.Timeout.

Pourquoi Go ignore-t-il ma variable HTTP_PROXY ?

Parce que vous avez construit un Transport personnalisé avec une fonction Proxy explicite, qui remplace le comportement par défaut ProxyFromEnvironment. Les variables d'environnement ne s'appliquent que lorsque vous utilisez http.DefaultTransport ou définissez Proxy: http.ProxyFromEnvironment vous-même. Notez également que les requêtes en boucle locale et localhost contournent toujours le proxy.

Qu'est-ce qui cause 'context canceled' avec un proxy Go ?

Le contexte de la requête a été annulé avant que le corps de la réponse ne soit lu - généralement un délai d'attente par requête expiré ou un cancel() qui s'est exécuté trop tôt via defer. Donnez à chaque requête son propre contexte de délai d'attente, videz et fermez le corps avant que l'annulation différée ne s'exécute, et ne partagez pas un seul contexte annulable entre plusieurs goroutines.

Go prend-il en charge les proxies SOCKS5 ?

Pas dans la bibliothèque standard, mais golang.org/x/net/proxy ajoute un composeur SOCKS5. Créez-le avec proxy.SOCKS5 et branchez-le dans Transport.DialContext pour que les noms d'hôte se résolvent au niveau du proxy plutôt que de fuir par une recherche DNS locale.

C'est tout le tableau côté client : un champ Transport pour la configuration, une fonction pour la rotation, x/net/proxy pour SOCKS5, colly pour un crawling poli, et un sémaphore plus des contextes par requête pour la concurrence. Commencez avec un pool rotatif propre et vous évitez la plupart de la liste des erreurs avant qu'elles ne se produisent.

Commencez avec les proxies résidentiels QuantumProxies