캡차 토큰 만료: 원인과 해결 방법
캡차 토큰은 챌린지를 통과했다는 것을 증명하는 수명이 짧은 증거입니다. Scarnox의 PopularCaptcha 토큰은 발급 후 약 2분 뒤에 만료됩니다. 그 시간이 지난 뒤 토큰을 제출하면, 사이트는 보통 캡차가 만료되었거나 유효하지 않다는 메시지와 함께 폼을 거부합니다. 해결책은 거의 항상 타이밍에 관한 것입니다. 제출 직전에 최대한 가깝게 캡차를 풀고, 각 토큰은 한 번만 사용하고, 첫 시도가 받아들여지지 않았다는 것을 확실히 알 때만 재시도하세요.
토큰이 만료되는 이유
토큰은 영구적인 자격 증명이 아닙니다. 사이트는 사람(또는 그를 대신하는 솔버)이 최근에 챌린지를 완료했는지 알고 싶어 하므로, 토큰에 짧은 수명을 줍니다. 시계는 여러분이 API를 처음 호출한 시점이 아니라 토큰이 발급된 시점부터 시작됩니다. 풀이와 제출 사이에 일어나는 모든 일이 그 2분을 갉아먹습니다.
흔한 원인
- 너무 일찍 풀이하기. 토큰을 한 묶음 미리 풀어서 나중을 위해 보관하는 것이 가장 흔한 원인입니다. 토큰을 사용할 무렵에는 이미 오래되어 있습니다.
- 풀이와 제출 사이의 느린 코드. 긴 데이터베이스 쓰기, 큐, 또는 폼 게시 전에 놓인
time.sleep이 토큰을 기간 밖으로 밀어낼 수 있습니다. - 토큰 재사용. 이미 받아들여졌거나 실패한 시도에서 이미 사용된 토큰은 다시 동작하지 않습니다. 각 토큰은 한 번만 쓸 수 있다고 생각하세요.
- 페이지 새로고침. 풀이 후 페이지를 새로고침하면, 폼이 새 챌린지를 발급해 가지고 있는 토큰을 무효화할 수 있습니다.
해결책: 제출 직전에 풀이하기
가장 단순하고 믿을 만한 패턴은 풀이와 제출을 한 단계로 묶는 것입니다. 토큰을 요청하고, 폼에 넣고, 폼을 POST하며, 그 사이에는 느린 작업을 하지 않습니다.
import requests
API_KEY = "scarnox_YOURAPIKEY"
CREATE_URL = "https://api.scarnox.com/api/tasks/create"
def fresh_token(sitekey: str, page_url: str) -> str:
r = requests.post(
CREATE_URL,
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"captcha_type": "PopularCaptchaTaskProxyless",
"site_key": sitekey,
"site_url": page_url,
},
timeout=185, # 토큰이 준비될 때까지 호출이 블록됩니다
)
data = r.json()
if data.get("errorId") != 0:
raise RuntimeError(f"{data.get('errorCode')}: {data.get('errorDescription')}")
return data["solution"]["token"]
def submit_form(page_url: str, sitekey: str, form: dict) -> requests.Response:
token = fresh_token(sitekey, page_url) # 게시 직전에 풀이합니다
return requests.post(
page_url,
data={**form, "h-captcha-response": token, "g-recaptcha-response": token},
timeout=30,
)
submit_form은 토큰을 캐시나 큐에 보관하지 않는다는 점에 주목하세요. 토큰을 하나 요청하고, 게시하고, 넘어갑니다. 실패한 풀이는 청구되지 않으므로, 실패한 요청도 풀이 쪽에서는 비용이 들지 않습니다.
안전하게 재시도하기
일부 사이트는 만료된 토큰을 분명한 응답으로 거부합니다. 다른 사이트는 오류 메시지가 들어 있는 평범한 페이지를 돌려주므로, 상태 코드만으로는 알 수 없습니다. 만료된 토큰에 대해 사이트가 실제로 돌려주는 응답을 살펴보고, 그에 맞춰 검사를 만드세요. 재시도 루프는 다음과 같을 수 있습니다.
def submit_with_retry(page_url: str, sitekey: str, form: dict, attempts: int = 2):
for _ in range(attempts):
resp = submit_form(page_url, sitekey, form)
if resp.status_code < 400 and "captcha" not in resp.text.lower():
return resp # 이 사이트의 응답 기준으로 받아들여졌습니다
raise RuntimeError("captcha token rejected after retries")
무언가를 만드는 폼에서의 재시도는 조심하세요. 첫 시도가 실제로 받아들여졌는데 코드가 응답을 잘못 읽었다면, 재시도가 중복 계정, 주문, 메시지를 만들 수 있습니다. 첫 게시가 받아들여지지 않았다고 확신할 때만 재시도하고, 검사는 작업 중인 사이트에 맞게 구체적으로 만드세요.
타이밍을 정확하게 유지하기
- 나중에 쓸 목적으로 토큰을 한 묶음 풀지 마세요. 제출마다 풀이하세요.
- 풀이와 게시 사이의 작업은 최소로 유지하세요.
- 풀이 요청의 타임아웃은 약 185초로 설정하세요. 호출은 토큰이 준비될 때까지 블록됩니다. 내 폼 게시에 거는 별도의 타이머는 토큰 수명보다 훨씬 짧아야 합니다.
- 파이프라인을 더 빠르게 만들어야 한다면 캡차 풀이 속도를 보세요. 토큰을 저장하지 않고 병렬로 풀이하는 방법을 다룹니다.
흔한 실수
- 토큰을 딕셔너리나 Redis에 나중을 위해 캐시하기. 캐시는 무해해 보이지만, 토큰이 기간을 벗어난 뒤에 사용되면 모든 제출이 실패합니다.
- 세션당 한 번만 풀이하기. 오래 유지되는 세션도 폼 게시마다 새 토큰이 필요합니다. 게시마다 별도의 챌린지 검사가 실행될 수 있기 때문입니다.
- 풀이와 제출 사이에 sleep 두기. 풀이 뒤에 놓인 속도 제한기나 의도적인 멈춤은 토큰의 수명을 소모합니다. 대신 풀이 전에 속도 조절을 두세요.
- 모든 실패를 똑같이 취급하기. 내 쪽의 타임아웃, 거부된 토큰, 사이트 오류는 각각 다르게 처리해야 합니다. 따로 기록하세요.
문제를 진단할 수 있도록 충분히 기록하기
제출이 실패하면 토큰이 발급된 시간, 폼이 게시된 시간, HTTP 상태, 응답의 처음 몇백 자를 기록하세요. 이 네 가지 값이 있으면 토큰이 오래되어 만료되었는지, 아니면 사이트가 다른 이유로 거부했는지 한눈에 알 수 있습니다. 공유 로그에는 전체 토큰을 남기지 마세요. 유효한 토큰은 그 챌린지 하나에 대한 자격 증명이기 때문입니다.
그래도 실패한다면 확인할 것
- 토큰이 반환된 시점과 폼이 게시된 시점 사이의 시간을 출력하세요.
site_url이 캡차를 보여 주는 페이지와 일치하는지 확인하세요.- sitekey를 오래된 복사본이 아니라 현재 페이지에서 읽었는지 확인하세요.
- 폼 필드 이름을 확인하세요. PopularCaptcha 토큰은
h-captcha-response에 들어가며, 종종g-recaptcha-response에도 넣습니다.
모든 오류 응답과 각각의 처리 방법은 캡차 솔버 오류 처리에 있습니다.