Scrapy Python: Xây dựng hệ thống Crawler triệu bản ghi không sợ ‘bay’ IP

Python tutorial - IT technology blog
Python tutorial - IT technology blog

Cơn ác mộng OOM lúc 2 giờ sáng

Cách đây vài tháng, mình nhận task crawl 500.000 sản phẩm từ một sàn TMĐT lớn để làm báo cáo thị trường. Lúc đó mình khá chủ quan: “Chỉ là lấy dữ liệu thôi mà, dùng requests với BeautifulSoup chắc là ổn”.

Đúng 2 giờ sáng, server báo lỗi đỏ rực. Script chạy được 3 tiếng thì treo cứng. RAM nhảy vọt lên mức 4GB rồi sập (Out of Memory). Tệ hơn nữa, IP server đã bị đối tác đưa vào blacklist sau khi gửi quá nhiều request dồn dập. Dự án ban đầu chỉ 200 dòng code, nhưng khi đắp thêm vá víu cho retry, proxy, và đa luồng, nó phình lên gần 2000 dòng mà vẫn chạy chập chờn.

Đó là lúc mình nhận ra một sự thật. Để làm hệ thống thu thập dữ liệu quy mô lớn (Large-scale Web Crawler), bạn không thể dùng các thư viện rời rạc. Bạn cần một Framework thực thụ. Scrapy chính là giải pháp thay đổi hoàn toàn cuộc chơi.

Tại sao BeautifulSoup thường hụt hơi khi làm dự án lớn?

Vấn đề cốt lõi là requests hoạt động theo cơ chế đồng bộ (synchronous). Nó gửi một request, đứng đợi phản hồi, rồi mới đi tiếp. Với 500.000 trang, mỗi trang mất trung bình 1 giây, bạn sẽ phải đợi 138 giờ liên tục. Con số này là không tưởng trong môi trường production.

Việc tự viết code quản lý lỗi, tự động thử lại (retry) hay lưu dữ liệu vào database mà không làm nghẽn luồng xử lý là một cực hình. Scrapy giải quyết triệt để bài toán này nhờ kiến trúc không đồng bộ (asynchronous) dựa trên Twisted framework. Thay vì đứng đợi, Scrapy bắn liên tiếp hàng chục request cùng lúc, giúp tối ưu hóa băng thông tối đa.

Ba hướng tiếp cận Crawler: Bạn đang ở đâu?

Trong giới cào dữ liệu, anh em thường chọn 1 trong 3 con đường sau:

  • Dùng Threading/Multiprocessing: Kết hợp với requests. Cách này cực khó quản lý trạng thái (state). Bạn rất dễ làm tràn bộ nhớ nếu không kiểm soát tốt số lượng thread.
  • Dùng Selenium/Playwright: Phương án này cực “sang” vì nó giả lập trình duyệt như người thật. Tuy nhiên, nó ngốn tài nguyên kinh khủng. Chỉ nên dùng khi trang web dùng quá nhiều JavaScript phức tạp mà bạn không thể bóc tách qua API.
  • Dùng Scrapy: Đây là “trùm cuối” về hiệu năng. Nó cho phép xử lý hàng trăm request song song mà vẫn giữ mức tiêu thụ RAM ổn định.

Triển khai Scrapy: Từ số 0 đến hệ thống thực chiến

Trước tiên, hãy cài đặt Scrapy. Mình khuyên bạn luôn dùng môi trường ảo (virtualenv) để giữ dự án sạch sẽ.

pip install scrapy

1. Cấu trúc dự án chuẩn

Đừng viết code tập trung vào một file duy nhất. Scrapy ép bạn làm việc theo module. Cách tổ chức này sẽ cứu bạn khi dự án phình to sau này.

scrapy startproject my_crawler
cd my_crawler
scrapy genspider ecommerce spider_site.com

2. Định nghĩa Item (Model dữ liệu)

Thay vì dùng dictionary lộn xộn, hãy khai báo rõ ràng các trường dữ liệu trong items.py. Nó đóng vai trò như một bản thiết kế (Schema) cho dữ liệu của bạn.

import scrapy

class ProductItem(scrapy.Item):
    name = scrapy.Field()
    price = scrapy.Field()
    url = scrapy.Field()
    sku = scrapy.Field()

3. Viết Spider – Bộ não điều khiển

Đây là nơi xử lý logic bóc tách. Scrapy hỗ trợ CSS Selector và XPath mạnh mẽ hơn hẳn find() của BeautifulSoup. Dưới đây là cách bạn lấy dữ liệu và tự động chuyển trang (pagination).

import scrapy
from my_crawler.items import ProductItem

class EcommerceSpider(scrapy.Spider):
    name = "ecommerce"
    start_urls = ['https://example-shop.com/products']

    def parse(self, response):
        for product in response.css('div.product-item'):
            item = ProductItem()
            item['name'] = product.css('h2::text').get()
            item['price'] = product.css('span.price::text').get()
            item['url'] = response.urljoin(product.css('a::attr(href)').get())
            yield item

        next_page = response.css('a.next::attr(href)').get()
        if next_page:
            yield response.follow(next_page, self.parse)

Bí kíp vận hành ở quy mô lớn

Để chạy thực tế, cấu hình mặc định là không đủ. Dưới đây là những tinh chỉnh mình đã đúc kết sau nhiều lần “ăn hành” trên server.

Item Pipelines: Bộ lọc dữ liệu sạch

Dữ liệu cào về thường chứa rác như khoảng trắng thừa hay ký tự lạ. Đừng xử lý việc này trong Spider. Hãy đẩy vào pipelines.py để giữ code bóc tách luôn gọn gàng.

class CleanDataPipeline:
    def process_item(self, item, spider):
        if item['price']:
            # Chuyển đổi "$1,200" thành 1200.0
            item['price'] = float(item['price'].replace('$', '').replace(',', '').strip())
        return item

Middlewares: Kỹ thuật né tránh bị chặn

Gửi 1000 request/phút từ một IP duy nhất là cách nhanh nhất để bị khóa tài khoản. Hãy mở settings.py và cấu hình xoay vòng User-Agent hoặc sử dụng Proxy Service.

# settings.py
DOWNLOAD_DELAY = 0.5  # Nghỉ giữa các request để tránh bị nghi ngờ
CONCURRENT_REQUESTS = 32 # Số lượng yêu cầu chạy song song

# Tự động đổi User-Agent để giả lập nhiều trình duyệt khác nhau
DOWNLOADER_MIDDLEWARES = {
    'scrapy.middlewares.useragent.UserAgentMiddleware': None,
    'scrapy_user_agents.middlewares.RandomUserAgentMiddleware': 400,
}

Bài học về quản lý tài nguyên: Đừng để RAM quá tải

Một sai lầm phổ biến là gom tất cả dữ liệu vào bộ nhớ rồi mới ghi file. Với 1 triệu dòng, script của bạn chắc chắn sẽ sập. Scrapy có cơ chế Feed Exports cực kỳ thông minh.

Nó cho phép stream dữ liệu trực tiếp ra file JSON hoặc CSV theo từng dòng:

scrapy crawl ecommerce -o data.jsonl

Mẹo nhỏ: Hãy dùng định dạng .jsonl (JSON Lines). Nếu server đột ngột mất điện, các dữ liệu đã cào trước đó vẫn được bảo toàn, không bị hỏng file như định dạng JSON truyền thống.

Lời kết

Scrapy không chỉ là một thư viện, nó là một tư duy hệ thống. Nó buộc bạn phải viết code sạch, module hóa rõ ràng và luôn ưu tiên hiệu năng. Nếu bạn chỉ cần lấy dữ liệu từ vài trang web nhỏ, BeautifulSoup vẫn rất tuyệt. Nhưng nếu mục tiêu của bạn là một hệ thống ổn định, chạy 24/7 và xử lý hàng triệu bản ghi, hãy chọn Scrapy. Đầu tư thời gian học nó lúc đầu sẽ giúp bạn có những giấc ngủ ngon hơn vào lúc 2 giờ sáng.

Share: