[{"data":1,"prerenderedAt":20},["ShallowReactive",2],{"blog:post:vi:phat-hien-va-xu-ly-403-429-khi-scraping-bang-proxy-xoay-python":3},{"slug":4,"lang":5,"title":6,"summary":7,"date":8,"tags":9,"tag_slugs":15,"thumbnail_url":16,"translations":17,"body":18,"asset_base":19},"phat-hien-va-xu-ly-403-429-khi-scraping-bang-proxy-xoay-python","vi","Phát hiện và xử lý 403/429 khi scraping bằng proxy xoay trong Python","Hướng dẫn thực tế nhận diện lỗi 403, 429 và CAPTCHA khi scraping, thiết lập retry, backoff và xoay proxy trong Python để duy trì pipeline ổn định.","2026-09-24",[10,11,12,13,14],"proxy","scraping","python","retry","rate-limit",[10,11,12,13,14],"https://blog-api.ro-proxy.com/api/blog/posts/phat-hien-va-xu-ly-403-429-khi-scraping-bang-proxy-xoay-python/thumbnail.svg?lang=vi",[5],"## Vì sao 403 và 429 là kẻ thù số một của scraper\n\nKhi scraping quy mô vừa và lớn, hai mã trạng thái xuất hiện nhiều nhất và phá pipeline nhanh nhất là **403 Forbidden** và **429 Too Many Requests**. 403 thường nghĩa là target đã chặn IP hoặc phát hiện dấu hiệu bot qua fingerprint. 429 nghĩa là bạn đang vượt rate limit của endpoint. Cả hai đều có thể xuất hiện xen kẽ: ban đầu 429, sau khi retry liên tục thì chuyển thành 403 (IP bị block hẳn).\n\nProxy xoay giúp giảm xác suất bị chặn, nhưng nếu bạn không xử lý response đúng cách, proxy pool sẽ nhanh chóng cạn kiệt IP sạch và pipeline sập.\n\nBài này đi sâu vào một workflow cụ thể: nhận diện 403/429, quyết định retry hay bỏ, chọn proxy thay thế, và backoff hợp lý — toàn bộ bằng Python.\n\n## Nguyên tắc cốt lõi: không retry mù quáng\n\nNhiều scraper mới dùng vòng lặp `while` retry đến khi thành công. Đây là lỗi phổ biến. Nguyên tắc nên theo:\n\n- **403 không phải tạm thời**: nếu một IP bị 403, retry ngay với cùng IP đó gần như chắc chắn vẫn 403. Cần đổi IP.\n- **429 có thể tạm thời**: server báo bạn đang quá nhanh. Có thể retry sau khi chờ, nhưng tốt hơn là giảm tốc độ và đổi IP.\n- **Retry quá nhiều = tự sát**: nếu target đang bảo trì hoặc chặn toàn ASN, retry 100 lần chỉ tốn proxy và bandwidth.\n\n### Quy tắc retry đề xuất\n\n| Mã | Hành động | Số lần retry tối đa |\n|---|---|---|\n| 429 | Backoff + đổi proxy | 3 |\n| 403 | Đổi proxy ngay, không backoff dài | 2 |\n| 5xx | Backoff ngắn, giữ proxy | 2 |\n| 200 | Thành công | 0 |\n| Khác (3xx, 4xx khác) | Không retry | 0 |\n\n## Thiết lập proxy pool với RoProxy\n\nGiả sử bạn dùng RoProxy rotating residential. Endpoint có dạng:\n\n```\nhttp://USER:PASS@gw.roproxy.com:PORT\n```\n\nVới rotating, mỗi request mới tự động nhận IP khác. Nhưng để kiểm soát tốt hơn, ta sẽ duy trì danh sách endpoint theo vùng và xoay thủ công khi gặp lỗi.\n\n```python\nimport itertools\nimport random\n\nPROXY_ENDPOINTS = [\n    \"http://user:pass@gw.roproxy.com:8000\",  # US\n    \"http://user:pass@gw.roproxy.com:8001\",  # EU\n    \"http://user:pass@gw.roproxy.com:8002\",  # APAC\n]\n\nproxy_cycle = itertools.cycle(PROXY_ENDPOINTS)\n\ndef next_proxy():\n    return next(proxy_cycle)\n```\n\nLý do dùng `itertools.cycle` thay vì random thuần: cycle đảm bảo phân phối đều, tránh \"bốc\" liên tục vào cùng một endpoint đang bị chặn.\n\n## Hàm fetch có retry thông minh\n\nĐây là phần trọng tâm. Ta kết hợp `requests` với logic retry dựa trên mã trạng thái.\n\n```python\nimport requests\nimport time\nfrom requests.adapters import HTTPAdapter\nfrom urllib3.util.retry import Retry\n\nMAX_RETRIES_429 = 3\nMAX_RETRIES_403 = 2\nBASE_BACKOFF = 2  # giây\n\ndef fetch_with_smart_retry(url, headers=None, timeout=15):\n    headers = headers or {\n        \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\",\n        \"Accept-Language\": \"en-US,en;q=0.9\",\n    }\n\n    retry_429 = 0\n    retry_403 = 0\n\n    while True:\n        proxy = next_proxy()\n        proxies = {\"http\": proxy, \"https\": proxy}\n\n        try:\n            resp = requests.get(url, headers=headers, proxies=proxies, timeout=timeout)\n        except requests.exceptions.RequestException as e:\n            print(f\"[NETWORK] {e} — đổi proxy, retry sau 1s\")\n            time.sleep(1)\n            continue\n\n        if resp.status_code == 200:\n            return resp\n\n        if resp.status_code == 429:\n            retry_429 += 1\n            if retry_429 > MAX_RETRIES_429:\n                raise Exception(f\"Vượt giới hạn retry cho 429 tại {url}\")\n            wait = BASE_BACKOFF * (2 ** (retry_429 - 1)) + random.uniform(0, 1)\n            print(f\"[429] retry {retry_429}/{MAX_RETRIES_429}, chờ {wait:.1f}s, đổi proxy\")\n            time.sleep(wait)\n            continue\n\n        if resp.status_code == 403:\n            retry_403 += 1\n            if retry_403 > MAX_RETRIES_403:\n                raise Exception(f\"Vượt giới hạn retry cho 403 tại {url}\")\n            print(f\"[403] đổi proxy ngay, retry {retry_403}/{MAX_RETRIES_403}\")\n            time.sleep(0.5)\n            continue\n\n        # Các mã khác: không retry\n        return resp\n```\n\n### Tại sao backoff có jitter\n\n`random.uniform(0, 1)` thêm vào thời gian chờ giúp tránh \"thundering herd\": nếu nhiều worker cùng bị 429 và cùng retry đúng giây nguyên, server sẽ lại bị quá tải. Jitter nhỏ khiến các worker rải ra.\n\n### Tại sao 403 không backoff dài\n\n403 thường là chặn IP, không phải quá tải. Chờ lâu không giúp gì. Đổi IP và retry nhanh là đúng chiến thuật. Nếu vẫn 403 sau 2 lần đổi IP, khả năng cao target đang chặn theo ASN hoặc fingerprint — cần dừng và xem xét strategy khác (đổi User-Agent, dùng residential thay datacenter, hoặc bỏ URL này).\n\n## Phát hiện CAPTCHA ngầm trong response 200\n\nMột bẫy nguy hiểm: server trả 200 nhưng body là trang CAPTCHA, không phải dữ liệu thật. Nếu scraper chỉ check status code, nó sẽ \"nghĩ\" thành công và lưu rác.\n\n```python\ndef is_captcha_page(html: str) -> bool:\n    signals = [\n        \"cf-challenge\",\n        \"captcha\",\n        \"recaptcha\",\n        \"hcaptcha\",\n        \"are you human\",\n        \"verify you are human\",\n    ]\n    lower = html.lower()\n    return any(s in lower for s in signals)\n\ndef fetch_clean(url, **kw):\n    resp = fetch_with_smart_retry(url, **kw)\n    if resp.status_code == 200 and is_captcha_page(resp.text):\n        raise Exception(f\"CAPTCHA phát hiện trong body 200 tại {url}\")\n    return resp\n```\n\nKhi gặp CAPTCHA, không nên retry vô hạn. Tùy workflow:\n\n- Bỏ URL, ghi log, quay lại sau với proxy vùng khác.\n- Chuyển sang headless browser (Playwright/Puppeteer) có hỗ trợ proxy, để vượt challenge JS.\n- Gửi URL vào hàng đợi CAPTCHA riêng, xử lý bằng dịch vụ giải CAPTCHA nếu nghiệp vụ cho phép.\n\n## Giới hạn tốc độ chủ động\n\nXử lý 429 tốt hơn là phòng tránh nó. Dù có proxy xoay, bạn vẫn nên giữ tốc độ request hợp lý mỗi IP.\n\n```python\nfrom collections import defaultdict\nimport time\n\nlast_request_time = defaultdict(float)\nMIN_INTERVAL_PER_PROXY = 1.5  # giây giữa 2 request cùng endpoint\n\ndef fetch_rate_limited(url, **kw):\n    proxy = next_proxy()\n    elapsed = time.time() - last_request_time[proxy]\n    if elapsed \u003C MIN_INTERVAL_PER_PROXY:\n        time.sleep(MIN_INTERVAL_PER_PROXY - elapsed)\n    last_request_time[proxy] = time.time()\n\n    proxies = {\"http\": proxy, \"https\": proxy}\n    # ... gọi requests.get như trên\n```\n\nLý do: dù rotating cho IP mới mỗi request, endpoint gateway vẫn là cùng một host:port. Một số target chặn theo dải IP của gateway, không phải từng IP. Giữ khoảng cách giữa các request qua cùng endpoint giảm rủi ro.\n\n## Ví dụ thực tế: scraping danh sách sản phẩm\n\nGiả sử bạn cần scrape 500 URL sản phẩm từ một site e-commerce. Workflow đầy đủ:\n\n```python\nimport json\n\nurls = [f\"https://example-shop.com/product/{i}\" for i in range(1, 501)]\nresults = []\nerrors = []\n\nfor url in urls:\n    try:\n        resp = fetch_clean(url)\n        # parse dữ liệu...\n        results.append({\"url\": url, \"status\": resp.status_code, \"length\": len(resp.text)})\n    except Exception as e:\n        errors.append({\"url\": url, \"error\": str(e)})\n        print(f\"[FAIL] {url}: {e}\")\n\n    # Nghỉ giữa các URL để giảm áp lực tổng thể\n    time.sleep(0.8)\n\nprint(f\"Thành công: {len(results)}, Lỗi: {len(errors)}\")\nwith open(\"errors.json\", \"w\") as f:\n    json.dump(errors, f, ensure_ascii=False, indent=2)\n```\n\n### Tại sao cần file errors.json\n\nKhông bao giờ scrape mà không ghi lại lỗi. File lỗi cho phép bạn:\n\n- Phân tích pattern: liệu 403 tập trung ở một vùng proxy nhất định?\n- Retry chỉ các URL thất bại trong lần chạy sau, tiết kiệm thời gian.\n- Phát hiện sớm nếu target mới đổi anti-bot.\n\n## Dấu hiệu cần đổi strategy\n\nRetry và xoay proxy không giải quyết mọi thứ. Nếu bạn thấy các dấu hiệu sau, cần thay đổi cách tiếp cận:\n\n- **Tỷ lệ 403 > 30%**: có thể target chặn theo ASN. Chuyển sang residential proxy từ nhà cung cấp khác hoặc dùng mobile proxy.\n- **429 liên tục dù đã giảm tốc**: target có rate limit rất chặt, cần phân tán theo thời gian (queue + scheduler) thay vì chạy song song.\n- **CAPTCHA xuất hiện > 10%**: fingerprint đang bị phát hiện. Cần dùng headless browser thật, chỉnh timezone, ngôn ngữ, và Accept-Language khớp với vùng IP.\n- **Timeout nhiều**: endpoint proxy đang quá tải. Giảm concurrency hoặc thêm endpoint dự phòng.\n\n## Kết luận\n\nXử lý 403 và 429 không phải thêm vài dòng `try/except`. Nó là một chiến thuật có cấu trúc: nhận diện đúng mã, quyết định retry hay bỏ, chọn proxy thay thế, backoff có jitter, và giới hạn tốc độ chủ động. Khi kết hợp với proxy xoay chất lượng từ RoProxy, pipeline scraping của bạn sẽ bền bỉ hơn nhiều và tốn ít IP sạch hơn — thay vì \"đốt\" proxy rồi mới tìm cách khắc phục.\n","https://blog-api.ro-proxy.com/api/blog/posts/phat-hien-va-xu-ly-403-429-khi-scraping-bang-proxy-xoay-python/assets",1790230401117]