Scarnox · Blog

การจัดการข้อผิดพลาดของ captcha solver: การลองใหม่ backoff และสิ่งที่ไม่ควรลองซ้ำ

การแก้ captcha คือการเรียกเครือข่ายไปยังระบบที่คุณไม่ได้ควบคุม เพื่อท้าทายที่อาจเปลี่ยนโดยไม่แจ้ง การจัดการความล้มเหลวให้ดีคือความแตกต่างระหว่างไปป์ไลน์ที่ลดระดับลงอย่างสง่างามกับไปป์ไลน์ที่เสียเวลาไปกับพายุการลองซ้ำ

กับ Scarnox ค่าใช้จ่ายช่วยคุณอยู่แล้ว การแก้ที่ล้มเหลวไม่มีค่าใช้จ่าย ดังนั้นการลองซ้ำมีต้นทุนแค่เวลา แต่การลองซ้ำความล้มเหลวที่ผิดประเภทก็ยังเสียเวลานั้นไป และการลองซ้ำหนักเกินไปอาจก่อปัญหาของตัวเอง คู่มือนี้ให้นโยบายการลองซ้ำที่คุณนำไปวางในงาน Python ได้

Scarnox แก้ PopularCaptcha ได้ในวันนี้ และการจัดการข้อผิดพลาดด้านล่างใช้ได้กับ captcha solver ทุกตัว นโยบายจึงไม่ขึ้นอยู่กับประเภทของ captcha

ตรวจ errorId ก่อนเสมอ

การตอบกลับการแก้มีฟิลด์ errorId 0 หมายถึงสำเร็จ และ token อยู่ภายใต้ solution.token ค่าอื่นใดหมายถึงการแก้ล้มเหลว การตอบกลับที่ล้มเหลวจะมี errorCode และ errorDescription ด้วย ให้บันทึกทั้งสองค่า เพราะโค้ดคือสิ่งที่คุณจะค้นหาภายหลัง และคำอธิบายคือสิ่งที่คุณจะอ่านเมื่อการรันมีปัญหา

{"errorId": 1, "errorCode": "...", "errorDescription": "..."}

อย่าสมมติว่าการตอบกลับเป็นความสำเร็จเพียงเพราะสถานะ HTTP เป็น 200 ตรวจ errorId ทุกครั้ง

ความล้มเหลวที่ควรลองใหม่

ความล้มเหลวบางอย่างเป็นการชั่วคราว ให้ลองใหม่ด้วย exponential backoff และการหน่วงสุ่มเล็กน้อย

ความล้มเหลวเหล่านี้คุ้มค่าแก่การลองใหม่ เพราะการลองครั้งถัดไปอาจสำเร็จโดยที่คุณไม่ต้องเปลี่ยนอะไร

ความล้มเหลวที่ต้องแก้ก่อนลองใหม่

ความล้มเหลวอื่นจะไม่หายไปด้วยการรอ การลองใหม่ด้วยอินพุตเดิมจะล้มเหลวแบบเดิม ให้แก้สิ่งเหล่านี้ก่อน

เมื่อความล้มเหลวไม่อยู่ในรายการใดเลย ให้ถือว่าเป็นความล้มเหลวประเภทต้องแก้ก่อน ดูข้อความ error ตัดสินใจ และเพิ่มโค้ดลงในรายการที่เหมาะสมในโค้ดของคุณ รักษารายการให้สั้นและผ่านการทบทวน

นโยบายที่นำไปวางได้

import os
import random
import time

import requests

API_URL = "https://api.scarnox.com/api/tasks/create"
HEADERS = {"Authorization": f"Bearer {os.environ['SCARNOX_KEY']}"}
MAX_ATTEMPTS = 4

class SolveError(Exception):
    def __init__(self, code: str, description: str, retryable: bool):
        super().__init__(f"{code}: {description}")
        self.code = code
        self.retryable = retryable

def solve_once(payload: dict) -> str:
    try:
        r = requests.post(API_URL, headers=HEADERS, json=payload, timeout=185)
    except requests.RequestException as exc:
        # ข้อผิดพลาดของเครือข่ายหรือ timeout ฝั่งเรา: ลองใหม่ได้อย่างปลอดภัย
        raise SolveError("NETWORK", str(exc), retryable=True)

    if r.status_code == 429 or r.status_code >= 500:
        raise SolveError(f"HTTP_{r.status_code}", r.text[:200], retryable=True)

    try:
        data = r.json()
    except ValueError:
        raise SolveError(f"HTTP_{r.status_code}", r.text[:200], retryable=False)

    if data.get("errorId") == 0:
        return data["solution"]["token"]

    # API รายงานความล้มเหลว ลองใหม่เฉพาะโค้ดที่เคยเห็นว่าเป็นการชั่วคราวเท่านั้น
    code = data.get("errorCode", "UNKNOWN")
    retryable = code in TRANSIENT_CODES
    raise SolveError(code, data.get("errorDescription", ""), retryable=retryable)

