# PHP cURL 프록시 스크래핑: CURLOPT_PROXY, 인증 및 Guzzle

대부분의 PHP 프록시 코드 조각은 여전히 2011년의 동일한 세 가지 버그를 가지고 있습니다. 여기에 프로덕션 환경에서도 유효한 깨끗한 CURLOPT_PROXY 설정 — 인증, SOCKS5, curl_multi 회전, Guzzle 및 타임아웃 — 이 있습니다.

PHP cURL 프록시 코드는 독특한 문제를 가지고 있습니다: 상위 랭킹 코드 조각이 10년 이상 되었고, 많은 코드가 실제 버그를 포함하고 있습니다 — 모든 요청을 두 번 실행하는 이중 `curl_exec()` 호출, "HTTPS 작동을 위해" 비활성화된 `CURLOPT_SSL_VERIFYPEER`, 이미 기본값으로 설정된 옵션들. 이 가이드는 현재, 올바른 버전입니다: 인증이 포함된 `CURLOPT_PROXY`, SOCKS5, `curl_multi`와의 병렬 회전, Guzzle에서의 동일한 설정, 밤새 멈추지 않도록 하는 타임아웃.

## 최소한의 올바른 PHP cURL 프록시 예제

```php
<?php
function fetch(string $url): string|false {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_PROXY          => 'http://residentialpremium.quantumproxies.io:8080',
        CURLOPT_PROXYUSERPWD   => 'USER:PASS',
        CURLOPT_RETURNTRANSFER => true,  // return the body, don't echo it
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS      => 3,
        CURLOPT_CONNECTTIMEOUT => 10,    // seconds to reach the proxy
        CURLOPT_TIMEOUT        => 30,    // seconds for the whole transfer
        CURLOPT_ENCODING       => '',    // accept gzip/br, decode automatically
    ]);
    $body = curl_exec($ch);
    if ($body === false) {
        error_log('cURL error ' . curl_errno($ch) . ': ' . curl_error($ch));
    }
    curl_close($ch);
    return $body;
}

echo fetch('https://httpbin.org/ip'); // -> the proxy exit IP
```

자격 증명은 `CURLOPT_PROXYUSERPWD`에 표시된 대로 존재할 수 있으며, 프록시 URL에 인라인으로 포함될 수도 있습니다 (`http://USER:PASS@host:port`) — 둘 다 작동합니다. `CURLOPT_RETURNTRANSFER`는 초보자가 놓치는 부분입니다: 이것이 없으면 `curl_exec()`가 응답을 직접 출력에 에코하고 `true`를 반환합니다. 이는 스크래퍼가 원하는 것이 아닙니다. PHP 8에서는 핸들이 `CurlHandle` 객체이며 리소스가 아닙니다; 옵션 호출은 변경되지 않았습니다.

## 사람들이 무작정 따라하는 옵션들 (그리고 하나의 실제 보안 실수)

- `CURLOPT_HTTPPROXYTUNNEL` — HTTPS 대상에는 필요하지 않습니다. cURL은 대상이 `https://`일 때 자동으로 CONNECT 터널을 설정합니다; 이 옵션은 단순 HTTP 대상에 대해 터널링을 강제할 뿐이며, 이는 스크래핑에서 거의 필요하지 않습니다.
- `CURLOPT_CUSTOMREQUEST => 'GET'` — GET은 이미 기본값입니다; 설정해도 아무것도 하지 않으며, 엣지 케이스에서 리다이렉트 처리를 망칩니다.
- `curl_exec()`를 두 번 호출 — 고전적인 Stack Overflow 버그: 요청이 두 번 실행되고 두 번째 반환 값이 첫 번째를 덮어씁니다. 핸들당 한 번만 호출해야 합니다.
- `CURLOPT_SSL_VERIFYPEER => false` — 진정으로 위험한 것. 이는 모든 요청에서 중간자 공격을 허용하여 인증서 오류를 "수정"합니다. PHP 매뉴얼에 따르면 실제 수정 방법은 php.ini에서 `curl.cainfo`를 최신 `cacert.pem` 번들로 지정하는 것입니다.

