Go Proxy Scraping: http.Transport, Authentifizierung, Rotation und Parallelität

In Go spielt sich die gesamte Proxy-Geschichte in einem Feld ab: Transport.Proxy. Richtig eingestellt, erhalten Sie pro Anfrage Rotation, SOCKS5 und colly kostenlos. Falsch eingestellt, erhalten Sie 'context canceled' um 2 Uhr morgens.

Go macht Proxying täuschend einfach: die gesamte Client-seitige Geschichte spielt sich in einem Feld ab, http.Transport.Proxy. Richtig gemacht, und pro Anfrage Rotation, SOCKS5 und colly Integration ergeben sich aus dem gleichen Design. Falsch gemacht, und Sie treffen auf die klassischen Go Proxy-Fehler - context canceled, Erschöpfung des Verbindungspools und eine HTTP_PROXY Variable, die Ihr benutzerdefinierter Transport stillschweigend ignoriert. Dieser Leitfaden geht den gesamten Weg mit ausführbarem Code.

Der Einzeiler: http.Transport mit einer Proxy-URL

Jede proxied Anfrage in Go fließt durch einen Transport. Das net/http Paket liefert einen Helfer, http.ProxyURL, der einen statischen Proxy für den gesamten Client festlegt. Anmeldedaten gehen direkt in die URL-Benutzerinfo - Go wandelt sie für Sie in den Proxy-Authorization Header um:

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
}

Setzen Sie immer Client.Timeout. Go hat keinen Standard-Timeout auf http.Client, sodass ein einzelner toter Ausgang die Goroutine für immer blockieren wird - der Hauptgrund, warum ein Go-Scraper hängen bleibt, ist ein fehlendes Timeout, nicht ein langsames Ziel.

Proxies mit einer Proxy-Funktion rotieren

Proxy ist nicht auf eine feste URL beschränkt - es ist eine Funktion func(*http.Request) (*url.URL, error), die Go einmal pro Anfrage aufruft. Das ist Ihr Rotationshaken. Wählen Sie einen zufälligen Endpunkt aus einem Pool und jede Anfrage verlässt durch eine andere IP, ohne jegliche Listenverwaltungslogik in Ihrer Abrufschleife:

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
}

Für die meisten Scraping-Vorgänge sollten Sie überhaupt keinen Pool verwalten. Zeigen Sie die Proxy Funktion auf ein einzelnes rotierendes Residential Gateway und das Gateway gibt Ihnen bei jeder Anfrage eine neue IP aus über 90 Millionen Adressen in über 200 Ländern - ein Endpunkt, keine Liste, Sticky-Sessions, wenn ein Fluss für ein paar Minuten den gleichen Ausgang benötigt.

Diagramm, das einen Go http.Client zeigt, der eine Anfrage an http.Transport delegiert, das die Proxy-Funktion aufruft, um durch ein rotierendes Residential Gateway zur Zielseite zu leiten
Ein Transport, eine Proxy-Funktion: die Funktion läuft pro Anfrage, sodass ein einzelner Client über den gesamten Pool rotiert.

Proxy-Authentifizierung in Go

USER:PASS@ in die Proxy-URL zu setzen, ist der saubere Weg und funktioniert sowohl für ProxyURL als auch für eine benutzerdefinierte Proxy Funktion. Wenn Sie es vorziehen, Anmeldedaten aus der URL herauszuhalten, setzen Sie den Header selbst auf dem CONNECT-Tunnel über Transport.ProxyConnectHeader. Wenn Ihr Anbieter stattdessen IP-Whitelist-Authentifizierung verwendet, lassen Sie die Anmeldedaten vollständig weg und autorisieren Sie die IP Ihres Servers im Dashboard - QuantumProxies unterstützt beides. Ein abgelehnter CONNECT mit 407 oder proxyconnect tcp bedeutet fast immer fehlende oder falsche Anmeldedaten, nicht eine Zielseitensperre.