# เพิ่มรหัสข้อผิดพลาดของ API ที่นี่ต่อเมื่อเห็นใน log แล้วว่าเป็นชั่วคราวเท่านั้น
TRANSIENT_CODES: set[str] = set()

def solve(payload: dict) -> str:
    for attempt in range(MAX_ATTEMPTS):
        try:
            return solve_once(payload)
        except SolveError as err:
            last_attempt = attempt == MAX_ATTEMPTS - 1
            if not err.retryable or last_attempt:
                raise
            delay = min(30, 2 ** attempt) + random.random()  # รอนานขึ้นทีละขั้น + ค่าสุ่มเล็กน้อย
            print(f"attempt {attempt + 1} failed ({err.code}), retrying in {delay:.1f}s")
            time.sleep(delay)
    raise RuntimeError("unreachable")

นโยบายนี้ไม่ลองซ้ำความล้มเหลวประเภทต้องแก้ก่อน และหยุดหลังจำนวนครั้งที่กำหนดไว้ ชุด TRANSIENT_CODES เริ่มต้นว่างโดยตั้งใจ: เพิ่มโค้ดเฉพาะเมื่อ log ของคุณแสดงว่าโค้ดนั้นเป็นการชั่วคราวแล้วเท่านั้น

ทำไม jitter จึงสำคัญ

หากผู้ทำงานห้าสิบคนล้มเหลวพร้อมกันและทุกคนหลับสองวินาทีพอดี พวกเขาจะลองใหม่พร้อมกันและล้มเหลวพร้อมกันอีกครั้ง การสุ่มเศษวินาทีเล็กน้อยช่วยกระจายพวกเขาออกไป นี่คือคำแนะนำเดียวกับที่ผู้ให้บริการ API รายใหญ่หลายรายมอบให้ไคลเอนต์ของตนเอง

timeout

ตั้ง timeout ของ HTTP ให้สูงกว่าเวลาการแก้ปกติมาก ประมาณ 185 วินาที คำขอจะบล็อกจนกว่า token จะพร้อม ดังนั้น timeout ฝั่ง client ที่สั้นอาจเปลี่ยนความสำเร็จที่ช้าให้เป็นความล้มเหลวที่คุณลองใหม่โดยเปล่าประโยชน์ timeout ของ client ที่ยาวกว่าการแก้คือการตั้งค่าที่ปลอดภัย

token แคชไม่ได้

token หมดอายุภายในประมาณสองนาที อย่าแคช token ข้ามการรัน และอย่าใช้ token เดิมซ้ำสำหรับการ submit ครั้งที่สอง หาก submit ล้มเหลวหลังจากใช้ token ไปแล้ว ให้แก้ใหม่ ไม่ใช่ส่ง token เก่าซ้ำ คู่มือการหมดอายุของ token อธิบายจังหวะเวลาอย่างละเอียดกว่า

การทำงานพร้อมกันและ backoff ข้ามผู้ทำงาน

หากผู้ทำงานหลายคนใช้ key เดียวกัน backoff ของผู้ทำงานคนหนึ่งจะไม่ชะลอคนอื่น เพิ่มขีดจำกัดร่วมของการแก้พร้อมกัน เช่น semaphore หรือ queue เพื่อไม่ให้การลองซ้ำที่พุ่งพรวดกลายเป็นคำขอที่พุ่งพรวด การจำกัดการทำงานพร้อมกันอย่างง่ายแก้ความล้มเหลวจากการจำกัดอัตราได้ส่วนใหญ่ก่อนจะเกิดขึ้น

การบันทึกที่มีประโยชน์

สำหรับทุกความพยายามที่ล้มเหลว ให้บันทึก errorCode หมายเลขความพยายาม sitekey โฮสต์ของหน้า และว่าความล้มเหลวนั้นถูกลองใหม่หรือไม่ อย่าบันทึก API key หรือรหัสผ่านของ proxy แดชบอร์ดที่นับโค้ดต่อชั่วโมงบอกคุณได้เร็วว่าปัญหาอยู่ที่โค้ดของคุณ proxy หรือบริการ

ขั้นตอนถัดไป

ทดสอบการจัดการข้อผิดพลาดของคุณกับการทดลองใช้ฟรี: สร้างบัญชี และเริ่มด้วยเครดิตทดลองใช้ฟรี ไม่ต้องใช้บัตร