curl_cffi vs requests: 무엇을 해결하고 무엇을 해결하지 못하는가

requests를 curl_cffi로 교체하면 많은 403이 200으로 바뀝니다. 그러나 소진된 IP에 대해서는 아무런 효과가 없습니다. 두 가지의 차이점과 문제의 원인을 알려주는 15분 테스트를 소개합니다.

curl_cffi vs requests 질문은 보통 사건 중간에 도착합니다: 몇 달 동안 실행되던 스크래퍼가 첫 번째 호출에서 403을 반환하기 시작하고, Reddit에서 클라이언트를 교체하라는 말을 듣고 실제로 작동합니다. 이는 실제 효과와 설명이 있는 현상입니다 — 그러나 '그냥 curl_cffi를 사용하세요'라는 방식으로 반복되면서 실제로 무슨 일이 일어나고 있는지와 어디서 도움이 멈추는지를 숨깁니다. requests는 느리거나 잘못 작성된 것이 아닙니다. 스크래핑 컨텍스트에서 정확히 하나의 단점이 있으며, 이는 API와는 무관합니다. 두 클라이언트가 진정으로 다른 점, 교체 시 변화하는 점, 그리고 어떤 라이브러리를 선택하든 위장이 절대 해결하지 못할 한 가지를 설명합니다.

중요한 한 가지 차이점

두 라이브러리는 동일한 헤더를 보냅니다. 차이점은 한 단계 아래에 있으며, 이는 HTTP 바이트가 이동하기 전에 연결을 여는 TLS 핸드셰이크에 있습니다. requests는 urllib3와 OpenSSL 위에 있으며, 이는 Python에 속하는 암호 목록, 확장 세트 및 순서를 광고합니다. curl_cffi는 브라우저의 ClientHello 바이트를 그대로 재현하는 curl의 패치된 포크에 대한 바인딩이며, HTTP/2 SETTINGS 프레임도 함께 재현합니다 — 따라서 JA3, JA3N 및 Akamai 해시는 스크립팅 라이브러리보다 실제 Chrome과 일치합니다. 안티봇 벤더는 이러한 서명을 데이터베이스에 저장합니다; Chrome User-Agent 헤더와 Python 핸드셰이크 간의 불일치는 해결할 수 없는 모순입니다. 우리는 JA3 및 JA4 지문 인식에서 이 메커니즘을 분석했으며, 동일한 효과가 curl이 브라우저가 200을 받는 동일한 URL에서 403을 받는 이유를 설명합니다.

비교의 나머지 모든 것은 구현에서 비롯됩니다. curl_cffi는 libcurl을 감싸고 있기 때문에 HTTP/2, HTTP/3, 웹소켓 및 asyncio를 상속받으며, requests는 이를 지원한 적이 없습니다. requests는 순수 Python이기 때문에 어디서든 설치할 수 있으며, 10년의 생태계를 가지고 있습니다. 두 진술은 동시에 참이며, 어느 쪽이 우세한지는 전적으로 대상에 달려 있습니다.

5분 안에 실행할 수 있는 재현 가능한 테스트

우리의 통과율 표를 포함하여 누구의 말을 맹신하지 마십시오. 지문 차이는 직접 관찰할 수 있습니다: 두 클라이언트를 TLS-echo 엔드포인트에 연결하고 그들이 보고하는 해시를 비교하십시오. 두 줄이 일치하면, 당신의 빌드는 아무것도 위장하지 않고 있는 것입니다.

# pip install requests curl_cffi
import requests
import curl_cffi

URL = "https://tls.browserleaks.com/json"

a = requests.get(URL, timeout=30).json()
b = curl_cffi.get(URL, impersonate="chrome", timeout=30).json()

print("requests   ja3n:", a["ja3n_hash"], "| akamai:", a.get("akamai_hash", "-"))
print("curl_cffi  ja3n:", b["ja3n_hash"], "| akamai:", b.get("akamai_hash", "-"))

# Two different ja3n hashes = impersonation is working. The curl_cffi README
# documents aa56c057ad164ec4fdcb7a5a283be9fc as a Chrome-matched ja3n value.
# The akamai (HTTP/2) fingerprint is usually empty for requests: it is
# HTTP/1.1 only, so there is no SETTINGS frame to fingerprint in the first place.

v0.15부터 동일한 검사를 위한 단일 명령어 버전이 있습니다: curl-cffi get tls.browserleaks.com/json --impersonate chrome. 다른 디버깅을 시작하기 전에 실행하십시오 — 이는 "내 위장이 잘못 구성되어 있다"와 "내 위장은 괜찮고 다른 것이 나를 차단하고 있다"를 구분합니다, 이는 완전히 다른 오후입니다.

프로토콜 지원, 동시성, 지문 인식 및 이식성에 대한 requests와 curl_cffi의 나란한 비교
requests는 이식성과 생태계에서 승리합니다; curl_cffi는 프로토콜과 지문에서 승리합니다. IP 품질에서는 어느 쪽도 승리하지 않습니다 — 이는 클라이언트 기능이 아닙니다.

