← Quay lại blog

5 sai lầm trong Scrapy proxy middleware khiến lưu lượng proxy bị lãng phí

Năm lỗi điển hình trong middleware Scrapy, khiến lưu lượng proxy bị lãng phí và trình phân tích bị cấm. Kèm theo ví dụ mã và giải pháp sẵn có.

📅29 tháng 9, 2026

Trình phân tích trên Scrapy gặp lỗi do timeout, bể proxy bị tiêu tốn nhanh hơn nhiều so với dự kiến, và nhật ký bị đầy với các phản hồi 407 và 403 — có phải là một bức tranh quen thuộc? Trong 90% trường hợp, vấn đề không phải ở chính các proxy, mà ở cách viết DownloaderMiddleware. Chúng ta sẽ phân tích năm lỗi thường gặp nhất trong middleware, biến lưu lượng đắt đỏ thành các yêu cầu rác, và chỉ cho bạn cách sửa chữa chúng với mã.

Lỗi 1: xoay vòng nguyên thủy mà không xem xét trạng thái của proxy

Cấu trúc thường gặp nhất mà bạn có thể tìm thấy trong các hướng dẫn là random.choice(PROXY_LIST) bên trong process_request. Vấn đề là xoay vòng như vậy không biết proxy nào vừa bị cấm và proxy nào vẫn còn hoạt động. Kết quả là trình phân tích tiếp tục gửi yêu cầu qua IP đã bị chặn, nhận được 403/429, thực hiện retry — và lại chọn cùng một địa chỉ, vì lựa chọn là ngẫu nhiên và không loại trừ các nút "xấu".

Cách tiếp cận đúng là theo dõi trạng thái của từng proxy: số lượng yêu cầu thành công, số lượng lỗi, thời gian sử dụng cuối cùng. Đây là một phiên bản làm việc tối thiểu:

import random
import time

class ProxyPool:
    def __init__(self, proxies):
        self.proxies = {p: {"fails": 0, "last_used": 0, "banned_until": 0} for p in proxies}

    def get_proxy(self):
        now = time.time()
        available = [
            p for p, state in self.proxies.items()
            if state["banned_until"] < now
        ]
        if not available:
            # nếu tất cả đều bị cấm, đặt lại cấm "cũ" nhất
            available = list(self.proxies.keys())
        return random.choice(available)

    def mark_fail(self, proxy, cooldown=300):
        self.proxies[proxy]["fails"] += 1
        self.proxies[proxy]["banned_until"] = time.time() + cooldown

    def mark_success(self, proxy):
        self.proxies[proxy]["fails"] = 0
        self.proxies[proxy]["last_used"] = time.time()

Bể proxy này loại trừ các IP bị cấm trong thời gian "làm mát" (cooldown) và đưa chúng trở lại sử dụng sau đó. Điều này đã giảm đáng kể lưu lượng, vì bạn không liên tục tấn công cùng một nút bị chặn.

Lỗi 2: xử lý retry và mã trạng thái không đúng

Lỗi điển hình thứ hai là sử dụng RetryMiddleware tiêu chuẩn từ Scrapy mà không sửa đổi. Mặc định, nó sẽ retry yêu cầu trên cùng một proxy mà đã thất bại, trừ khi bạn rõ ràng thêm việc thay đổi proxy trong process_exception. Kết quả là bạn có một bức tranh cổ điển: 3 retry, 3 cấm, yêu cầu vẫn thất bại, và lưu lượng đã bị tiêu tốn.

Một điểm thứ hai là không phải tất cả mã trạng thái đều cần được retry giống nhau. 429 (Quá nhiều yêu cầu) yêu cầu một khoảng dừng và thay đổi IP, 403 thường có nghĩa là cấm proxy cụ thể (cần thay thế ngay lập tức), trong khi 5xx — thường là vấn đề tạm thời từ phía máy chủ, có thể retry trên cùng một proxy. Nếu gom tất cả vào một chỗ, middleware sẽ hoặc quá hung hăng đốt proxy, hoặc chờ quá lâu ở những nơi cần phải thay đổi IP ngay lập tức.

