Goプロキシスクレイピング: http.Transport、認証、ローテーションと並行処理

Goでは、プロキシに関するすべての話は1つのフィールドに集約されています: Transport.Proxy。これを正しく設定すれば、リクエストごとのローテーション、SOCKS5、collyが無料で手に入ります。間違って設定すると、午前2時に「context canceled」に遭遇します。

Goはプロキシを非常にシンプルに見せます: クライアント側のすべての話は1つのフィールド、http.Transport.Proxyに集約されています。これを正しく設定すれば、リクエストごとのローテーション、SOCKS5、collyの統合が同じ設計から自然に生まれます。間違えると、典型的なGoプロキシの失敗に遭遇します - context canceled、接続プールの枯渇、そしてカスタムトランスポートが静かに無視するHTTP_PROXY変数。このガイドは実行可能なコードと共に全体の道筋を案内します。

ワンライナー: プロキシURLを持つhttp.Transport

GoのすべてのプロキシリクエストはTransportを通じて流れます。net/httpパッケージにはヘルパーhttp.ProxyURLがあり、クライアント全体に1つの静的プロキシを固定します。資格情報はURLのユーザー情報に直接入れられ、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にデフォルトのタイムアウトがないため、1つのデッドエグジットがゴルーチンを永遠にブロックします - Goスクレイパーがハングしているように見える一番の理由は、遅いターゲットではなく、タイムアウトが欠けていることです。

プロキシ関数を使用したプロキシのローテーション

Proxyは固定URLに限定されていません - それはfunc(*http.Request) (*url.URL, error)という関数であり、Goはリクエストごとに1回呼び出します。それがあなたのローテーションフックです。プールからランダムなエンドポイントを選び、各リクエストが異なる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関数を1つのローテーティング住宅ゲートウェイに向け、ゲートウェイが90M以上のアドレスを持つ200以上の国で各リクエストに新しいIPを提供します - 1つのエンドポイント、リストなし、数分間同じ出口が必要なフローにはスティッキーセッション。

Go http.Clientがリクエストをhttp.Transportに委任し、Proxy関数がローテーティング住宅ゲートウェイを通じてターゲットサイトにルーティングする図
1つのTransport、1つのProxy関数: 関数はリクエストごとに実行されるため、1つのクライアントがプール全体をローテーションします。

Goにおけるプロキシ認証

プロキシURLにUSER:PASS@を入れるのがクリーンな方法で、ProxyURLとカスタムProxy関数の両方で機能します。資格情報をURLから外したい場合は、Transport.ProxyConnectHeaderを介してCONNECTトンネルのヘッダーを自分で設定します。プロバイダーがIPホワイトリスト認証を使用している場合は、資格情報を完全に削除し、ダッシュボードでサーバーのIPを承認します - QuantumProxiesは両方をサポートしています。407またはproxyconnect tcpで拒否されたCONNECTは、ほとんどの場合、ターゲット側のブロックではなく、資格情報が欠けているか間違っていることを意味します。

HTTP_PROXY環境変数(およびGoがそれを無視する場合)

Goのhttp.DefaultTransporthttp.ProxyFromEnvironmentを使用し、HTTP_PROXYHTTPS_PROXYNO_PROXYを読み取ります(golang.org/x/net/http/httpproxyパッケージに従います)。2つの動作が人々を混乱させます。まず、これらの変数は完全なURLまたは裸のhost:portを受け入れ、httpスキームが仮定されます。次に、localhostまたはループバックアドレスへのリクエストは常にプロキシをバイパスし、nil URLを返します。より大きな問題は逆です: 自分で&http.Transport{Proxy: ...}を構築した瞬間、環境変数の動作を置き換えます - 明示的な関数が優先され、HTTP_PROXYは無視されます。両方を望む場合は、自分でラップしてください。

GoにおけるSOCKS5プロキシ

標準ライブラリにはSOCKS5クライアントがないため、golang.org/x/net/proxyを取り込み、それを通じてダイヤルします。ダイヤラーをTransport.DialContextに供給し、DNSがあなたのマシンではなくプロキシで解決されるようにします(他のスタックで<socks5h>が解決する同じホスト名リーク問題)。2つのスキームを比較する場合、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エラーが発生する場所でもあります。これは、リクエストのコンテキストがボディを完全に読み込む前にキャンセルされたときに発生します - 通常はリクエストごとのタイムアウトが期限切れになったか、deferによって早く実行されたcancel()です。セマフォでファンアウトを制限し、各リクエストに独自のタイムアウトコンテキストを与え、キャンセルが発生する前にボディを読み込んで閉じます:

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