curl_cffi가 해결하지 못하는 것: IP 평판

여기서 라이브러리 교체 조언이 빠뜨린 부분이 있습니다. TLS 지문은 "이것이 어떤 소프트웨어인가?"라는 질문에 답합니다. "어디서 오는가?"에 대해서는 아무것도 말하지 않습니다 — 그리고 두 번째 질문은 당신의 출구 IP에 대한 별도의 조회를 통해 답변됩니다: 어떤 ASN이 소유하고 있는지, 호스팅 제공자인지 소비자 ISP인지, 남용 피드에 나타났는지, 지난 한 시간 동안 같은 주소에서 이 사이트에 몇 개의 다른 세션이 있었는지. 데이터센터 범위의 클라우드 VM에서 도착한 완벽한 Chrome 핸드셰이크는 서버 랙에 설치된 Chrome 브라우저입니다. 이는 python-requests보다 더 설득력 있는 것이 아닙니다. 어떤 경우에는 모순이 더 날카롭기 때문에 덜 설득력 있습니다.

프로젝트 자체의 FAQ는 IP 품질을 요청 속도와 JavaScript 지문보다 우선시하며, 위장만으로는 충분하지 않을 수 있는 이유를 설명합니다. 그 순서는 우연이 아닙니다: 평판은 방어자가 평가하기에 가장 저렴한 신호이며, 공격자가 위조하기에 가장 어려운 신호입니다, 헤더나 암호 목록과 달리 로컬에서 생성할 수 없기 때문입니다. requests 기반 스크래퍼가 이미 데이터센터 풀을 통해 실행되고 차단되고 있었다면, 동일한 풀에서 curl_cffi로 이동하면 실패한 두 가지 검사 중 하나가 변경됩니다. 부드러운 대상에서는 부분적인 개선을, 어려운 대상에서는 전혀 개선을 보지 못할 것입니다 — 이는 사람들이 보고하는 혼란스러운 결과와 정확히 일치합니다.

그 축을 위한 해결책은 코드가 아닌 주소 품질입니다: 실제 소비자 ISP 할당에서 나온 주거 IP, 이는 200개 이상의 국가에서 9천만 개 이상의 주소를 제공합니다. 변경하기 전에 현재 출구가 어떻게 보이는지 확인하고 싶다면, 우리의 무료 IP 품질 검사기가 대상이 볼 수 있는 ASN 및 분류를 보고합니다.

어느 축이 고장났는지를 알려주는 2x2

추측하지 말고 실제 대상에 대해 두 변수를 독립적으로 테스트하십시오. 네 개의 요청, 네 개의 출력 줄, 그리고 결과가 문제를 명명합니다:

import requests
import curl_cffi

TARGET = "https://your-target.example/api/items"
DC  = "http://USER:PASS@your-datacenter-gateway:PORT"
RES = "http://USER:PASS@gate.quantumproxies.io:PORT"

def probe(label, fn):
    try:
        print(f"{label:26} -> {fn().status_code}")
    except Exception as e:
        print(f"{label:26} -> {type(e).__name__}")

probe("requests  + datacenter",
      lambda: requests.get(TARGET, proxies={"https": DC}, timeout=30))
probe("curl_cffi + datacenter",
      lambda: curl_cffi.get(TARGET, proxy=DC, impersonate="chrome", timeout=30))
probe("requests  + residential",
      lambda: requests.get(TARGET, proxies={"https": RES}, timeout=30))
probe("curl_cffi + residential",
      lambda: curl_cffi.get(TARGET, proxy=RES, impersonate="chrome", timeout=30))

네 개의 결과를 진리표처럼 읽으십시오:

한 번이 아니라 몇 번 실행하십시오. 차단 레이어는 확률적이며, 단일 200은 거의 아무것도 말해주지 않습니다.

실제 가정용 IP로 주거 행을 테스트하십시오

데이터센터 IP에서 브라우저와 일치하는 TLS 핸드셰이크가 차단되고 주거 IP에서 동일한 핸드셰이크가 통과하는 다이어그램
위장과 IP 평판은 별개의 게이트입니다. 하나를 고치고 다른 하나를 남겨두는 것이 'curl_cffi로 전환했는데 아무것도 변하지 않았다'는 흔한 보고의 이유입니다.

추가 종속성이 가치가 없을 때

솔직함은 재작성보다 저렴합니다. 다음과 같은 경우 requests에 머무르십시오:

그리고 대부분의 사람들이 놓치는 중간 경로가 있습니다: 핸드셰이크를 얻기 위해 requests를 포기할 필요가 없습니다. 유지보수자들은 curl-adapter를 가리키며, 이는 curl_cffi를 requests 전송 어댑터로 장착하고, PyPI의 httpx-curl-cffi는 httpx에 대해 동일한 작업을 수행합니다. 기존 코드와 생태계를 유지하고, 오직 전송되는 바이트만 변경됩니다.

알아두면 좋은 마이그레이션 주의사항

API는 대부분의 스크립트가 import를 변경한 후 실행될 만큼 충분히 가깝지만, 호환성 페이지에는 실제 차이점이 나열되어 있으며, 대규모 포트를 하기 전에 읽어두는 것이 좋습니다. 리다이렉트 응답 본문은 Response.history에 유지되지 않습니다. 빈 도메인이 있는 쿠키는 리다이렉트를 통해 손실될 수 있습니다. 스트리밍 응답 객체는 피클링할 수 없지만, 일반 응답은 가능합니다. 파일 API는 약간 다릅니다. 그리고 전송이나 어댑터는 전혀 없습니다, 왜냐하면 라이브러리는 의도적으로 libcurl-impersonate에 고정되어 있기 때문입니다. 프록시 구성도 사람들을 혼란스럽게 만드는 작은 방식으로 다릅니다:

# requests: dict, and retries mounted on an adapter
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

s = requests.Session()
s.mount("https://", HTTPAdapter(max_retries=Retry(total=4, status_forcelist=[429, 503])))
r = s.get(url, proxies={"https": "http://USER:PASS@gate.quantumproxies.io:PORT"}, timeout=30)

# curl_cffi: single proxy string preferred, retries are a session parameter
from curl_cffi import Session
from curl_cffi.requests import RetryStrategy

s = Session(
    impersonate="chrome",
    proxy="http://USER:PASS@gate.quantumproxies.io:PORT",
    retry=RetryStrategy(count=4, delay=1.0, backoff="exponential"),
    timeout=30,
)
r = s.get(url)  # note: retry fires on transport errors, not on 429/503

전체 프록시 표면 — dict 키, proxy_auth, 요청별 회전, 비동기 및 잘못된 WRONG_VERSION_NUMBER 오류를 생성하는 https:// 접두사 —는 우리의 curl_cffi 프록시 가이드에서 단계별로 다루어집니다. 그대로 유지할 경우, 다른 쪽에 대한 동등한 참조는 우리의 Python Requests 프록시 가이드입니다.

자주 묻는 질문

curl_cffi는 requests보다 빠른가요?

예, 그리고 프로젝트의 벤치마크는 이를 requests가 아닌 aiohttp 및 pycurl과 동등하게 놓습니다. 이점은 C에서 작업을 수행하는 libcurl과 HTTP/2 멀티플렉싱에서 비롯되며, 똑똑한 Python에서 비롯된 것이 아닙니다. 몇 개의 순차적 호출에서는 차이가 보이지 않지만, 높은 동시성, 특히 비동기에서는 상당합니다.

curl_cffi는 안전하게 사용할 수 있나요?

MIT 라이선스이며, 널리 배포되고 사전 컴파일된 휠을 제공하므로 빌드 단계가 없습니다. 주의할 만한 한 가지 경고는 v0.15.0 권고 사항으로, 리다이렉트 기반 SSRF를 다룹니다. 다른 사람들이 제공한 URL을 가져오는 경우, allow_redirects="safe"를 설정하거나 리다이렉트를 비활성화하십시오. 브라우저를 위장하는 것은 기술적 조치이지, 사이트의 약관을 무시할 수 있는 허가가 아닙니다.

curl_cffi는 Cloudflare를 우회하나요?

TLS 및 HTTP/2 지문을 제거하여 기본 보호 수준을 제거합니다. JavaScript 챌린지를 실행하거나 Turnstile을 해결하거나 플래그된 출구 IP를 수정할 수 없습니다. 유지보수자들은 FAQ에서 이를 명확히 하고 있으며, 더 나은 프록시 풀과 브라우저 자동화를 더 높은 수준에 추천합니다.

curl_cffi vs httpx 또는 tls_client — 무엇을 사용해야 하나요?

httpx는 HTTP/2와 비동기를 제공하지만 지문 위장은 제공하지 않으므로, requests와 curl_cffi 사이에 위치합니다. tls_client도 TLS 프로필을 위장하며 유사한 벤치마크를 보입니다; curl_cffi는 더 큰 커뮤니티를 가지고 있으며 HTTP/3 및 웹소켓을 추가합니다. httpx가 이미 스택에 있다면, httpx-curl-cffi 전송은 재작성 없이 위장을 제공합니다.

간단히 말해: 대상이 핸드셰이크를 읽을 때 curl_cffi로 전환하고, 그렇지 않을 때 requests에 머무르며, 어느 선택도 데이터센터 IP를 정화할 것이라고 기대하지 마십시오. 클라이언트는 한 축에서 다르고, 프록시는 다른 축에서 다르며, 차단된 스크래퍼는 거의 항상 두 가지에 대한 이야기입니다. 네 개의 프로브를 실행하고, 진리표를 읽고, 인터넷이 소리쳤던 것이 아니라 데이터가 가리키는 축을 수정하십시오.

위장이 도달할 수 없는 축을 수정하십시오