![설정해야 할 여섯 옵션을 담은 PHP curl_setopt_array: CURLOPT_PROXY, PROXYUSERPWD, RETURNTRANSFER, CONNECTTIMEOUT 10, TIMEOUT 30, ENCODING, 그리고 지워야 할 복사-붙여넣기의 잔재: HTTPPROXYTUNNEL, CUSTOMREQUEST GET, SSL_VERIFYPEER false, 중복 curl_exec. 카드는 인증서 문제는 대신 curl.cainfo로 고치라고 말합니다.](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335147/blog/generated/qp-b9-008-diag1.png)
*여섯 가지 옵션이 전체 작업을 수행합니다; 오른쪽 열은 2011년 튜토리얼에서 복사-붙여넣기한 부채입니다.*

## PHP에서 SOCKS5 프록시

```php
<?php
// scheme style: socks5h = DNS resolved by the proxy (use for scraping)
curl_setopt($ch, CURLOPT_PROXY, 'socks5h://USER:PASS@residentialpremium.quantumproxies.io:1080');

// split style: same result via CURLOPT_PROXYTYPE
curl_setopt($ch, CURLOPT_PROXY, 'residentialpremium.quantumproxies.io:1080');
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);
curl_setopt($ch, CURLOPT_PROXYUSERPWD, 'USER:PASS');
```

