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:
- 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.
- Mở DevTools → Network tab → XHR/Fetch. Nếu thấy endpoint
/api/posts.jsonhay tương tự trả về JSON sạch → gọi thẳng API đó, không cần parse HTML. - 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).
- 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:
- 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. - 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.
- 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ự. - Kiểm tra
robots.txttrước. Truy cậpexample.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. - Selector fragile là technical debt thật sự. Selector kiểu
div > div:nth-child(3) > spansẽ break ngay khi trang thay đổi layout. Ưu tiên selector dựa trênclasscó ý nghĩa ngữ nghĩa như.product-titlehay attributedata-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.