Die HTTP_PROXY Umgebungsvariable (und wann Go sie ignoriert)

Go's http.DefaultTransport verwendet http.ProxyFromEnvironment, das HTTP_PROXY, HTTPS_PROXY und NO_PROXY liest (gemäß dem golang.org/x/net/http/httpproxy Paket). Zwei Verhaltensweisen führen zu Verwirrung. Erstens akzeptieren diese Variablen entweder eine vollständige URL oder ein einfaches host:port, wobei das http Schema angenommen wird. Zweitens umgehen Anfragen an localhost oder eine Loopback-Adresse immer den Proxy und geben eine null URL zurück. Der größere Stolperstein ist das Gegenteil: Sobald Sie Ihr eigenes &http.Transport{Proxy: ...} erstellen, haben Sie das Verhalten der Umgebungsvariablen ersetzt - Ihre explizite Funktion gewinnt und HTTP_PROXY wird ignoriert. Wenn Sie beides wollen, wickeln Sie sie selbst ein.

SOCKS5 Proxies in Go

Die Standardbibliothek hat keinen SOCKS5-Client, also ziehen Sie golang.org/x/net/proxy hinzu und wählen Sie darüber. Füttern Sie den Dialer in Transport.DialContext ein, damit DNS beim Proxy aufgelöst wird, nicht auf Ihrem Rechner (das gleiche Hostname-Leak-Problem, das socks5h in anderen Stacks löst). Wenn Sie die beiden Schemata abwägen, deckt unser Hinweis auf SOCKS5 vs HTTP Proxies ab, wann welches gewinnt:

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 in colly

Wenn Sie mit colly scrapen, erhalten Sie Rotation, ohne den Transport zu berühren. Das colly/proxy Paket liefert einen RoundRobinProxySwitcher, der Endpunkte pro Anfrage zyklisch wechselt, und LimitRule regelt das Tempo. Dies ist der schnellste Weg zu einem höflichen, rotierenden Crawler in 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,
})

Parallelität richtig gemacht (und die context-canceled Falle)

Go's Parallelität ist der Grund, warum Teams es für Scraping wählen, und auch der Ursprung des context canceled Fehlers. Er tritt auf, wenn der Kontext einer Anfrage abgebrochen wird, bevor der Body vollständig gelesen ist - normalerweise ein abgelaufenes pro Anfrage Timeout oder ein cancel(), das zu früh ausgeführt wurde, weil ein defer falsch platziert war. Begrenzen Sie Ihr Fan-out mit einem Semaphor, geben Sie jeder Anfrage ihren eigenen Timeout-Kontext und lesen und schließen Sie den Body, bevor das Cancel ausgelöst wird:

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

Zwei weitere Produktionshinweise. Setzen Sie MaxIdleConnsPerHost hoch genug, damit ein großer Pool Verbindungen wiederverwendet, anstatt Dateideskriptoren zu erschöpfen - die übliche Ursache für EOF nach ein paar hundert Anfragen. Und rotieren Sie den Ausgang bei einem Fehler, anstatt denselben erneut zu versuchen; eine tote Residential-IP erholt sich nicht beim nächsten Versuch. Für das breitere Design bei Millionen von Seiten deckt unser Leitfaden zur Architektur des großflächigen Web-Scrapings Warteschlangen, Deduplizierung und Proxy-Tiering ab.

Einen Client wiederverwenden, den Transport optimieren

Erstellen Sie den http.Client einmal und teilen Sie ihn über jede Goroutine - er ist sicher für gleichzeitige Nutzung und sein Verbindungspool macht einen Go-Scraper schnell. Ein frischer Client pro Anfrage verwirft Keep-Alive und erzwingt jedes Mal einen neuen TLS-Handshake. Drei Transport Felder sind unter Last wichtig: MaxIdleConns und MaxIdleConnsPerHost dimensionieren den Pool, und IdleConnTimeout beendet veraltete Verbindungen, sodass ein rotierender Proxy-Pool niemals einen toten Ausgang fixiert. Setzen Sie sie explizit - die Standardbibliotheks-Defaults sind für einen Browser abgestimmt, nicht für einen Crawler, der Tausende von Anfragen pro Minute ausführt.

