Web Scraping với Python BeautifulSoup: Chọn đúng công cụ trước khi code

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

Mình bắt đầu làm automation từ một script nhỏ ~200 dòng để thu thập giá sản phẩm mỗi ngày. Giờ cái project đó đã phình lên 2000 dòng, và một trong những bài học đau nhất là chọn nhầm công cụ ngay từ đầu — mất cả tuần viết Selenium cho một trang hoàn toàn server-side render, trong khi requests + BeautifulSoup làm xong trong 2 tiếng.

Trước khi code bất cứ dòng nào, cần biết Python có 3 hướng tiếp cận chính để scrape web:

  • requests + BeautifulSoup — Download HTML tĩnh, parse cấu trúc DOM
  • Selenium / Playwright — Điều khiển browser thật, chờ JavaScript render xong
  • Scrapy — Framework đầy đủ tính năng cho crawling quy mô lớn

So sánh 3 hướng tiếp cận

Ba công cụ này không cạnh tranh nhau — mỗi cái giải quyết một loại bài toán khác nhau. Chọn nhầm từ đầu, như mình đã mắc phải, là mất nguyên một tuần để viết lại.

requests + BeautifulSoup

  • ✅ Cài đặt 5 phút, code dễ đọc, không cần GUI
  • ✅ Nhanh, nhẹ — chạy tốt trên VPS 512MB RAM
  • ✅ Đủ mạnh cho phần lớn use case thực tế
  • ❌ Không handle được JavaScript — trang dùng React/Vue/Angular sẽ trả về HTML rỗng hoặc skeleton
  • ❌ Không simulate được click, scroll, form submit

Selenium / Playwright

  • ✅ Chạy được trang full JavaScript, SPA
  • ✅ Simulate được mọi thao tác người dùng
  • ❌ Mỗi browser instance ngốn ~200MB RAM, chậm hơn 10-20 lần
  • ❌ Dễ bị detect hơn — nhiều site hiện tại fingerprint headless browser và trả về CAPTCHA hoặc 403 ngay lập tức

Scrapy

  • ✅ Async, crawl hàng nghìn trang song song
  • ✅ Built-in pipeline, middlewares, retry logic, export CSV/JSON
  • ❌ Learning curve cao — phải hiểu Spider, Item, Pipeline trước khi viết được dòng đầu tiên
  • ❌ Overkill hoàn toàn cho project scrape vài chục trang

Phân tích: Khi nào dùng cái gì?

Rule of thumb mình hay áp dụng trước khi bắt đầu bất kỳ scraping project nào:

  1. Mở DevTools → View Page Source. Nếu dữ liệu cần lấy xuất hiện trong source HTML → dùng requests + BeautifulSoup.
  2. Mở DevTools → Network tab → XHR/Fetch. Nếu thấy endpoint /api/posts.json hay tương tự trả về JSON sạch → gọi thẳng API đó, không cần parse HTML.
  3. Dữ liệu chỉ xuất hiện sau khi scroll, click, đăng nhập → dùng Playwright (ưu tiên hơn Selenium vì API hiện đại hơn).
  4. Cần crawl hàng trăm, hàng nghìn URL cùng lúc với retry/pipeline → dùng Scrapy.

Nếu đây là scraping project đầu tiên của bạn, bắt đầu với requests + BeautifulSoup. Cài trong 5 phút, chạy trên VPS rẻ nhất cũng được, xử lý phần lớn bài toán không cần JS. Phần dưới hướng dẫn từng bước.

Triển khai từng bước với BeautifulSoup

Bước 1: Cài đặt

pip install requests beautifulsoup4 lxml

lxml là parser nhanh hơn html.parser có sẵn của Python. Nên cài luôn từ đầu.

Bước 2: Download HTML và parse cơ bản

import requests
from bs4 import BeautifulSoup

url = "https://example.com/articles"

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}

response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()  # Raise exception nếu status != 200

soup = BeautifulSoup(response.text, "lxml")
print(soup.title.text)  # In tiêu đề trang

Luôn set User-Agent. Nhiều server trả 403 hoặc trả về HTML khác hẳn khi request đến không kèm header này. timeout=10 để script không treo vô thời hạn khi server im lặng.

Bước 3: Tìm element bằng CSS selector và find()

BeautifulSoup có 2 cách tìm element chính:

# Cách 1: find() và find_all() — tìm theo tag, class, id
title = soup.find("h1", class_="article-title")
all_links = soup.find_all("a", href=True)

# Cách 2: select() — CSS selector (quen thuộc nếu biết CSS)
title = soup.select_one("h1.article-title")
all_links = soup.select("nav a[href]")

# Lấy text và attributes
print(title.get_text(strip=True))
print(all_links[0]["href"])

Mình thường dùng select() vì CSS selector linh hoạt hơn, đặc biệt khi cần chọn theo attribute hoặc quan hệ parent-child phức tạp.

Bước 4: Ví dụ thực tế — Scrape danh sách bài viết

import requests
from bs4 import BeautifulSoup
import time

def scrape_blog_posts(url):
    headers = {"User-Agent": "Mozilla/5.0 (compatible; research-bot/1.0)"}

    response = requests.get(url, headers=headers, timeout=10)
    response.raise_for_status()

    soup = BeautifulSoup(response.text, "lxml")
    posts = []

    # Điều chỉnh selector theo cấu trúc HTML của trang đích
    for article in soup.select("article.post"):
        title_el = article.select_one("h2.entry-title a")
        date_el = article.select_one("time.entry-date")

        if title_el:
            posts.append({
                "title": title_el.get_text(strip=True),
                "url": title_el["href"],
                "date": date_el["datetime"] if date_el else None
            })

    return posts

# Crawl nhiều trang — nhớ delay giữa các request
all_posts = []
for page in range(1, 6):
    url = f"https://example.com/blog/page/{page}/"
    posts = scrape_blog_posts(url)
    all_posts.extend(posts)
    time.sleep(1.5)  # Delay 1.5 giây — respectful scraping

print(f"Scraped {len(all_posts)} posts")

time.sleep() không phải tùy chọn — đây là phép lịch sự tối thiểu khi scrape. Server người ta không có nghĩa vụ serve request của mình không giới hạn tốc độ.

Bước 5: Xử lý edge case và HTML bị broken

from urllib.parse import urljoin

# Xử lý element không tồn tại an toàn — không để script crash
price_el = soup.select_one(".product-price")
price = price_el.get_text(strip=True) if price_el else "N/A"

# Lấy text đệ quy, loại bỏ tag HTML con
description = soup.select_one(".description")
clean_text = description.get_text(separator=" ", strip=True)

# Xử lý relative URL → absolute URL
base_url = "https://example.com"
for link in soup.select("a[href]"):
    full_url = urljoin(base_url, link["href"])

# Fix encoding tiếng Việt nếu bị lỗi
response = requests.get(url, headers=headers)
response.encoding = "utf-8"  # Force UTF-8 thay vì để auto-detect
soup = BeautifulSoup(response.text, "lxml")

Những điều mình ước biết trước khi bắt đầu

Sau khi project phình lên 2000 dòng và phải rebuild lại một lần, đây là mấy thứ mình nghĩ đáng biết từ sớm:

  1. Inspect Network tab, không phải Elements tab. Nhiều trang load dữ liệu qua JSON API ngầm. Mở DevTools → Network → Filter XHR — đôi khi thấy ngay /api/v1/products.json, gọi thẳng endpoint đó clean hơn nhiều so với parse HTML.
  2. Lưu HTML local khi debug. Download HTML ra file, đọc từ file trong khi phát triển — không cần hit server mỗi lần chạy test, tiết kiệm thời gian và tránh bị rate-limit.
  3. Dùng soup.prettify() để hiểu cấu trúc. In ra HTML được format đẹp trước khi viết selector — đỡ phải nhìn chằm chằm vào HTML minified một dòng 10.000 ký tự.
  4. Kiểm tra robots.txt trước. Truy cập example.com/robots.txt để xem trang cho phép crawl những path nào. Không phải quy định pháp lý nhưng là ethical practice — và một số trang có điều khoản cấm scrape thương mại.
  5. Selector fragile là technical debt thật sự. Selector kiểu div > div:nth-child(3) > span sẽ break ngay khi trang thay đổi layout. Ưu tiên selector dựa trên class có ý nghĩa ngữ nghĩa như .product-title hay attribute data-testid.

Bước tiếp theo

BeautifulSoup xử lý được phần lớn bài toán scraping thực tế — thu thập giá, monitor tin tức, backup nội dung, nghiên cứu thị trường. Cái khó không phải API của BeautifulSoup. Cái khó là đọc đúng cấu trúc HTML của trang đích, rồi viết selector đủ bền vững khi layout thay đổi 3 tháng sau.

Lúc nào nên chuyển tool? Cần crawl vài nghìn URL song song với retry logic và export pipeline → thử Scrapy. Gặp trang SPA không có JSON API ngầm → chuyển sang Playwright. Còn lại, đừng over-engineer — BeautifulSoup giải quyết được khoảng 80% bài toán scraping thông thường.

Share: