Go 프록시 스크래핑: http.Transport, 인증, 회전 및 동시성

Go에서는 전체 프록시 스토리가 하나의 필드에 존재합니다: Transport.Proxy. 이를 올바르게 설정하면 요청별 회전, SOCKS5 및 colly를 무료로 얻을 수 있습니다. 잘못 설정하면 새벽 2시에 'context canceled' 오류를 만나게 됩니다.

Go는 프록시 설정을 놀랍도록 간단하게 만듭니다: 전체 클라이언트 측 스토리가 하나의 필드, http.Transport.Proxy에 존재합니다. 이를 올바르게 설정하면 요청별 회전, SOCKS5 및 colly 통합이 동일한 설계에서 모두 해결됩니다. 잘못 설정하면 Go 프록시의 고전적인 실패 - context canceled, 연결 풀 소진, 그리고 사용자 정의 전송이 조용히 무시하는 HTTP_PROXY 변수를 만나게 됩니다. 이 가이드는 실행 가능한 코드와 함께 전체 경로를 안내합니다.

한 줄 요약: 프록시 URL을 가진 http.Transport

Go에서 모든 프록시 요청은 Transport를 통해 흐릅니다. net/http 패키지는 전체 클라이언트에 대해 하나의 정적 프록시를 고정하는 도우미 http.ProxyURL을 제공합니다. 자격 증명은 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에 기본 타임아웃이 없으므로 단일 죽은 출구가 고루틴을 영원히 차단할 것입니다 - Go 스크래퍼가 멈춘 것처럼 보이는 가장 큰 이유는 느린 대상이 아니라 누락된 타임아웃입니다.

프록시 함수로 프록시 회전

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 함수를 단일 회전 주거 게이트웨이로 지정하면 게이트웨이가 200개 이상의 국가에서 90M+ 주소에 걸쳐 모든 요청에 대해 새로운 IP를 제공합니다 - 하나의 엔드포인트, 목록 없음, 몇 분 동안 동일한 출구가 필요한 흐름에는 고정 세션.

Go http.Client가 http.Transport에 요청을 위임하고, Proxy 함수를 호출하여 회전 주거 게이트웨이를 통해 대상 사이트로 라우팅하는 다이어그램
하나의 Transport, 하나의 Proxy 함수: 함수는 요청당 실행되므로, 단일 클라이언트가 전체 풀을 회전합니다.

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_PROXY, HTTPS_PROXYNO_PROXY를 읽습니다 (golang.org/x/net/http/httpproxy 패키지에 따라). 두 가지 동작이 사람들을 혼란스럽게 합니다. 첫째, 이러한 변수는 전체 URL 또는 단순 host:port를 수용하며, 여기서 http 스킴이 가정됩니다. 둘째, localhost 또는 루프백 주소로의 요청은 항상 프록시를 우회하여 nil URL을 반환합니다. 더 큰 문제는 반대입니다: 자신만의 &http.Transport{Proxy: ...}를 구축하는 순간, 환경 변수 동작을 대체한 것입니다 - 명시적인 함수가 우선하며 HTTP_PROXY는 무시됩니다. 둘 다 원한다면 직접 래핑하십시오.

Go에서의 SOCKS5 프록시

표준 라이브러리에는 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 오류가 발생하는 곳입니다. 이는 요청의 컨텍스트가 본문이 완전히 읽히기 전에 취소될 때 발생합니다 - 일반적으로 만료된 요청별 타임아웃이나 잘못된 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()

생산 노트 두 가지. MaxIdleConnsPerHost를 충분히 높게 설정하여 큰 풀이 파일 디스크립터를 소진하는 대신 연결을 재사용하도록 하십시오 - 몇 백 개의 요청 후 EOF가 발생하는 일반적인 원인입니다. 그리고 실패 시 동일한 출구를 다시 시도하는 대신 출구를 회전하십시오; 죽은 주거 IP는 다음 시도에서 복구되지 않습니다. 수백만 페이지의 더 넓은 설계를 위해, 우리의 대규모 스크래핑 아키텍처 가이드는 큐, 중복 제거 및 프록시 계층화를 다룹니다.

하나의 클라이언트를 재사용하고 전송을 조정하십시오