class SmartRetryMiddleware:
    def __init__(self, pool):
        self.pool = pool

    def process_response(self, request, response, spider):
        proxy = request.meta.get("proxy")
        if response.status in (403, 407):
            if proxy:
                self.pool.mark_fail(proxy, cooldown=600)
            new_request = request.copy()
            new_request.meta["proxy"] = self.pool.get_proxy()
            new_request.dont_filter = True
            return new_request

        if response.status == 429:
            if proxy:
                self.pool.mark_fail(proxy, cooldown=120)
            new_request = request.copy()
            new_request.meta["proxy"] = self.pool.get_proxy()
            new_request.dont_filter = True
            return new_request

        if proxy:
            self.pool.mark_success(proxy)
        return response

Ở đây, điều quan trọng là phân loại cooldown theo loại lỗi: cấm cứng (403/407) — khoảng dừng dài, giới hạn tần suất (429) — khoảng dừng ngắn. Điều này tiết kiệm hàng chục phần trăm lưu lượng trong các phiên chạy dài.

Lỗi 3: thiếu phiên sticky cho các trang web có xác thực

Nếu trình phân tích làm việc với một trang web có đăng nhập, giỏ hàng, phân trang với việc lưu trạng thái hoặc captcha kiểm tra theo IP — việc thay đổi proxy cho mỗi yêu cầu sẽ làm hỏng phiên. Trang web thấy rằng yêu cầu số 1 đến từ một IP, trong khi yêu cầu số 2 (trong cùng một phiên cookie) đến từ một IP khác, và điều này ngay lập tức kích hoạt bảo vệ chống bot, ngay cả khi cả hai IP đều "sạch".

Giải pháp là gán một proxy cho một phiên logic (ví dụ, cho một tài khoản cụ thể hoặc cho chuỗi yêu cầu trong cùng một miền) trong một khoảng thời gian cố định, thay vì thay đổi nó cho mỗi yêu cầu. Điều này được gọi là phiên sticky.

class StickySessionMiddleware:
    def __init__(self, pool, ttl=600):
        self.pool = pool
        self.ttl = ttl
        self.sessions = {}  # session_id -> (proxy, expires_at)

    def process_request(self, request, spider):
        session_id = request.meta.get("session_id")
        if not session_id:
            return

        now = time.time()
        session = self.sessions.get(session_id)

        if session and session[1] > now:
            request.meta["proxy"] = session[0]
        else:
            proxy = self.pool.get_proxy()
            self.sessions[session_id] = (proxy, now + self.ttl)
            request.meta["proxy"] = proxy

Đối với các nhiệm vụ cần sự ổn định của IP trong suốt phiên — xác thực, làm việc với tài khoản cá nhân, các biểu mẫu nhiều bước — proxy r résident với hỗ trợ phiên là lựa chọn tốt nhất: chúng cho phép giữ cùng một IP đầu ra trong vài phút hoặc giờ, và sau đó thay đổi nó một cách có kiểm soát, thay vì ngẫu nhiên cho mỗi yêu cầu.

Lỗi 4: truyền xác thực proxy không chính xác

Lỗi thứ tư — kỹ thuật, nhưng xuất hiện trong gần như mọi dự án thứ hai. Các nhà phát triển truyền tên đăng nhập và mật khẩu của proxy trực tiếp trong URL dạng http://user:pass@ip:port thông qua request.meta["proxy"]. Điều này hoạt động trong hầu hết các trường hợp, nhưng khi sử dụng proxy qua đường hầm HTTPS hoặc khi làm việc với một số nhà cung cấp, phương pháp xác thực này không được HttpProxyMiddleware tiêu chuẩn xử lý chính xác, và các yêu cầu bị thất bại với 407 Proxy Authentication Required, mặc dù thông tin xác thực là đúng.

Cách đáng tin cậy hơn là truyền tiêu đề Proxy-Authorization một cách rõ ràng, được mã hóa bằng base64:

import base64

class ProxyAuthMiddleware:
    def process_request(self, request, spider):
        proxy = request.meta.get("proxy")
        if not proxy:
            return

        # proxy không có thông tin xác thực trong URL
        request.meta["proxy"] = proxy

        user = request.meta.get("proxy_user")
        password = request.meta.get("proxy_pass")
        if user and password:
            credentials = f"{user}:{password}"
            encoded = base64.b64encode(credentials.encode()).decode()
            request.headers["Proxy-Authorization"] = f"Basic {encoded}"

Cách tiếp cận này hoạt động ổn định hơn khi mở rộng đến hàng trăm yêu cầu song song và không phụ thuộc vào cách mà phiên bản cụ thể của thư viện phân tích URL với thông tin xác thực nhúng. Điều này đặc biệt quan trọng khi làm việc với proxy di động, nơi xác thực thường phụ thuộc vào danh sách trắng IP hoặc kiểm tra tiêu đề nghiêm ngặt.

Lỗi 5: không có giám sát và ghi nhật ký cấm

Lỗi cuối cùng và có thể là tốn kém nhất — thiếu ghi nhật ký về các proxy bị cấm, tần suất và các miền nào. Nếu không có dữ liệu này, không thể hiểu được điều gì thực sự tiêu tốn lưu lượng: liệu bể proxy đã cạn kiệt trên một trang web cụ thể hay vấn đề nằm ở chính trình phân tích (yêu cầu quá thường xuyên, thiếu độ trễ, tiêu đề đáng ngờ).

Tập hợp tối thiểu các chỉ số mà bạn nên ghi nhật ký trong middleware:

  • Số lượng yêu cầu trên mỗi proxy trong phiên phân tích
  • Số lượng và mã lỗi (403, 407, 429, 5xx) cho mỗi proxy
  • Thời gian sống của proxy trước khi bị cấm đầu tiên
  • Các miền mà cấm xảy ra thường xuyên nhất
import logging
import json

logger = logging.getLogger("proxy_stats")

class ProxyStatsMiddleware:
    def __init__(self):
        self.stats = {}

    def process_response(self, request, response, spider):
        proxy = request.meta.get("proxy", "unknown")
        domain = request.url.split("/")[2]
        key = f"{proxy}|{domain}"

        entry = self.stats.setdefault(key, {"requests": 0, "errors": 0})
        entry["requests"] += 1
        if response.status in (403, 407, 429):
            entry["errors"] += 1

        if entry["requests"] % 50 == 0:
            logger.info(json.dumps(self.stats))

        return response

Nếu không có thống kê như vậy, bất kỳ nỗ lực nào để "tối ưu hóa" middleware đều trở thành đoán mò. Với nó, bạn có thể thấy rõ: nếu, chẳng hạn, một miền cụ thể cấm 80% bể proxy trong 10 yêu cầu đầu tiên — vấn đề không phải ở proxy, mà ở mẫu yêu cầu (không có xoay vòng User-Agent, tần suất quá cao, không có độ trễ giữa các yêu cầu).

Ví dụ hoạt động của middleware hoàn chỉnh

Tập hợp tất cả lại trong settings.py — thứ tự của middleware là rất quan trọng, vì nó quyết định thứ tự áp dụng các kiểm tra:

DOWNLOADER_MIDDLEWARES = {
    "myproject.middlewares.ProxyAuthMiddleware": 350,
    "myproject.middlewares.StickySessionMiddleware": 400,
    "myproject.middlewares.SmartRetryMiddleware": 550,
    "myproject.middlewares.ProxyStatsMiddleware": 900,
}

RETRY_ENABLED = False  # tắt retry tiêu chuẩn, sử dụng của riêng mình
DOWNLOAD_TIMEOUT = 15
CONCURRENT_REQUESTS_PER_DOMAIN = 8

Lưu ý về RETRY_ENABLED = False — điều này là rất quan trọng, nếu không cơ chế retry tích hợp của Scrapy sẽ xung đột với logic thay đổi proxy của bạn, và các yêu cầu sẽ bắt đầu bị trùng lặp hoặc retry hai lần. Cũng quan trọng là giới hạn CONCURRENT_REQUESTS_PER_DOMAIN — tính song song quá cao cho một miền ngay cả với xoay vòng proxy cũng trông đáng ngờ đối với các hệ thống chống bot.

Loại proxy nào nên chọn cho Scrapy

Middleware giải quyết một nửa vấn đề, nửa còn lại là lựa chọn đúng bể proxy cho nhiệm vụ. Dưới đây là sự so sánh cho các kịch bản phân tích điển hình.

Loại proxy Khi nào sử dụng Ưu điểm Nhược điểm
Proxy trung tâm dữ liệu Phân tích hàng loạt các trang mở mà không có bảo vệ chống bot nghiêm ngặt Tốc độ cao, chi phí lưu lượng thấp Dễ bị phát hiện, thường nằm trong danh sách đen
Proxy cư trú Phân tích các chợ, trang web có bảo vệ JS, xác thực IP thực, tỷ lệ cấm thấp, hỗ trợ phiên sticky Tốc độ thấp hơn so với DC
Proxy di động Phân tích các phiên bản di động của trang web và API với bảo vệ chống bot nghiêm ngặt Độ tin cậy tối đa từ các trang web mục tiêu Chi phí lưu lượng cao nhất

Quy tắc thực tiễn: để phân tích các trang tĩnh mà không có bảo vệ mạnh mẽ, proxy trung tâm dữ liệu rẻ sẽ phù hợp với middleware hợp lý từ bài viết này. Nếu trang web sử dụng Cloudflare, PerimeterX, DataDome hoặc các hệ thống tương tự — IP trung tâm dữ liệu sẽ bị cấm gần như ngay lập tức, và lúc này tốt hơn là chuyển sang bể cư trú ngay lập tức, tiết kiệm thời gian cho việc gỡ lỗi middleware.

Danh sách kiểm tra trước khi khởi động trình phân tích vào sản xuất

  • Middleware loại trừ các proxy bị cấm trong thời gian làm mát, chứ không chọn chúng ngẫu nhiên
  • Logic retry phân biệt các loại lỗi (403/407 so với 429 so với 5xx) với thời gian làm mát khác nhau
  • Đối với các nhiệm vụ theo phiên, sử dụng gán sticky cho proxy, chứ không thay đổi cho mỗi yêu cầu
  • Xác thực proxy được truyền qua tiêu đề Proxy-Authorization, chứ không chỉ qua URL
  • Thống kê cấm được ghi lại theo miền và proxy để chẩn đoán vấn đề
  • Middleware Retry tiêu chuẩn của Scrapy đã bị tắt để không xung đột với logic tùy chỉnh
  • Tính song song được giới hạn ở các giá trị hợp lý, chứ không được đặt ở mức tối đa

Kết luận

Hầu hết các vấn đề về tiêu tốn lưu lượng proxy trong Scrapy không được giải quyết bằng cách mua thêm bể IP, mà là sửa chữa logic của middleware: phân loại lỗi theo loại, phiên sticky cho các kịch bản phức tạp, xác thực chính xác và giám sát liên tục các cấm. Mã từ bài viết này có thể được sử dụng làm cơ sở và điều chỉnh cho dự án cụ thể — cấu trúc vẫn hoạt động cho cả các trình phân tích nhỏ và các cài đặt Scrapy-Cluster phân tán.

Nếu middleware đã được cấu hình đúng, nhưng các cấm vẫn xảy ra quá thường xuyên — có thể vấn đề nằm ở chất lượng của chính bể IP. Đối với việc phân tích các trang web có bảo vệ chống bot tiên tiến, bạn nên thử proxy cư trú với hỗ trợ phiên — chúng giảm đáng kể tỷ lệ kích hoạt bảo vệ so với các địa chỉ trung tâm dữ liệu với cùng logic middleware.