Zweispaltige Tabelle, die Go Proxy-Fehler - proxyconnect verweigert, context canceled, x509, EOF, ignoriertes HTTP_PROXY - ihren Ursachen und Lösungen zuordnet
Fast jeder Go Proxy-Fehler lässt sich auf einen dieser fünf zurückführen - überprüfen Sie die Spalte Ursache, bevor Sie denselben Ausgang erneut versuchen.

Wann man aufhören sollte, Transports von Hand zu rollen

Der obige Code reicht für saubere Ziele aus. Sobald eine Seite Cloudflare, TLS-Fingerprinting oder JavaScript-Rendering hinzufügt, sieht ein roher Go TLS-Handshake nichts wie der von Chrome aus, und kein Proxy behebt dieses Missverhältnis. An diesem Punkt ist eine Scraper API, die einen echten Browser-Fingerabdruck trägt, IPs rotiert und JS für Sie rendert, weniger Code und eine höhere Erfolgsrate als das Stack selbst zu pflegen. Wenn Sie eine Sprache für einen neuen Scraper wählen, zeigt unser beste Proxies für Web-Scraping Beitrag die Kompromisse zwischen den Stacks auf.

Rendering und Rotation an die Scraper API auslagern

Häufig gestellte Fragen

Wie setze ich einen Proxy für den Go HTTP-Client?

Erstellen Sie einen http.Transport mit einem Proxy Feld und übergeben Sie ihn an http.Client. Verwenden Sie http.ProxyURL(u) für einen einzelnen statischen Proxy oder eine func(*http.Request) (*url.URL, error), um den Ausgang pro Anfrage zu wählen. Setzen Sie Anmeldedaten in die URL als http://user:pass@host:port und setzen Sie immer Client.Timeout.

Warum ignoriert Go meine HTTP_PROXY Variable?

Weil Sie einen benutzerdefinierten Transport mit einer expliziten Proxy Funktion erstellt haben, die das Standardverhalten ProxyFromEnvironment ersetzt. Umgebungsvariablen gelten nur, wenn Sie http.DefaultTransport verwenden oder selbst Proxy: http.ProxyFromEnvironment setzen. Beachten Sie auch, dass Loopback- und localhost-Anfragen immer den Proxy umgehen.

Was verursacht 'context canceled' bei einem Go Proxy?

Der Kontext der Anfrage wurde abgebrochen, bevor der Antwort-Body gelesen wurde - typischerweise ein abgelaufenes pro Anfrage Timeout oder ein cancel(), das zu früh über defer ausgeführt wurde. Geben Sie jeder Anfrage ihren eigenen Timeout-Kontext, leeren und schließen Sie den Body, bevor das verzögerte Cancel ausgeführt wird, und teilen Sie keinen abbrechbaren Kontext über viele Goroutinen.

Unterstützt Go SOCKS5 Proxies?

Nicht in der Standardbibliothek, aber golang.org/x/net/proxy fügt einen SOCKS5-Dialer hinzu. Erstellen Sie ihn mit proxy.SOCKS5 und stecken Sie ihn in Transport.DialContext, damit Hostnamen beim Proxy aufgelöst werden, anstatt durch eine lokale DNS-Abfrage zu lecken.

Das ist das gesamte Client-seitige Bild: ein Transport Feld für die Einrichtung, eine Funktion für die Rotation, x/net/proxy für SOCKS5, colly für höfliches Crawlen und ein Semaphor plus pro Anfrage Kontexte für Parallelität. Beginnen Sie mit einem sauberen rotierenden Pool und Sie umgehen die meisten Fehler, bevor sie auftreten.

Beginnen Sie mit QuantumProxies Residential Proxies