Scrape không bị chặn: hướng dẫn thực tế kèm code
Bị chặn khi scraping thường có nghĩa là một trong ba điều: bạn gửi quá nhiều request quá nhanh, client của bạn trông không giống trình duyệt nào hoặc giống một bot đã biết, hoặc địa chỉ IP của bạn nằm trong danh sách mà trang ít tin tưởng hơn. Bạn có thể giảm cả ba bằng vài thói quen: nhận ra chặn sớm, chậm lại khi trang đẩy lùi, giữ một danh tính nhất quán và trung thực, và chỉ dùng proxy ở nơi phù hợp. Hướng dẫn này đi qua từng điểm với code chạy được. Chỉ dùng nó cho các trang bạn được phép scrape.
Nhận biết một lần chặn
Một lần chặn không phải lúc nào cũng trông như trang lỗi. Hãy chú ý:
- 403 Forbidden, thường có nghĩa là trang đã từ chối client của bạn. Dừng lại và kiểm tra xem bạn có được phép ở đó không trước khi thử lại.
- 429 Too Many Requests, nghĩa là bạn đang gửi quá nhiều. Thường đi kèm header
Retry-After. - 503 Service Unavailable, có thể nghĩa là trang đang quá tải. Hãy lùi lại.
- Phản hồi 200 gần như không có nội dung, một chuyển hướng tới trang đăng nhập hoặc thử thách, hoặc một trang thiếu dữ liệu bạn mong đợi.
Hãy ghi log mã trạng thái và một đoạn mẫu ngắn của phần thân cho mọi lỗi. Một lần chặn mà bạn không thấy trong log là một lần chặn bạn không thể sửa.
Lùi lại bằng retry
Khi trang nói "chậm lại", code của bạn nên chậm lại nhiều hơn một chút. Backoff lũy thừa, với giới hạn số lần thử, là cách chuẩn. Nó cũng tôn trọng Retry-After khi trang gửi header đó:
import time
import requests
session = requests.Session()
session.headers.update({
"User-Agent": "ExampleBot/1.0 (+https://example.com/bot)",
"Accept-Language": "en-US,en;q=0.9",
})
def polite_get(url: str, max_attempts: int = 4) -> requests.Response:
delay = 2.0
resp = None
for _ in range(max_attempts):
resp = session.get(url, timeout=30)
if resp.status_code not in (429, 503):
return resp
retry_after = resp.headers.get("Retry-After", "")
wait = float(retry_after) if retry_after.isdigit() else delay
time.sleep(wait)
delay *= 2
return resp # vẫn bị chặn: trả về phản hồi cuối cùng để code gọi xử lý
resp = polite_get("https://example.com/products?page=1")
print(resp.status_code, len(resp.text))
Lưu ý hàm này không làm gì. Nó không retry một 403, và nó không xoay qua các danh tính để vượt qua một chặn. Một 403 là quyết định dừng lại, không phải một câu đố cần giải.
Chẩn đoán một lần chặn từng bước
Khi một lần chạy bắt đầu thất bại, hãy đi qua các kiểm tra sau trước khi thay đổi bất cứ điều gì khác:
- Xem URL thất bại đầu tiên và mã trạng thái của nó. Một lần chặn thường bắt đầu tại một điểm cụ thể, không ngẫu nhiên.
- So sánh tốc độ request trong vài phút trước lần thất bại đầu tiên. Đợt bùng phát là nguyên nhân phổ biến nhất.
- Kiểm tra xem lỗi có gắn với một đường dẫn, một mục hay một khung giờ trong ngày không.
- Tải cùng URL một lần bằng tay trong một trình duyệt bình thường. Nếu trình duyệt tải được, khác biệt nằm ở mẫu request của bạn. Nếu trình duyệt cũng bị chặn, trang tạm thời không phục vụ trang đó cho bạn.
- Chỉ sau đó mới thay đổi code. Chậm lại trước, backoff thứ hai, và xem xét danh tính cuối cùng.
Thay đổi nhiều thứ cùng lúc khiến bạn không biết điều nào đã giúp.
Giữ một danh tính nhất quán và trung thực
Scraper bị chặn khi danh tính của chúng thay đổi giữa các request, hoặc khi nó không khớp với phần còn lại của lưu lượng. Một phiên duy nhất với một User-Agent rõ ràng là danh tính ổn định đơn giản nhất. Đặt tên cho bot của bạn, thêm một liên kết liên hệ và chỉ gửi những header mà một client bình thường sẽ gửi. Đổi User-Agent để trông như nhiều khách truy cập khác nhau là một hình thức né tránh, và nó có xu hướng làm tăng khả năng bị chặn, không phải giảm.
Nếu một trang phục vụ nội dung khác cho trình duyệt so với client HTTP đơn giản, hãy dùng trình duyệt thật cho những trang đó và giữ cùng giới hạn tốc độ. Đó là con đường hợp pháp, và nó giữ mẫu request ở mức hợp lý.
Dùng proxy cẩn thận
Proxy thay đổi địa chỉ mà request xuất phát từ đó. Nó là công cụ cho những nhu cầu hợp pháp, như kiểm thử từ một khu vực cụ thể trên một trang bạn được phép kiểm thử, hoặc phân tán tải qua hạ tầng của chính bạn. Nó không phải cách để thoát khỏi một giới hạn mà trang đã đặt lên bạn. Định dạng proxy là định dạng mà các loại task proxy dùng:
proxies = {
"http": "http://user:pass@proxy.example.net:8000",
"https": "http://user:pass@proxy.example.net:8000",
}
resp = session.get("https://example.com/products", proxies=proxies, timeout=30)
Việc chọn loại proxy là một câu hỏi riêng, được bao quát trong proxy dân cư và proxy datacenter và proxy xoay vòng cho scraping.
Khi lần chặn trở thành captcha
Một số trang trả lời một vấn đề về tốc độ bằng captcha thay vì chặn cứng. Thứ tự hợp lý là: dừng lại, chậm lại, kiểm tra lại luật lệ, và chỉ sau đó mới cân nhắc giải. Với widget PopularCaptcha, API đang hoạt động của Scarnox trả về một token mà bạn gửi trong trường h-captcha-response. Với các loại captcha khác, hãy kiểm tra danh sách hiện có, vì hỗ trợ cho reCAPTCHA, Cloudflare Turnstile, GeeTest, AWS WAF và Arkose đang được chuẩn bị. Hướng dẫn chi tiết là scraping captcha trên web.
Nếu bạn bắt đầu thấy chặn trên một trang, nguyên nhân gần như luôn là mẫu request. Tránh captcha khi scraping bao quát các thói quen giúp ngăn chặn điều đó.
Scrape có trách nhiệm
- Kiểm tra
robots.txtvà điều khoản của trang trước khi bắt đầu, và tuân theo các đường dẫn cùng độ trễ mà họ yêu cầu. - Dừng lại khi nhận 403 hoặc một thông báo "không scrape" rõ ràng. Đừng tiếp tục thử.
- Giữ tốc độ đủ thấp để một khách truy cập bình thường không nhận ra lưu lượng của bạn.
- Không thu thập dữ liệu cá nhân mà bạn không có lý do để giữ, và xử lý mọi dữ liệu bạn thu thập theo quy định về quyền riêng tư áp dụng cho bạn.
- Nếu bạn cần khối lượng lớn hơn nhiều so với mức trang phục vụ thoải mái, hãy xin chủ trang một feed hoặc API.