Firecrawl: Thu Thập Dữ Liệu Web Cho Ứng Dụng AI Khi Scraper Thông Thường Thất Bại

Artificial Intelligence tutorial - IT technology blog
Artificial Intelligence tutorial - IT technology blog

2 giờ sáng, pipeline AI của mình ngừng hoạt động

Mình đang build một công cụ RAG (Retrieval-Augmented Generation) để tổng hợp thông tin từ các trang documentation kỹ thuật. Deadline demo sáng hôm sau. Mọi thứ chạy ngon trên localhost với data mẫu — nhưng khi bắt đầu crawl data thật từ production sites, hệ thống bắt đầu trả về toàn bộ garbage.

Log đầy những thứ kiểu này:

[ERROR] Parsed content: "Please enable JavaScript to view this page"
[ERROR] Parsed content: "Verifying you are human. This may take a few seconds."
[ERROR] Empty markdown extracted from https://docs.example.com/api-reference

BeautifulSoup đã bắt được HTML rồi đấy — nhưng là HTML của loading screen, không phải nội dung thật. AI được feed đống rác này vào thì output cũng tệ không kém.

Nguyên nhân: Web hiện đại không thân thiện với scraper truyền thống

Sau khoảng 30 phút debug lúc nửa đêm, mình mới hiểu rõ vấn đề. Các trang documentation và web hiện đại thường gây ra 3 loại vấn đề với scrapers thông thường:

  • JavaScript rendering: Nội dung load qua React/Vue/Next.js — BeautifulSoup chỉ thấy HTML trước khi JS chạy.
  • Anti-bot protection: Cloudflare, CAPTCHA, User-Agent detection chặn request tự động.
  • Dynamic content: Infinite scroll, lazy loading, nội dung phụ thuộc vào interaction của người dùng.

Mình đã thử chuyển sang Selenium. Nó chạy được — nhưng chậm kinh khủng. Crawl 50 trang mất 20 phút, chưa kể memory leak sau vài giờ chạy liên tục. Với pipeline cần xử lý hàng nghìn trang, đây không phải hướng đi khả thi.

# Approach cũ với BeautifulSoup - fail với JS-rendered pages
import requests
from bs4 import BeautifulSoup

def scrape_old_way(url):
    response = requests.get(url, headers={"User-Agent": "Mozilla/5.0..."})
    soup = BeautifulSoup(response.text, "html.parser")
    # Kết quả nhận về: "Please enable JavaScript" 😭
    return soup.get_text()

Firecrawl là gì và tại sao nó khác biệt

Mình tìm ra Firecrawl sau 20 phút Google tuyệt vọng lúc 3 giờ sáng. Đây là API service (có cả self-hosted option) không phải general-purpose scraper — nó sinh ra để làm đúng một việc: cung cấp data sạch cho AI pipeline. So với các tool khác:

  • Render JavaScript đầy đủ trước khi extract content
  • Tự chuyển HTML thành Markdown sạch — định dạng mà LLM tiêu hóa tốt nhất
  • Xử lý anti-bot, rate limiting, retry hoàn toàn tự động
  • Crawl toàn bộ website theo depth, không chỉ một page đơn lẻ

Cài đặt Firecrawl Python SDK

pip install firecrawl-py

Lấy API key tại firecrawl.dev — có free tier để test. Sau đó set environment variable:

export FIRECRAWL_API_KEY="fc-your-api-key-here"

Các cách sử dụng Firecrawl trong thực tế

1. Scrape một trang đơn lẻ

Đơn giản nhất: đưa vào một URL, nhận về Markdown sạch.

import os
from firecrawl import FirecrawlApp

app = FirecrawlApp(api_key=os.environ["FIRECRAWL_API_KEY"])

# Scrape một trang, nhận về Markdown
result = app.scrape_url(
    "https://docs.python.org/3/library/asyncio.html",
    formats=["markdown"]
)

print(result["markdown"][:500])
# Output: nội dung sạch, sẵn sàng feed vào LLM

Kết quả trả về là Markdown thuần — không có HTML tag thừa, không có navigation menu rác, không có footer quảng cáo. Ai đã từng ngồi viết regex bóc nội dung thủ công sẽ hiểu ngay đây tiết kiệm được bao nhiêu công.

2. Crawl toàn bộ website theo độ sâu

Muốn index toàn bộ docs site cho RAG pipeline — không phải từng page một:

import os
from firecrawl import FirecrawlApp

app = FirecrawlApp(api_key=os.environ["FIRECRAWL_API_KEY"])

# Crawl toàn bộ docs site, tối đa 50 trang
crawl_result = app.crawl_url(
    "https://docs.example.com",
    limit=50,
    scrape_options={"formats": ["markdown"]}
)

for page in crawl_result["data"]:
    print(f"URL: {page['metadata']['sourceURL']}")
    print(f"Content length: {len(page['markdown'])} chars")
    print("---")

3. Extract dữ liệu có cấu trúc với LLM

Tính năng mình dùng nhiều nhất — extract thông tin theo schema định sẵn, Firecrawl tự dùng AI để điền vào:

import os
from firecrawl import FirecrawlApp

app = FirecrawlApp(api_key=os.environ["FIRECRAWL_API_KEY"])

# Extract có cấu trúc từ trang product
result = app.extract(
    ["https://example.com/product/123"],
    schema={
        "type": "object",
        "properties": {
            "product_name": {"type": "string"},
            "price": {"type": "number"},
            "features": {
                "type": "array",
                "items": {"type": "string"}
            },
            "availability": {"type": "boolean"}
        },
        "required": ["product_name", "price"]
    }
)

print(result["data"])
# Output: {"product_name": "...", "price": 29.99, "features": [...], ...}

Giải pháp tốt nhất: Kết hợp Firecrawl với LLM

Đây là pattern mình đang chạy trên production sau khi vá xong cái pipeline lúc 2 giờ sáng đó. Kết hợp Firecrawl để crawl và Claude để xử lý nội dung:

import os
import anthropic
from firecrawl import FirecrawlApp

firecrawl = FirecrawlApp(api_key=os.environ["FIRECRAWL_API_KEY"])
claude = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

def research_topic(url: str, question: str) -> str:
    """
    Crawl một URL và dùng Claude để trả lời câu hỏi từ nội dung đó.
    """
    # Step 1: Lấy nội dung sạch từ web
    scrape_result = firecrawl.scrape_url(url, formats=["markdown"])
    content = scrape_result.get("markdown", "")

    if not content:
        return "Không thể lấy nội dung từ URL này."

    # Step 2: Dùng Claude để trả lời từ nội dung đã crawl
    response = claude.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        messages=[
            {
                "role": "user",
                "content": f"""Dựa vào nội dung web sau đây, hãy trả lời câu hỏi.

Nội dung:
{content[:8000]}

Câu hỏi: {question}"""
            }
        ]
    )

    return response.content[0].text

# Ví dụ sử dụng
answer = research_topic(
    url="https://docs.python.org/3/library/asyncio-task.html",
    question="Sự khác biệt giữa asyncio.create_task() và asyncio.ensure_future() là gì?"
)
print(answer)

Xây dựng RAG indexer đơn giản

Cần lưu data lại để query nhiều lần? Đây là skeleton của một indexer tối giản:

import os
from firecrawl import FirecrawlApp
from typing import List, Dict

firecrawl = FirecrawlApp(api_key=os.environ["FIRECRAWL_API_KEY"])

class SimpleRAGIndexer:
    def __init__(self):
        self.documents: List[Dict] = []

    def index_website(self, base_url: str, max_pages: int = 20):
        """Crawl và index toàn bộ website."""
        print(f"Đang crawl {base_url}...")

        result = firecrawl.crawl_url(
            base_url,
            limit=max_pages,
            scrape_options={"formats": ["markdown"]}
        )

        for page in result.get("data", []):
            if page.get("markdown"):
                self.documents.append({
                    "url": page["metadata"]["sourceURL"],
                    "content": page["markdown"],
                    "title": page["metadata"].get("title", "")
                })

        print(f"Đã index {len(self.documents)} trang.")
        return self.documents

    def search(self, query: str, top_k: int = 3) -> List[Dict]:
        """Simple keyword search — thực tế nên dùng vector DB."""
        query_lower = query.lower()
        results = [
            {**doc, "score": doc["content"].lower().count(query_lower)}
            for doc in self.documents
            if doc["content"].lower().count(query_lower) > 0
        ]
        return sorted(results, key=lambda x: x["score"], reverse=True)[:top_k]

# Sử dụng
indexer = SimpleRAGIndexer()
indexer.index_website("https://docs.python.org/3/library/", max_pages=30)

relevant_docs = indexer.search("async await coroutine")
for doc in relevant_docs:
    print(f"URL: {doc['url']} | Score: {doc['score']}")

Một số lưu ý từ thực tế

Dùng trong production được khoảng 3 tháng. Nhìn chung khá ổn — nhưng có mấy điểm cần biết trước:

  • Rate limits: Free tier của Firecrawl có giới hạn request/tháng. Nên ước tính nhu cầu trước khi chọn plan — đừng để bị ngắt giữa chừng lúc đang crawl production data.
  • Self-hosted option: Firecrawl là open-source (mendableai/firecrawl trên GitHub). Tự deploy trên VPS nếu cần data privacy hoặc muốn kiểm soát chi phí dài hạn.
  • Cache kết quả: Crawl cùng URL nhiều lần rất tốn quota. Cache với TTL hợp lý — 24 giờ cho documentation, 1 giờ cho news feed.
  • robots.txt: Firecrawl respect robots.txt theo mặc định. Override phải cấu hình rõ ràng — và nhớ kiểm tra xem bạn có quyền crawl trang đó không.

So sánh nhanh các giải pháp

Công cụ          | JS Render | Output sạch | Dễ dùng | Chi phí
-----------------|-----------|-------------|---------|------------------
BeautifulSoup    |     ❌    |      ❌     |   ✅   | Free
Selenium         |     ✅    |      ❌     |   ❌   | Free (chậm)
Playwright       |     ✅    |      ❌     | Medium  | Free (cần infra)
Firecrawl API    |     ✅    |      ✅     |   ✅   | Paid / Self-host
Firecrawl (self) |     ✅    |      ✅     | Medium  | Infra only

Với AI pipeline cần data sạch và nhất quán, Firecrawl giải quyết đúng bài toán mà không cần viết thêm 500 dòng code xử lý edge case. Không phải vì nó “tốt nhất” trên mọi tiêu chí — BeautifulSoup vẫn đủ dùng cho các site tĩnh. Pipeline RAG của mình hiện crawl khoảng 200–300 trang mỗi ngày, error rate dưới 2%, và quan trọng hơn hết — không còn thức đêm debug scraper nữa.

Share: