2026년 10월 3일, 작은 규모의 스텔스 브라우저 벤치마크를 돌렸습니다. 대조군인 순정 Playwright와 Patchright, nodriver, Camoufox, Zendriver를 공개 봇 탐지 테스트 페이지에 접속시켰고, 한 번은 Hetzner 데이터센터 IP로, 한 번은 미국 레지덴셜 프록시를 거쳐 실행했습니다. 자동화 판정은 모두 브라우저가 결정했습니다. 순정 Playwright는 두 IP 모두에서 모든 실행마다 deviceandbrowserinfo, BrowserScan, Fingerprint playground, Pixelscan에 탐지됐습니다. headful로 실행한 스텔스 도구 4종은 판정이 나온 모든 실행에서 앞의 세 페이지를 통과했고, Pixelscan은 이들의 headful 실행 39회 중 2회를 탐지했습니다(2회는 n/a). 레지덴셜 IP가 바꾼 것은 네트워크를 보는 검사뿐이었고, 그마저 늘 좋은 방향은 아니었습니다.
호스트 하나, 프록시 국가 하나에서 기록한 54회 실행이며, 테스트용으로 만든 페이지를 대상으로 했으므로 실제 서비스의 안티봇 순위가 아닙니다. 아래에 방법, 원본 판정, 데이터로 뒷받침되지 않는 것, 그리고 재현용 코드를 정리했습니다.
스텔스 브라우저 벤치마크: 요약
브라우저가 자동화 흔적을 흘리면 레지덴셜 IP로도 구할 수 없습니다. 브라우저가 깨끗하다면 IP는 주로 네트워크 쪽 검사, 즉 프록시 탐지, 타임존 일관성, WebRTC에 영향을 줍니다. 이 페이지들에서는 양쪽으로 작용했습니다.
- 자동화 판정은 IP에 따라 바뀌지 않았습니다. Playwright는 두 네트워크 모두에서 headful 10회 실행 전부 deviceandbrowserinfo에서
isBot: true, BrowserScan에서 “WebDriver: Robot”, Fingerprint playground에서 “bad bot (type: webdriver)”를 받았습니다. Patchright, nodriver, Zendriver, Camoufox는 어떤 headful 실행에서도 이런 판정을 받지 않았습니다. - Pixelscan은 데이터센터 IP에 반응했습니다. 프록시를 전혀 쓰지 않았는데도 Hetzner IP에서 실행한 20회 모두 “Proxy detected”를 표시했습니다. 레지덴셜 프록시를 거쳤을 때는 34회 중 3회였습니다.
- Fingerprint playground는 레지덴셜 IP에 반응했습니다. 프록시 신호가 레지덴셜 실행 34회 중 33회를 탐지했고, 종종 프록시 네트워크 이름까지 붙였습니다(“residential proxy from SOAX”, “from INFATICA” 등). Chrome 기반 도구에 대해서는 Hetzner IP를 탐지하지 않았습니다. Chrome 기반 스텔스 도구 3종의 의심 점수는 데이터센터에서 0–6, 스티키 레지덴셜 프록시를 거쳐서는 8–24였습니다.
- 도구 5종 중 3종이 프록시를 거치면서 WebRTC로 서버의 실제 IP를 노출했습니다. Playwright, Patchright, nodriver는 프록시를 쓴 모든 실행에서 노출했습니다. Zendriver는 기본적으로 WebRTC를 차단하고 Camoufox는 위장합니다.
- IP보다 headless 모드가 더 중요했습니다. headless 모드에서는 Patchright와 nodriver가 headful일 때 통과했던 바로 그 페이지들에 탐지됐습니다.
테스트 구성: 버전, 네트워크 조건, 대상
모든 테스트는 4코어 Linux 호스트에서 Xvfb를 갖춘 Debian 12 컨테이너 하나로, 한 번에 브라우저 하나씩 실행했습니다. 각 도구는 자체 문서가 권장하는 실행 설정만 썼습니다. 커스텀 User-Agent, 핑거프린트 인젝션, 마우스 이동, 리소스 차단은 없었습니다. Patchright의 경우 README의 “Best Practice” 설정, 즉 persistent context, 실제 Chrome, headful, no_viewport=True를 썼습니다.
| 도구 | 패키지 | 브라우저 | 실행 방식 |
|---|---|---|---|
| Playwright(대조군) | playwright 1.62.0 | Playwright Chromium 151.0.7922.34 | chromium.launch(headless=False), 기본값 |
| Patchright | patchright 1.63.0 | Google Chrome 154.0.8037.97 | launch_persistent_context(channel="chrome", headless=False, no_viewport=True) |
| nodriver | nodriver 0.50.3 | Google Chrome 154.0.8037.97 | start(headless=False, sandbox=False) |
| Zendriver | zendriver 0.17.1 | Google Chrome 154.0.8037.97 | start(headless=False, sandbox=False) |
| Camoufox | camoufox 0.5.6 | Camoufox 152.0.4-beta.31 | AsyncCamoufox(headless=False, os="linux", geoip=True) |
타협한 점 두 가지를 먼저 밝힙니다. 대조군은 최신 1.63.0이 아닌 Playwright 1.62.0입니다. camoufox 0.5.6이 playwright<1.63을 요구하는데 모든 도구가 한 환경을 공유했기 때문입니다. 또 호스트 디스크가 거의 차서 macOS·Windows 폰트 번들 압축 해제를 건너뛰었기 때문에 Camoufox는 os="linux"와 Linux 폰트만으로 실행했습니다. Camoufox의 기본 동작은 OS를 무작위로 고르는 것입니다.
네트워크 조건:
- 데이터센터: 호스트 자체 IP, AS24940 Hetzner Online GmbH, 헬싱키(ipinfo.io 기준).
- 레지덴셜, 스티키: 실행마다 새 스티키 세션을 쓰는 미국 레지덴셜 출구. 프록시를 쓴 모든 실행에서 모든 도구가 같은 소형 로컬 릴레이를 거쳐 연결했습니다. nodriver와 Zendriver는 Chrome 플래그로 프록시 인증 정보를 넘길 수 없기 때문에, 이 릴레이가 프록시 인증을 붙이고 바이트 수를 셉니다.
- 부가 조건 세 가지: 연결마다 출구 IP가 바뀌는 로테이팅 레지덴셜 패스, 예열(warm-up) 패스(아래에서 설명), 그리고 데이터센터 IP에서 도구별 headless 1회 실행.
대상은 실행마다 새 브라우저에서 이 순서대로, 각각 새 탭으로 방문했습니다: bot.sannysoft.com, BrowserScan 봇 탐지, deviceandbrowserinfo “Are you a bot?”, CreepJS, Fingerprint playground, nowsecure.nl(Cloudflare 뒤에 있으며, 일반 curl은 cf-mitigated: challenge와 함께 403을 받음), 그리고 Pixelscan의 봇 체크와 핑거프린트 체크. Fingerprint의 봇 탐지 제품 페이지는 실시간 판정이 아닌 정적 JSON 샘플만 보여 주기 때문에 제외했습니다.
측정 항목: 페이지 로드 여부, 페이지가 출력한 그대로의 판정, 그리고 판정이 나타나기까지의 시간(0.5초마다 폴링, 타임아웃 45초). 방문할 때마다 페이지 텍스트와 전체 페이지 스크린샷을 저장했습니다. 파싱할 수 없는 판정은 추측하지 않고 “unparsed”로 기록했습니다. headful 방문 중 4건이 그렇게 끝났는데, 페이지 타임아웃 3건과 갱신 도중 캡처 1건입니다. 표에서는 n/a로 표시했습니다.
실행 횟수: 데이터센터, 로테이팅, 스티키 레지덴셜은 조건별·도구별 3회, headless와 예열은 도구별 1회. UTC 06:50부터 08:02 사이에 총 54회 실행했습니다.
결과: 브라우저 레벨 자동화 검사
표의 숫자는 실행 횟수입니다. “3/3”은 세 번 모두 페이지가 브라우저를 탐지했다는 뜻입니다. 모든 셀은 파싱한 결과 파일에서 가져왔습니다.
| 도구 | 네트워크 | sannysoft 실패 항목 | BrowserScan 탐지 | deviceandbrowserinfo isBot | CreepJS headless | Fingerprint “Bot” | Pixelscan 봇 배너 |
|---|---|---|---|---|---|---|---|
| Playwright(대조군) | 데이터센터(Hetzner) | WebDriver | 3/3 | 3/3 | 33% | 3/3: You are a bad bot (type: webdriver) | 3/3 |
| Playwright(대조군) | 미국 레지덴셜, 스티키 | WebDriver | 3/3 | 3/3 | 33% | 3/3: You are a bad bot (type: webdriver) | 3/3 |
| Patchright | 데이터센터(Hetzner) | 없음 | 0/3 | 0/3 | 0% | Not detected (3/3) | 0/3 |
| Patchright | 미국 레지덴셜, 스티키 | 없음 | 0/3 | 0/3 | 0% | Not detected (3/3) | 0/3 |
| nodriver | 데이터센터(Hetzner) | 없음 | 0/3 | 0/3 | 0% | Not detected (3/3) | 0/3 |
| nodriver | 미국 레지덴셜, 스티키 | 없음 | 0/3 | 0/3 | 0% | Not detected (3/3) | 0/2 (+1 n/a) |
| Zendriver | 데이터센터(Hetzner) | 없음 | 0/3 | 0/3 | 0% | Not detected (3/3) | 0/3 |
| Zendriver | 미국 레지덴셜, 스티키 | 없음 | 0/3 | 0/3 | 0% | Not detected (3/3) | 0/3 |
| Camoufox | 데이터센터(Hetzner) | Chrome | 0/3 | 0/3 | 0% | Not detected (3/3) | 0/3 |
| Camoufox | 미국 레지덴셜, 스티키 | Chrome | 0/3 | 0/3 | 0% | Not detected (3/3) | 0/3 |
표에 드러나지 않는 세부 사항:
- Camoufox의 유일한 sannysoft 실패 항목은 “Chrome (New): missing”입니다. Firefox 기반 브라우저라면 당연한 결과이며, 자동화 흔적이 아닙니다.
- CreepJS는 “% like headless” 점수도 출력합니다. 모든 headful 실행에서 Playwright는 50%, Chrome 기반 스텔스 도구 3종은 44%, Camoufox는 6%였습니다. “% stealth” 점수는 모두 0%였습니다. CreepJS는 이 수치들을 통과/실패로 구분하지 않습니다.
- Pixelscan 봇 체크는 두 번 뒤집혔습니다. Patchright가 단 한 번의 예열 실행에서 “Bot Behavior Detected”(CDP 타일)를 받았고, Zendriver가 로테이팅 패스에서 한 번 받았습니다. 둘 다 스크린샷에서 확인됩니다. Pixelscan 방문 2건은 n/a입니다. 한 건은 타임아웃이었고, 다른 한 건은 텍스트 캡처가 페이지 갱신 도중에 이루어졌는데, 잠시 뒤 찍은 스크린샷에는 “You're Definitely a Human”이 표시되어 있습니다.
- nowsecure.nl은 데이터센터 IP의 순정 Playwright를 포함해 모든 headful 실행에서 화면에 챌린지 없이 페이지 내용을 보여 줬습니다. headful에서는 도구 간 차이를 전혀 가려내지 못했습니다.
이 검사들이 왜 순정 Playwright를 잡아내는지(navigator.webdriver 플래그, CDP Runtime.enable의 부작용, 실행 플래그)는 Patchright 심층 분석과 브라우저 핑거프린팅 해설을 참고하세요.
결과: IP를 보는 신호, 데이터센터 vs 레지덴셜
네트워크를 평가하는 페이지는 두 개뿐입니다. Fingerprint playground는 VPN, 프록시, IP 차단 목록, 의심 점수를 보여 주고, Pixelscan 핑거프린트 체크는 프록시와 위치 일관성을 보여 줍니다. deviceandbrowserinfo는 자사 테스트가 “IP 평판이나 사용자 행동을 사용하지 않는다”고 명시합니다. IP에 따라 판정이 움직이지 않은 것은 이와 부합합니다.
| 도구 | 네트워크 | Fingerprint 프록시 플래그 | Fingerprint VPN 플래그 | Fingerprint 의심 점수 | Pixelscan “Proxy detected” | Pixelscan 위치 검사 | 프록시 뒤 서버 IP의 WebRTC 노출 |
|---|---|---|---|---|---|---|---|
| Playwright(대조군) | 데이터센터(Hetzner) | 0/3 | 0/3 | 19 | 3/3 | Timezone spoofed | n/a(프록시 없음, 실제 IP가 보이는 게 정상) |
| Playwright(대조군) | 미국 레지덴셜, 스티키 | 2/3 | 0/3 | 25–27 | 0/3 | Timezone spoofed | 3/3 |
| Patchright | 데이터센터(Hetzner) | 0/3 | 0/3 | 0–6 | 3/3 | Timezone spoofed | n/a(프록시 없음, 실제 IP가 보이는 게 정상) |
| Patchright | 미국 레지덴셜, 스티키 | 3/3 | 0/3 | 8–20 | 0/3 | Timezone spoofed | 3/3 |
| nodriver | 데이터센터(Hetzner) | 0/3 | 0/3 | 0–6 | 3/3 | Timezone spoofed | n/a(프록시 없음, 실제 IP가 보이는 게 정상) |
| nodriver | 미국 레지덴셜, 스티키 | 3/3 | 2/3 | 10–23 | 0/3 | Timezone spoofed | 3/3 |
| Zendriver | 데이터센터(Hetzner) | 0/3 | 0/3 | 0–6 | 3/3 | Timezone spoofed | 차단됨(3/3) |
| Zendriver | 미국 레지덴셜, 스티키 | 3/3 | 0/3 | 8–24 | 0/3 | Timezone spoofed | 차단됨(3/3) |
| Camoufox | 데이터센터(Hetzner) | 3/3 | 3/3 | 17 | 3/3 | Timezone spoofed | n/a(프록시 없음, 실제 IP가 보이는 게 정상) |
| Camoufox | 미국 레지덴셜, 스티키 | 3/3 | 0/3 | 4–14 | 1/3 | 일치(미국 도시 표시) | 0/3 |
레지덴셜 IP가 도움이 된 부분
Pixelscan이 “Proxy detected”를 더 이상 표시하지 않았습니다. 데이터센터 실행에서는 프록시를 쓰지 않았는데도 매번 표시했는데, 호스팅 ASN을 프록시로 읽는 것으로 보입니다. 레지덴셜 풀을 거쳐서는 스티키 실행 15회 중 1회만 표시했습니다. Camoufox는 위치도 일치했습니다. geoip=True 옵션이 출구 IP 기준으로 타임존, 로케일, 위치 정보, WebRTC IP를 설정하기 때문에, Pixelscan은 “Timezone spoofed” 대신 미국 도시를 보여 줬습니다.
오히려 불리했거나 효과가 없던 부분
Fingerprint의 프록시 신호는 세 가지 프록시 모드 전체에서 프록시를 쓴 34회 중 33회 레지덴셜 출구를 프록시 IP로 인식했고, 상당수에는 업체 이름까지 붙였습니다. 우리는 이를 Fingerprint가 레지덴셜 풀에 대한 정보를 보유하고 있다는 뜻으로 해석합니다. 모든 IP가 무언가를 흘렸다는 뜻은 아니며, 특정 사이트가 그 신호로 무엇을 할지에 대해서도 아무것도 말해 주지 않습니다. Chrome 기반 스텔스 도구의 의심 점수는 프록시를 거치자 내려가지 않고 올라갔습니다. Playwright는 19에서 25–27로 올랐고, 단 한 번의 예열 실행은 47점이었습니다.
이 중 일부는 자초한 결과일 가능성이 큽니다. Chrome 기반 도구들은 컨테이너의 UTC 타임존을 그대로 썼기 때문에, 미국 IP에 UTC 시계라는 조합이 프록시를 쓴 모든 실행에서 Pixelscan에 “Timezone spoofed”로 읽혔습니다. Camoufox는 같은 메커니즘을 반대 방향으로 보여 줍니다. 헬싱키 데이터센터 IP에서 GeoIP 조회가 출구를 Europe/Berlin으로 판단한 반면 ipinfo는 헬싱키라고 했습니다. 그러자 Fingerprint는 데이터센터 실행 3회 모두에서 VPN(“timezone mismatch”)과 신뢰도 낮은 데이터센터 프록시를 탐지했고, 같은 IP의 Chrome 도구들은 둘 다 받지 않았습니다. IP와 타임존이 어긋나면 어느 쪽이든 눈에 띕니다.
WebRTC가 프록시 뒤의 서버를 노출했습니다
CreepJS와 Pixelscan은 프록시를 쓴 Playwright, Patchright, nodriver의 모든 실행에서 호스트의 Hetzner 주소를 WebRTC IP로 표시했습니다. HTTP 트래픽은 레지덴셜 출구를 거쳤지만, UDP로 가는 STUN 요청은 그러지 않았습니다. Zendriver는 기본값으로 --webrtc-ip-handling-policy=disable_non_proxied_udp와 --force-webrtc-ip-handling-policy를 넘기며, CreepJS는 Zendriver의 모든 실행에서 WebRTC를 차단됨으로 보고했습니다. Camoufox는 주소를 프록시 출구로 바꿨습니다. WebRTC 차단 자체도 일부 사이트가 점수에 반영할 수 있는 신호입니다. 이 페이지들 중 그것을 감점하는 곳이 있는지는 테스트하지 않았고, Pixelscan은 감점하지 않았습니다. 그 밖에 어떤 불일치를 보는지는 안티봇 시스템이 프록시를 탐지하는 방법을 참고하세요.
IP보다 headless 모드가 더 큰 흔적이었습니다
데이터센터 IP에서 도구별로 headless 1회씩 실행했습니다. 나머지보다 표본이 작지만 방향은 뚜렷합니다.
| 도구(headless, 데이터센터 IP, 1회) | sannysoft 실패 항목 | deviceandbrowserinfo isBot | CreepJS headless | Fingerprint “Bot” | nowsecure.nl |
|---|---|---|---|---|---|
| Playwright(대조군) | CHR_MEMORY:FAIL, HEADCHR_UA:FAIL, WebDriver | 1/1 | 100% | 1/1: You are a bad bot (type: headless_chrome) | 45초 후에도 “Just a moment...”에서 멈춤 |
| Patchright | CHR_MEMORY:FAIL, HEADCHR_UA:FAIL | 1/1 | 67% | 1/1: You are a bad bot (type: headless_chrome) | 45초 후에도 “Just a moment...”에서 멈춤 |
| nodriver | CHR_MEMORY:FAIL, HEADCHR_UA:FAIL | 1/1 | 67% | 1/1: You are a bad bot (type: headless_chrome) | 45초 후에도 “Just a moment...”에서 멈춤 |
| Zendriver | 없음 | 1/1 | 33% | Not detected (1/1) | 45초 후에도 “Just a moment...”에서 멈춤 |
| Camoufox | Chrome | 0/1 | 0% | Not detected (1/1) | 페이지 로드됨 |
Patchright와 nodriver는 headless로 바꾸기만 했는데 deviceandbrowserinfo와 Fingerprint(“bad bot, type: headless_chrome”)에서 깨끗한 상태에서 탐지 상태로 바뀌었습니다. sannysoft에서는 HEADCHR_UA와 CHR_MEMORY 항목이 실패했습니다. Zendriver를 포함한 모든 Chrome 기반 도구가 nowsecure.nl에서 Cloudflare의 “Just a moment...” 페이지에 멈췄습니다. headful에서는 순정 Playwright를 포함해 같은 도구들이 바로 통과했습니다. Zendriver는 Fingerprint의 봇 판정은 피했지만 deviceandbrowserinfo에는 여전히 탐지됐습니다. Camoufox는 이 모든 페이지에서 headless와 headful이 똑같이 보였습니다.
운영상 발견 두 가지: 스티키 세션과 백그라운드 트래픽
동시 첫 연결이 스티키 세션을 깨뜨렸습니다
스티키 세션 ID를 써도 Chrome 기반 실행은 첫 페이지에서는 한 IP를, 몇 페이지 뒤에는 다른 IP를 보이는 경우가 많았습니다. 스티키 실행 12회 중 11회에서 그랬습니다. Camoufox는 3회 모두 IP 하나를 유지했는데, 아마 GeoIP 조회가 브라우저 시작 전에 요청 하나를 보내기 때문일 것입니다. curl로도 재현했습니다. 새 세션 ID로 동시 요청 6개를 보내자 두 번의 시도에서 각각 3개, 5개의 서로 다른 IP가 나왔습니다. 그 뒤의 순차 요청은 하나의 IP를 유지했습니다. 브라우저 실행 전에 순차 예열 요청 하나를 추가하자 4회 모두 문제가 해결됐습니다. 또 첫 프록시 패스에서는 게이트웨이가 인식하지 못하는 세션 파라미터를 써서 모든 연결이 조용히 새 IP를 받았습니다. 그 패스는 위에서 로테이팅 조건으로 보고했습니다. 두 경우 모두의 교훈은 같습니다. 스티키 세션을 믿기 전에 순차 요청 두 번으로 확인하세요.
일부 실제 Chrome 구성은 프록시를 통해 백그라운드 다운로드를 합니다
종량제 레지덴셜 트래픽 기준으로, 이 9개 페이지를 한 번 도는 데 Playwright는 약 7MB, Patchright는 약 8MB(한 번은 12MB), Camoufox는 9–11MB를 썼습니다. nodriver와 Zendriver는 대부분의 실행에서 40–61MB를 썼습니다. 호스트별 바이트를 기록한 실행에서는 그 대부분이 Chrome 백그라운드 다운로드인 optimizationguide-pa.googleapis.com에서 나왔습니다. 약 9MB와 15MB만 쓴 실행도 두 번 있었으니 매번 일어나는 일은 아닙니다. Patchright도 새 프로필로 실제 Chrome을 실행하지만 이런 현상이 없었으므로, 도구가 Chrome을 어떻게 실행하느냐에 달린 문제입니다. GB 단위로 비용을 낸다면 규모를 키우기 전에 호스트별 바이트를 세어 보세요.
이 벤치마크가 보여 주지 않는 것
- 실제 서비스의 안티봇 테스트가 아닙니다. 이 페이지들은 주로 브라우저 쪽 흔적을 검사합니다. 상용 시스템은 여기에 TLS·HTTP/2 핑거프린트, 행동, IP 이력, 사이트별 모델을 더합니다. 여기서 깨끗한 결과는 필요조건일 뿐 충분조건이 아닙니다. 이 페이지들이 거의 다루지 않는 층위 하나는 TLS 핑거프린팅 가이드에서 다룹니다.
- 표본이 작습니다. 셀당 3회, 하루, 핀란드의 데이터센터 호스트 하나, 프록시 국가 하나입니다. Pixelscan처럼 한 번 뒤집힌 결과는 노이즈와 구분할 수 없습니다.
- 상호작용이 없었습니다. 클릭, 스크롤, 폼, 마우스 경로가 없었으므로 행동 기반 탐지는 시험되지 않았습니다.
- 일부 검사는 이력을 저장합니다. Fingerprint playground는 기기별 이력(“1 IP, 2 Linked IDs in the past 24 hours”)을 보여 주므로, 반복 방문이 이후 점수에 영향을 줬을 수 있습니다.
- 기본값을 조정하지 않았습니다. Chrome 기반 도구는 UTC와 en-US를 그대로 썼고, Camoufox는 Linux로 제한했으며, 각 README의 기본값 이상으로는 아무것도 강화하지 않았습니다. 조정한 구성이라면 결과가 다를 수 있습니다. 그것은 다음에 테스트할 내용이지, 이번에 측정한 것이 아닙니다.
- 여러분의 대상 사이트는 다를 것입니다. 데이터센터와 레지덴셜 중 무엇을 쓸지는 무엇을 어디서 수집하느냐에 달려 있습니다. 비용 측면은 스크래핑용 레지덴셜 vs 데이터센터에서 다룹니다. 실제 사이트에 이런 테스트를 돌리기 전에 robots.txt, 사이트 약관, 개인정보 관련 규정을 확인하세요.
재현 방법
테스트 하네스는 약 260줄의 Python, 80줄짜리 릴레이, 그리고 저장된 페이지 텍스트만 읽는 파서로 구성됩니다. 아래는 우리 데이터에서 깨끗하게 통과한 Patchright 설정으로, 앞에서 다룬 두 가지 수정이 들어 있습니다. Chrome이 동시 연결을 열기 전에 스티키 세션을 고정하는 예열 요청, 그리고 Zendriver의 WebRTC 플래그입니다. 미국 레지덴셜 출구로 deviceandbrowserinfo에 한 번 실행해 봤고("isBot": false), 이 확인은 54회 실행에 포함되지 않습니다.
import asyncio, os, tempfile, urllib.parse, urllib.request, uuid
from patchright.async_api import async_playwright
USER = os.environ["PROXY_USER"] # 프록시 사용자 이름
PASS = os.environ["PROXY_PASS"] # 프록시 비밀번호, 절대 하드코딩하지 말 것
GATE = "gate.proxyhat.com:8080"
def sticky_username() -> str:
# 브라우저 실행마다 새 세션 하나, -sid-가 연결 간 출구 IP를 유지
return f"{USER}-country-US-sid-{uuid.uuid4().hex[:10]}-ttl-30m"
def warm_up(username: str) -> str:
# Chrome이 동시 연결을 열기 전에 세션을 고정하는 순차 요청 하나
url = f"http://{urllib.parse.quote(username)}:{urllib.parse.quote(PASS)}@{GATE}"
opener = urllib.request.build_opener(urllib.request.ProxyHandler({"http": url, "https": url}))
return opener.open("https://api.ipify.org", timeout=30).read().decode()
async def main():
username = sticky_username()
print("session pinned to", warm_up(username))
async with async_playwright() as p:
ctx = await p.chromium.launch_persistent_context(
user_data_dir=tempfile.mkdtemp(),
channel="chrome", # Patchright README 권장대로 실제 Google Chrome
headless=False, # 서버에서는 Xvfb 아래에서 실행
no_viewport=True,
proxy={"server": f"http://{GATE}", "username": username, "password": PASS},
args=["--webrtc-ip-handling-policy=disable_non_proxied_udp",
"--force-webrtc-ip-handling-policy"],
)
page = await ctx.new_page()
await page.goto("https://deviceandbrowserinfo.com/are_you_a_bot")
await page.wait_for_function("document.body.innerText.includes('\"isBot\"')", timeout=45000)
print(await page.evaluate("document.body.innerText.match(/\"isBot\": (true|false)/)[0]"))
await ctx.close()
asyncio.run(main())
사용자 이름 접미사는 ProxyHat 문법입니다(타기팅은 -country-US, 스티키 세션은 -sid-…-ttl-30m). 사용하는 업체의 형식으로 바꾸세요. 긴 실행 전에 출구를 확인하려면 무료 프록시 체커로 사이트에 보이는 정보를 볼 수 있습니다. 우리와 같은 방식으로 셀을 채점하려면:
- 실행마다 새 브라우저를 띄우고, 페이지마다 새 탭으로 방문한 뒤, 페이지 자체의 판정 텍스트가 화면에 나올 때까지 기다리세요. 고정 sleep은 쓰지 마세요. CreepJS는 계속 계산을 하므로 점수가 나타난 뒤 6초를 더 줬습니다.
- 탭을 닫기 전에
document.body.innerText와 전체 페이지 스크린샷을 저장하세요. - 페이지가 출력한 것만 파싱하세요. 예를 들어 deviceandbrowserinfo의
"isBot"JSON, BrowserScan의Normal/Robot쌍, CreepJS의 “N% headless”입니다. 그 외에는 모두 “unparsed”입니다. - 시작 시 출구 IP를 기록하고 마지막 페이지가 보여 주는 IP와 비교하세요. 다르다면 세션이 스티키하지 않았던 것입니다.
- 실행마다 프록시 바이트를 세서, 청구서보다 먼저 백그라운드 트래픽을 발견하세요.
이 결과를 어떻게 활용할까
브라우저부터 고치세요. 순정 Playwright는 두 IP 모두에서 잡혔고, 스텔스 도구 4종은 headful에서 어느 IP로도 잡히지 않았습니다. headless가 대상 사이트를 통과한다는 것을 직접 측정하지 않았다면 headful로 실행하세요(서버에서는 Xvfb). 그다음 네트워크 쪽 이야기를 일관되게 맞추세요. 타임존을 출구 IP에 맞추고, WebRTC를 차단하거나 프록시로 보내고, 스티키 세션이 정말 스티키한지 확인하세요. 레지덴셜 프록시는 그 뒤에 추가하되, 프록시를 인식하는 벤더들은 어차피 많은 레지덴셜 IP를 알아볼 것이라고 예상하세요. IP는 처음부터 데이터센터 트래픽으로 분류되느냐를 결정하고, 브라우저는 봇으로 탐지되느냐를 결정합니다.