http.Client를 한 번 생성하고 모든 고루틴에 걸쳐 공유하십시오 - 이는 동시 사용에 안전하며, 연결 풀이 Go 스크래퍼를 빠르게 만드는 요소입니다. 요청당 새로운 클라이언트는 keep-alive를 버리고 매번 새로운 TLS 핸드셰이크를 강제합니다. 세 가지 Transport 필드가 부하 시 중요합니다: MaxIdleConnsMaxIdleConnsPerHost는 풀의 크기를 조정하고, IdleConnTimeout은 회전 프록시 풀이 죽은 출구를 고정하지 않도록 오래된 연결을 은퇴시킵니다. 이를 명시적으로 설정하십시오 - 표준 라이브러리 기본값은 브라우저에 맞춰져 있으며, 수천 개의 요청을 분당 실행하는 크롤러에는 맞지 않습니다.

Go 프록시 오류와 그 원인 및 해결책을 매핑하는 두 열 테이블 - proxyconnect 거부, context canceled, x509, EOF, 무시된 HTTP_PROXY
거의 모든 Go 프록시 실패는 이 다섯 가지 중 하나에 매핑됩니다 - 동일한 출구를 다시 시도하기 전에 원인 열을 확인하십시오.

전송을 수작업으로 롤링하는 것을 언제 멈출지

위의 코드는 깨끗한 대상에 충분합니다. 사이트가 Cloudflare, TLS 지문 인식 또는 JavaScript 렌더링을 추가하면, 원시 Go TLS 핸드셰이크는 Chrome의 것과 전혀 다르게 보이며, 그 불일치를 수정할 수 있는 프록시는 없습니다. 그 시점에서는 실제 브라우저 지문을 가지고 IP를 회전하고 JS를 렌더링하는 Scraper API가 스택을 수작업으로 유지하는 것보다 더 적은 코드와 더 높은 성공률을 제공합니다. 새로운 스크래퍼를 위한 언어를 선택 중이라면, 우리의 웹 스크래핑을 위한 최고의 프록시 게시물은 스택 간의 트레이드오프를 매핑합니다.

렌더링 및 회전을 Scraper API에 오프로드하십시오

자주 묻는 질문

Go HTTP 클라이언트에 프록시를 어떻게 설정하나요?

http.TransportProxy 필드와 함께 빌드하고 http.Client에 전달하십시오. 단일 정적 프록시에는 http.ProxyURL(u)을 사용하거나, 요청당 출구를 선택하려면 func(*http.Request) (*url.URL, error)을 사용하십시오. 자격 증명을 http://user:pass@host:port 형식으로 URL에 넣고 항상 Client.Timeout을 설정하십시오.

Go가 내 HTTP_PROXY 변수를 무시하는 이유는 무엇인가요?

명시적인 Proxy 함수를 가진 사용자 정의 Transport를 빌드했기 때문입니다. 이는 기본 ProxyFromEnvironment 동작을 대체합니다. 환경 변수는 http.DefaultTransport를 사용하거나 Proxy: http.ProxyFromEnvironment를 직접 설정할 때만 적용됩니다. 또한 루프백 및 localhost 요청은 항상 프록시를 우회한다는 점도 유의하십시오.

Go 프록시에서 'context canceled' 오류의 원인은 무엇인가요?

요청의 컨텍스트가 응답 본문이 읽히기 전에 취소되었습니다 - 일반적으로 만료된 요청별 타임아웃이나 잘못된 defer로 인해 너무 일찍 실행된 cancel() 때문입니다. 각 요청에 자체 타임아웃 컨텍스트를 부여하고, 연기된 취소가 실행되기 전에 본문을 소진하고 닫으십시오. 하나의 취소 가능한 컨텍스트를 여러 고루틴에 걸쳐 공유하지 마십시오.

Go는 SOCKS5 프록시를 지원하나요?

표준 라이브러리에는 없지만, golang.org/x/net/proxy가 SOCKS5 다이얼러를 추가합니다. proxy.SOCKS5로 생성하고 Transport.DialContext에 연결하여 호스트 이름이 로컬 DNS 조회를 통해 누출되지 않고 프록시에서 해결되도록 하십시오.

이것이 전체 클라이언트 측 그림입니다: 설정을 위한 하나의 Transport 필드, 회전을 위한 함수, SOCKS5를 위한 x/net/proxy, 예의 바른 크롤링을 위한 colly, 그리고 동시성을 위한 세마포어와 요청별 컨텍스트입니다. 깨끗한 회전 풀로 시작하면 대부분의 오류 목록을 발생하기 전에 건너뛸 수 있습니다.

QuantumProxies 주거 프록시로 시작하십시오