さらに2つのプロダクションノート。MaxIdleConnsPerHostを十分に高く設定し、大きなプールがファイルディスクリプタを使い果たすのではなく接続を再利用するようにします - 数百のリクエスト後のEOFの通常の原因です。そして、失敗時には同じものを再試行するのではなく出口をローテーションします; デッドな住宅IPは次の試行で回復しません。数百万ページの広範な設計については、大規模スクレイピングアーキテクチャに関するガイドがキュー、重複排除、プロキシ階層化をカバーしています。

1つのクライアントを再利用し、トランスポートを調整する

http.Clientを1度作成し、すべてのゴルーチンで共有します - それは並行使用に安全であり、その接続プールがGoスクレイパーを高速にします。リクエストごとに新しいクライアントを作成すると、キープアライブを無駄にし、毎回新しいTLSハンドシェイクを強制します。負荷時に重要な3つのTransportフィールド: MaxIdleConnsMaxIdleConnsPerHostはプールのサイズを決定し、IdleConnTimeoutは古い接続を退役させ、ローテーティングプロキシプールがデッドな出口に固定されないようにします。これらを明示的に設定してください - 標準ライブラリのデフォルトはブラウザ用に調整されており、毎分数千のリクエストを実行するクローラーには適していません。

Goプロキシエラーをその原因と修正にマッピングする2列のテーブル - proxyconnect拒否、context canceled、x509、EOF、無視されたHTTP_PROXY
ほぼすべてのGoプロキシの失敗はこれら5つのいずれかにマッピングされます - 同じ出口を再試行する前に原因列を確認してください。

トランスポートの手作業をやめる時

上記のコードはクリーンなターゲットには十分です。サイトがCloudflare、TLSフィンガープリンティング、またはJavaScriptレンダリングを追加すると、Raw Go TLSハンドシェイクはChromeのものとは全く異なり、プロキシではその不一致を修正できません。その時点で、実際のブラウザフィンガープリントを持ち、IPをローテーションし、JSをレンダリングするScraper APIは、スタックを手作業で維持するよりも少ないコードで成功率が高いです。新しいスクレイパーの言語を選ぶ場合、私たちのウェブスクレイピングのためのベストプロキシ投稿がスタック間のトレードオフをマッピングしています。

レンダリングとローテーションをScraper APIにオフロードする

よくある質問

Go HTTPクライアントにプロキシを設定するにはどうすればいいですか?

Proxyフィールドを持つhttp.Transportを構築し、それをhttp.Clientに渡します。単一の静的プロキシにはhttp.ProxyURL(u)を使用し、リクエストごとに出口を選ぶにはfunc(*http.Request) (*url.URL, error)を使用します。資格情報をhttp://user:pass@host:portとしてURLに入れ、常にClient.Timeoutを設定してください。

なぜGoは私のHTTP_PROXY変数を無視するのですか?

カスタムTransportを明示的なProxy関数で構築したため、デフォルトのProxyFromEnvironment動作を置き換えました。環境変数はhttp.DefaultTransportを使用するか、Proxy: http.ProxyFromEnvironmentを自分で設定した場合にのみ適用されます。また、ループバックとlocalhostのリクエストは常にプロキシをバイパスすることにも注意してください。

Goプロキシで「context canceled」が発生する原因は何ですか?

リクエストのコンテキストがレスポンスボディを読み込む前にキャンセルされました - 通常はリクエストごとのタイムアウトが期限切れになったか、deferによって早く実行されたcancel()です。各リクエストに独自のタイムアウトコンテキストを与え、キャンセルが実行される前にボディを排出して閉じ、1つのキャンセル可能なコンテキストを多くのゴルーチンで共有しないでください。

GoはSOCKS5プロキシをサポートしていますか?

標準ライブラリではありませんが、golang.org/x/net/proxyがSOCKS5ダイヤラーを追加します。proxy.SOCKS5で作成し、Transport.DialContextに接続してホスト名がローカルDNSルックアップを通じて漏れないようにします。

これがクライアント側の全体像です: セットアップのための1つのTransportフィールド、ローテーションのための関数、SOCKS5のためのx/net/proxy、礼儀正しいクロールのためのcolly、並行処理のためのセマフォとリクエストごとのコンテキスト。クリーンなローテーティングプールから始めれば、エラーリストのほとんどを事前に回避できます。

QuantumProxiesの住宅プロキシから始めましょう