스킴은 DNS가 어디에서 발생하는지를 결정합니다: `socks5://` (및 `CURLPROXY_SOCKS5`)는 호스트 이름을 서버에서 해석하여 모든 대상 도메인을 로컬 리졸버에 노출합니다; `socks5h://` (`CURLPROXY_SOCKS5_HOSTNAME`)는 프록시 종료 지점에서 해석합니다. 후자를 선호하십시오. QuantumProxies는 모든 플랜에서 [HTTP 및 SOCKS5](https://quantumproxies.io/socks5-proxies)를 제공하므로 프로토콜 전환은 한 줄의 변경으로 가능합니다, 새로운 구독이 필요하지 않습니다. 동일한 DNS 규칙은 명령줄에서도 적용됩니다 — 이 페이지의 모든 것에 대한 셸 등가물은 [curl 프록시 레시피](https://quantumproxies.io/blog/curl-with-proxy-examples)를 참조하십시오.

## curl_multi를 사용한 회전 및 병렬 요청

PHP에는 async/await가 없지만 병렬 스크래핑에는 필요하지 않습니다 — `curl_multi`는 단일 프로세스에서 수십 개의 전송을 동시에 실행합니다. 모든 핸들을 회전 게이트웨이에 지정하면 각 요청이 자동으로 다른 [주거 IP](https://quantumproxies.io/residential-proxies)에서 종료됩니다, 코드에 프록시 목록 관리가 필요 없습니다:

```php
<?php
$urls = ['https://example.com/p/1', 'https://example.com/p/2', 'https://example.com/p/3'];

$mh = curl_multi_init();
$handles = [];

foreach ($urls as $url) {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        // one gateway, a fresh residential exit per request
        CURLOPT_PROXY          => 'http://USER:PASS@residentialpremium.quantumproxies.io:8080',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_ENCODING       => '',
    ]);
    curl_multi_add_handle($mh, $ch);
    $handles[$url] = $ch;
}

do {
    curl_multi_exec($mh, $running);
    curl_multi_select($mh);        // wait for activity instead of spinning
} while ($running > 0);

$results = [];
foreach ($handles as $url => $ch) {
    $results[$url] = curl_multi_getcontent($ch);
    curl_multi_remove_handle($mh, $ch);
    curl_close($ch);
}
curl_multi_close($mh);
```

배치를 적당히 유지하십시오 — 대부분의 대상에 대해 8에서 16개의 동시 핸들이 충분하며, 속도보다 페이싱이 더 중요합니다: 배치 사이에 몇 백 밀리초의 `usleep()`은 거의 비용이 들지 않으며 요청 속도를 429를 트리거하는 임계값 아래로 유지합니다. 각 핸들에 대해 `curl_getinfo($ch, CURLINFO_HTTP_CODE)`를 확인하고 실패를 인라인으로 다시 가져오는 대신 재시도 큐로 라우팅하십시오. 흐름이 로그인, 장바구니, 쿠키가 있는 페이지 매김과 같은 하나의 ID를 공유해야 하는 여러 요청에 걸쳐 있을 때, 사용자 이름에 접미사를 붙여 그 핸들을 고정 세션으로 전환하십시오: `USER-sid-a1b2c3-ttl-30m`는 회전 대신 지속 시간 동안 동일한 종료 IP를 유지합니다.

![PHP 워커가 8개에서 16개 핸들로 된 curl_multi 배치를 하나의 프록시 URL로 보내면, 게이트웨이가 출구를 결정합니다: 새로운 주거용 IP로 회전하거나, 같은 출구를 유지하는 고정 세션이거나, 지역 타겟 출구입니다. 카드 요약: 회전, USER-session-a1b2c3 고정 사용자 이름, 지역 타겟팅이 모두 프록시 URL 안에 있습니다.](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335147/blog/generated/qp-b9-008-diag2.png)
*작업자는 단순하게 유지되고, 게이트웨이는 스마트하게 유지됩니다: 회전, 고정성 및 지리적 타겟팅은 모두 프록시 URL에 존재합니다.*

## 헤더와 쿠키: 주장하는 브라우저처럼 보이기

기본 cURL 헤더가 있는 깨끗한 종료 IP는 여전히 봇으로 읽힙니다 — PHP는 요청을 꾸미지 않으면 스스로를 발표합니다. 세 가지 옵션이 대부분의 차이를 메웁니다: 현재 브라우저 문자열이 있는 `CURLOPT_USERAGENT`, `Accept-Language` 및 관련 항목에 대한 `CURLOPT_HTTPHEADER`, 그리고 동일한 파일을 가리키는 쿠키 쌍 `CURLOPT_COOKIEJAR` / `CURLOPT_COOKIEFILE`, 이는 브라우저가 하는 방식으로 핸들 간에 세션 쿠키를 지속시킵니다. 쿠키 지속성은 사람들이 예상하는 것보다 더 중요합니다: 많은 사이트가 첫 번째 히트에 토큰을 설정하고 이를 반환하지 않는 클라이언트를 조용히 도전합니다. 주장하는 ID를 일관되게 유지하십시오 — `Accept-Language`가 없는 Chrome User-Agent와 기본 헤더 세트는 자체적으로 지문이며, 일관성 검사는 중간 수준의 안티봇 공급업체가 두 번째 요청에서 PHP 스크래퍼를 잡는 방법입니다.

## Guzzle: 동일한 프록시, 더 나은 사용성

```php
<?php
use GuzzleHttp\Client;

$client = new Client([
    'proxy'           => 'http://USER:PASS@residentialpremium.quantumproxies.io:8080',
    'timeout'         => 30,
    'connect_timeout' => 10,
]);

$res = $client->get('https://httpbin.org/ip');
echo $res->getBody();

// per-request override, e.g. a sticky session for a login flow
$res = $client->get('https://example.com/account', [
    'proxy' => 'http://USER-sid-a1b2c3-ttl-30m:PASS@residentialpremium.quantumproxies.io:8080',
]);
```

Guzzle은 내부적으로 cURL을 구동하므로 위의 모든 것이 여전히 적용됩니다 — 단지 더 잘 읽힙니다. 한 가지 경고: Guzzle의 기본 `timeout`은 0이며, 이는 영원히 기다린다는 의미입니다. 전송 중간에 멈추는 프록시 종료는 PHP-FPM 작업자를 무기한 고정시킬 수 있으므로, 생성하는 모든 클라이언트에 대해 두 타임아웃을 설정하십시오. `proxy` 옵션은 또한 `http`, `https` 및 `no` 키가 있는 배열을 허용하여 스킴별 라우팅이나 내부 호스트 제외가 필요할 때 사용합니다.

## PHP cURL이 충분하지 않을 때

cURL은 HTML을 가져옵니다; JavaScript를 실행하지 않습니다. 응답이 빈 div가 있는 스켈레톤 페이지로 돌아오면, 데이터는 클라이언트에서 렌더링됩니다 — [빈 페이지 응답](https://quantumproxies.io/blog/scraper-returns-empty-page-javascript)에 대한 우리의 가이드가 이를 감지하는 방법을 보여줍니다. 그리고 강화된 대상에서는, cURL의 TLS 핸드셰이크 자체가 IP가 얼마나 깨끗하든 상관없이 당신을 드러냅니다 ([브라우저는 작동하지만 cURL은 403](https://quantumproxies.io/blog/curl-403-but-browser-works) 미스터리). PHP는 요청당 헤드리스 브라우저를 합리적으로 실행할 수 없으므로, 실용적인 탈출구는 [Scraper API](https://quantumproxies.io/scraper-api)입니다: 단일 엔드포인트로의 하나의 cURL 호출, 그리고 렌더링, 브라우저 지문, 회전 및 구문 분석이 깨끗한 markdown, JSON 또는 HTML로 돌아옵니다.

[PHP에서 Scraper API 시도하기](https://quantumproxies.io/scraper-api)

## 자주 묻는 질문

### PHP cURL에서 프록시를 어떻게 사용합니까?

`CURLOPT_PROXY`를 전체 프록시 URL로 설정하고, 자격 증명이 필요하면 `CURLOPT_PROXYUSERPWD`를 추가하며, 항상 `CURLOPT_RETURNTRANSFER`와 명시적인 타임아웃을 활성화하십시오. 이것이 필요한 전체 세트입니다 — HTTPS 대상은 자동으로 터널링되며, 나머지는 선택적 튜닝입니다.

### PHP에서 프록시 사용자 이름과 비밀번호를 어떻게 전달합니까?

`curl_setopt($ch, CURLOPT_PROXYUSERPWD, 'user:pass')`를 사용하거나 URL에 포함하십시오: `http://user:pass@host:port`. HTTP 407을 받으면, 프록시가 해당 자격 증명을 거부한 것입니다 — 오타를 확인하고 특수 문자를 URL 인코딩하십시오. IP 화이트리스트 인증을 사용하는 공급자는 서버의 IP가 승인되면 자격 증명이 전혀 필요하지 않습니다.

### PHP cURL이 SOCKS5 프록시를 사용할 수 있습니까?

예. `socks5h://` URL을 `CURLOPT_PROXY`에 전달하거나 `CURLOPT_PROXYTYPE`을 `CURLPROXY_SOCKS5_HOSTNAME`으로 설정하십시오. 호스트 이름 변형을 일반 `socks5`보다 선호하여 DNS가 서버가 아닌 프록시 종료 지점에서 해석되도록 하십시오 — 그렇지 않으면 모든 대상 도메인이 로컬 리졸버를 통해 누출됩니다.

### CURLOPT_HTTPPROXYTUNNEL은 무엇이며 필요합니까?

이는 cURL이 프록시를 통해 CONNECT로 터널링하도록 강제하며, 프록시에게 URL을 가져오도록 요청하는 대신입니다. `https://` 대상에 대해 cURL은 자동으로 터널링하므로, 이 옵션은 거의 모든 스크래핑 코드에서 중복됩니다. 단순 HTTP 대상에 대해 터널링 의미가 필요할 때만 설정하십시오.

### PHP cURL을 통해 프록시의 타임아웃을 어떻게 설정합니까?

둘 다 설정하십시오: `CURLOPT_CONNECTTIMEOUT`은 프록시에 도달하고 터널을 여는 것을 다루고, `CURLOPT_TIMEOUT`은 전체 전송을 제한합니다. 밀리초 변형 (`_MS`)은 더 엄격한 예산에 사용됩니다. 이들이 없으면 중단된 종료가 요청을 유지하며 — PHP-FPM에서는 전체 작업자를 유지하며 — 웹 서버의 자체 제한이 이를 종료할 때까지 유지됩니다.

첫 번째 코드 조각을 복사하고, 체크리스트를 염두에 두면, PHP는 완벽히 견고한 스크래핑 언어입니다: 병렬성을 위한 `curl_multi`, ID를 위한 회전 게이트웨이, 사용성을 위한 Guzzle, 그리고 반격하는 페이지를 위한 Scraper API.

[QuantumProxies 주거 IP에서 PHP 스크래퍼 실행](https://quantumproxies.io/residential-proxies)
