Tôi scrape Hacker News bằng Python như thế nào (2 cách, có đầy đủ code)

Cập nhật lần cuối vào August 20, 2026
Tôi scrape Hacker News bằng Python như thế nào (2 cách, có đầy đủ code)

Vài tháng trước, tôi muốn dựng một bản tin hằng ngày tổng hợp các bài viết nổi bật trên Hacker News cho team ở Thunderbit. Phản xạ đầu tiên của tôi là cứ đánh dấu trang lại rồi mỗi sáng mở ra lướt qua. Cách đó chỉ trụ được khoảng ba ngày, trước khi tôi nhận ra mình đang mất 20 phút mỗi ngày chỉ để đọc tiêu đề và copy-paste link vào bảng tính.

Hacker News là một trong những nguồn tin công nghệ cô đọng và đáng giá nhất trên internet — khoảng 13 triệu lượt truy cập mỗi tháng, tầm 1.300 bài mới được đăng mỗi ngày, và khoảng 13.000 bình luận được tạo ra mỗi ngày. Dù bạn đang theo dõi xu hướng công nghệ mới, giám sát thương hiệu của mình, xây pipeline tuyển dụng từ các thread “Who’s Hiring”, hay đơn giản là muốn cập nhật thứ mà cộng đồng developer đang quan tâm, thì việc tự theo dõi thủ công gần như chắc chắn là một trận chiến rất không cân sức.

Tin vui là: scrape Hacker News bằng Python thật ra khá đơn giản. Trong bài này, tôi sẽ hướng dẫn bạn hai cách đầy đủ — scrape HTML bằng BeautifulSoup và dùng HN Firebase API chính thức — kèm theo phân trang, xuất dữ liệu, các pattern sẵn sàng cho production, và một lựa chọn không cần code cho lúc Python trở nên hơi quá tay.

Vì sao nên scrape Hacker News bằng Python?

Hacker News không chỉ là một trang gom link thông thường. Đây là một feed được cộng đồng tuyển chọn, nơi những câu chuyện công nghệ đáng chú ý nhất tự nổi lên nhờ upvote và thảo luận sôi nổi. Tệp người dùng nghiêng mạnh về dân công nghệ (khoảng 76% nam, độ tuổi chính 25–34), và tỷ lệ truy cập trực tiếp 66% cho thấy đây là một cộng đồng đọc rất đều, rất có thói quen — không phải kiểu lướt qua cho vui.

Đó là lý do nhiều người scrape dữ liệu HN:

Trường hợp sử dụngKết quả nhận được
Bản tin công nghệ hằng ngàyCác bài nổi bật, điểm số và link được gửi vào email hoặc Slack
Theo dõi thương hiệu/đối thủCảnh báo khi công ty hoặc sản phẩm của bạn được nhắc tới
Phân tích xu hướngTheo dõi công nghệ, ngôn ngữ lập trình hoặc chủ đề nào đang tăng độ quan tâm theo thời gian
Tuyển dụngPhân tích các thread "Who's Hiring" để lấy tin tuyển dụng, stack công nghệ và tín hiệu lương thưởng
Nghiên cứu nội dungTìm chủ đề có hiệu suất tốt để viết hoặc chia sẻ
Phân tích cảm xúcĐo phản hồi của cộng đồng về sản phẩm, lần ra mắt hoặc thay đổi trong ngành

Những công ty có tổng giá trị hơn 400 tỷ USD như Stripe, Dropbox và Airbnb đều ghi nhận Hacker News là nơi mang lại phản hồi sớm và người dùng quan trọng. Drew Houston đã đăng demo của Dropbox lên HN vào tháng 4/2007, bài đó lên #1, và danh sách chờ beta tăng từ 5.000 lên 75.000 người chỉ trong một ngày. Dữ liệu HN không chỉ thú vị — nó còn có giá trị kinh doanh rất rõ ràng.

Dữ liệu này là công khai, nhưng cấu trúc trang khiến việc thu thập thủ công khá phiền. Tự động hóa bằng Python là cách thực tế hơn.

Hai cách scrape Hacker News bằng Python: Tổng quan

Hướng dẫn này bao gồm hai phương pháp đầy đủ, có thể chạy ngay:

  1. Scrape HTML với requests + BeautifulSoup — lấy HTML thô từ news.ycombinator.com rồi phân tích để trích xuất dữ liệu bài viết. Rất hợp để học nền tảng web scraping và lấy đúng những gì đang hiển thị trên trang.
  2. Hacker News Firebase API chính thức — gọi thẳng các endpoint JSON, không cần phân tích HTML. Phù hợp hơn cho pipeline dữ liệu ổn định, truy xuất bình luận và dữ liệu lịch sử.

Dưới đây là bảng so sánh để bạn chọn cách hợp nhất:

Tiêu chíScrape HTML (requests + BS4)HN Firebase APIThunderbit (No-Code)
Độ phức tạp khi thiết lậpTrung bình (phân tích selector HTML)Thấp (JSON endpoint)Không cần (Chrome extension 2 cú nhấp)
Tính cập nhật dữ liệuReal-time trang chủReal-time (bất kỳ item nào theo ID)Real-time
Rủi ro giới hạn tốc độTrung bình (robots.txt yêu cầu delay 30 giây)Thấp (chính thức, khá thoáng)Thunderbit quản lý
Truy cập bình luậnKhó (HTML lồng nhau)Dễ (recursive item IDs)Có tính năng scrape subpage
Dữ liệu lịch sửHạn chếQua Algolia Search APIKhông áp dụng
Phù hợp nhất vớiHọc scraping nền tảngPipeline dữ liệu ổn địnhNgười không biết code, cần export nhanh

Cả hai cách đều có code Python đầy đủ để chạy ngay. Và nếu bạn chỉ muốn lấy dữ liệu mà không muốn viết code, tôi cũng sẽ nhắc đến lựa chọn đó.

Trước khi bắt đầu

  • Độ khó: Từ cơ bản đến trung cấp
  • Thời gian cần thiết: Khoảng 15–20 phút cho mỗi phương pháp
  • Bạn cần có:
    • Python 3.11+ đã cài sẵn
    • Terminal hoặc trình soạn thảo code
    • Trình duyệt Chrome (nếu muốn xem HTML của HN hoặc thử tùy chọn no-code)
    • Thunderbit Chrome Extension (không bắt buộc, cho cách không cần code)

scrape-hacker-news-methods.webp

Thiết lập môi trường Python

Trước khi đụng vào dữ liệu HN, hãy chuẩn bị môi trường trước. Tôi khuyên bạn nên tạo virtual environment để dependency của dự án được gọn gàng.

# Tạo và kích hoạt virtual environment
python3 -m venv hn-scraper
# macOS/Linux:
source hn-scraper/bin/activate
# Windows:
hn-scraper\Scripts\activate

# Cài các gói cần cho cả hai phương pháp
pip install requests==2.33.1 beautifulsoup4==4.14.3 pandas==3.0.2 openpyxl==3.1.5

Nếu muốn dùng các pattern kiểu production sau này (cache, retry), bạn cũng nên cài thêm:

pip install requests-cache==1.3.1 tenacity==9.1.4

Không cần API key đặc biệt, không cần token xác thực. Dữ liệu của HN là công khai.

Cách 1: Scrape Hacker News bằng BeautifulSoup

Đây là kiểu cổ điển — tải HTML, phân tích nó, rồi lấy dữ liệu bạn cần. Đây là cách hầu hết mọi người học web scraping, và layout dạng bảng khá đơn giản của HN khiến nó trở thành môi trường luyện tập rất ổn.

Bước 1: Lấy trang chủ Hacker News

Mở editor và tạo file tên scrape_hn_bs4.py. Đây là đoạn code khởi đầu:

import requests
from bs4 import BeautifulSoup

url = "https://news.ycombinator.com/news"
headers = {"User-Agent": "Mozilla/5.0 (educational HN scraper)"}
response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, "html.parser")

print(f"Status: {response.status_code}, Page length: {len(response.text)} chars")

Chạy thử. Bạn sẽ thấy Status: 200 và độ dài trang khoảng 40.000–50.000 ký tự. Đó là HTML thô của trang chủ HN đang nằm trong bộ nhớ, sẵn sàng để phân tích.

Bước 2: Hiểu cấu trúc HTML

HN dùng layout dạng bảng — không phải CSS grid hay flex hiện đại. Mỗi bài trên trang gồm hai hàng <tr> chính:

  • Hàng bài viết (<tr class="athing submission">): chứa thứ hạng, tiêu đề và link
  • Hàng metadata (hàng <tr> kế tiếp): chứa điểm số, tác giả, thời gian và số bình luận

Các selector quan trọng:

  • span.titleline > a — tiêu đề bài viết và URL
  • span.score — số điểm (ví dụ: "118 points")
  • a.hnuser — username của tác giả
  • span.age — thời điểm đăng
  • Thẻ <a> cuối cùng trong .subtext có chữ "comment" — số bình luận

Nếu bạn nhấp chuột phải vào bất kỳ tiêu đề nào trên Chrome rồi chọn "Inspect", bạn sẽ thấy dạng như sau:

<span class="titleline">
  <a href="https://darkbloom.dev">Darkbloom – Private inference on idle Macs</a>
</span>

Và hàng metadata bên dưới:

<span class="score" id="score_47788542">118 points</span>
by <a href="user?id=twapi" class="hnuser">twapi</a>
<span class="age" title="2026-04-16T04:06:39 1776312399">
  <a href="item?id=47788542">2 hours ago</a>
</span>
| <a href="item?id=47788542">65&nbsp;comments</a>

Hiểu selector là bước rất quan trọng — nếu HN đổi markup, bạn sẽ phải cập nhật lại chúng. (Mách nhỏ: dùng API sẽ tránh hẳn vấn đề này.)

Bước 3: Trích xuất tiêu đề, link và điểm số

Giờ vào phần thực hành. Ta sẽ lặp qua từng hàng bài viết, lấy tiêu đề và link từ hàng bài viết, rồi lấy điểm số từ hàng metadata ngay bên dưới.

import requests
from bs4 import BeautifulSoup
from pprint import pprint

url = "https://news.ycombinator.com/news"
headers = {"User-Agent": "Mozilla/5.0 (educational HN scraper)"}
response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, "html.parser")

stories = []
story_rows = soup.select("tr.athing")

for row in story_rows:
    # Title và URL từ hàng bài viết
    title_tag = row.select_one("span.titleline > a")
    if not title_tag:
        continue
    title = title_tag.get_text()
    link = title_tag.get("href", "")

    # Metadata từ hàng kế tiếp
    meta_row = row.find_next_sibling("tr")
    score = 0
    author = ""
    comments = 0

    if meta_row:
        if score_tag := meta_row.select_one("span.score"):
            score = int(score_tag.get_text().replace(" points", ""))
        if author_tag := meta_row.select_one("a.hnuser"):
            author = author_tag.get_text()
        # Số bình luận: thẻ <a> cuối cùng có chữ "comment"
        for a_tag in meta_row.select("a"):
            text = a_tag.get_text()
            if "comment" in text:
                comments = int(text.split("\xa0")[0])

    stories.append({
        "title": title,
        "url": link,
        "score": score,
        "author": author,
        "comments": comments,
    })

# Lọc các bài có từ 50 điểm trở lên, sắp xếp theo điểm
top_stories = sorted(
    [s for s in stories if s["score"] >= 50],
    key=lambda x: x["score"],
    reverse=True,
)

pprint(top_stories[:10])

Một vài lưu ý về code:

  • Toán tử walrus (:=) hoạt động từ Python 3.8+. Nó giúp gán và kiểm tra trong cùng một dòng — rất tiện cho các phần tử tùy chọn như span.score có thể không tồn tại ở mọi hàng (ví dụ: bài tuyển dụng thường không có điểm).
  • HN dùng \xa0 (khoảng trắng không ngắt dòng) giữa con số và chữ "comments", nên ta phải split theo ký tự này.
  • Các bài dẫn tới trang khác của HN (ví dụ bài "Ask HN") sẽ có URL tương đối bắt đầu bằng item?id=. Bạn có thể muốn thêm tiền tố https://news.ycombinator.com/ cho các link này.

Bước 4: Chạy và xem kết quả

Lưu lại rồi chạy:

python scrape_hn_bs4.py

Bạn sẽ thấy output kiểu như:

[{'author': 'twapi',
  'comments': 65,
  'score': 118,
  'title': 'Darkbloom – Private inference on idle Macs',
  'url': 'https://darkbloom.dev'},
 {'author': 'sebg',
  'comments': 203,
  'score': 247,
  'title': 'Show HN: I built an open-source Perplexity alternative',
  'url': 'https://github.com/...'},
 ...]

Đó là 30 bài từ trang 1. Nhưng HN có hàng trăm bài đang hoạt động tại bất kỳ thời điểm nào. Phần phân trang sẽ được nói ở phần sau.

Cách 2: Scrape Hacker News bằng API chính thức

HN Firebase API là cách được HN chính thức hỗ trợ để truy cập dữ liệu Hacker News. Không cần xác thực, không cần API key, không cần phân tích HTML. Bạn nhận về JSON sạch sẽ. Tôi dùng cách này cho mọi thứ cần chạy ổn định trong production.

Những endpoint API chính bạn cần biết

Base URL là https://hacker-news.firebaseio.com/v0/. Đây là các endpoint quan trọng:

EndpointTrả vềVí dụ
/v0/topstories.jsonMảng tối đa 500 ID bài nổi bật[47788542, 47787901, ...]
/v0/newstories.jsonTối đa 500 ID bài mới nhấtCùng định dạng
/v0/beststories.jsonTối đa 500 ID bài tốt nhấtCùng định dạng
/v0/askstories.jsonTối đa 200 ID bài "Ask HN"Cùng định dạng
/v0/showstories.jsonTối đa 200 ID bài "Show HN"Cùng định dạng
/v0/jobstories.jsonTối đa 200 ID bài tuyển dụngCùng định dạng
/v0/item/{id}.jsonChi tiết đầy đủ của bất kỳ item nào (story, comment, poll)Đối tượng JSON
/v0/user/{username}.jsonHồ sơ người dùngĐối tượng JSON
/v0/maxitem.jsonID item lớn nhất hiện tạiSố nguyên (ví dụ: 47789427)

Một item bài viết trông sẽ như thế này:

{
  "by": "twapi",
  "descendants": 65,
  "id": 47788542,
  "kids": [47789171, 47788769, 47788762],
  "score": 118,
  "time": 1776312399,
  "title": "Darkbloom – Private inference on idle Macs",
  "type": "story",
  "url": "https://darkbloom.dev"
}

Trường kids chứa ID của các bình luận con trực tiếp. Mỗi bình luận cũng là một item riêng và có thể có kids của nó — đó chính là cấu trúc cây bình luận.

Bước 1: Lấy danh sách ID bài nổi bật

Tạo file tên scrape_hn_api.py:

import requests
import time
from pprint import pprint

API_BASE = "https://hacker-news.firebaseio.com/v0"

# Lấy ID các bài nổi bật
response = requests.get(f"{API_BASE}/topstories.json")
story_ids = response.json()

print(f"Got {len(story_ids)} top story IDs")
# Output: Got 500 top story IDs

500 ID bài viết chỉ trong một request — không cần parsing, không cần selector, chỉ là một mảng JSON.

Bước 2: Lấy chi tiết từng bài theo ID

Giờ ta cần dữ liệu bài viết thực sự. Đây là lúc xuất hiện vấn đề fan-out: 500 bài nghĩa là 500 API call riêng lẻ. Trong benchmark của tôi, mỗi request item mất khoảng 1,2 giây nếu chạy tuần tự. Với 500 bài, tức là gần 10 phút.

Với hầu hết trường hợp, bạn không cần tới cả 500. Đây là code lấy top 30 bài:

def fetch_story(story_id):
    """Lấy chi tiết của một bài từ HN API."""
    resp = requests.get(f"{API_BASE}/item/{story_id}.json")
    return resp.json()

# Lấy chi tiết cho 30 bài đứng đầu
stories = []
for sid in story_ids[:30]:
    story = fetch_story(sid)
    if story and story.get("type") == "story":
        stories.append({
            "title": story.get("title", ""),
            "url": story.get("url", ""),
            "score": story.get("score", 0),
            "author": story.get("by", ""),
            "comments": story.get("descendants", 0),
            "time": story.get("time", 0),
            "id": story.get("id"),
        })
    time.sleep(0.1)  # Lịch sự một chút — nghỉ ngắn giữa các request

# Sắp xếp theo điểm, hiển thị top 10
top = sorted(stories, key=lambda x: x["score"], reverse=True)[:10]
pprint(top)

time.sleep(0.1) thêm một khoảng nghỉ nhỏ để lịch sự hơn. Firebase API không công bố giới hạn tốc độ cụ thể, nhưng spam bất kỳ API nào mà không có khoảng nghỉ cũng là thói quen xấu.

Bước 3: Scrape bình luận (đi qua cây đệ quy)

Đây là lúc API tỏ ra vượt trội hơn hẳn so với scrape HTML. Bình luận trên HN có thể lồng rất sâu — trả lời của trả lời của trả lời. Nếu làm bằng HTML, bạn phải phân tích cấu trúc table lồng nhau rất phức tạp. Với API, trường kids của mỗi comment cho bạn ID của các comment con, rồi bạn chỉ cần đi cây dữ liệu theo kiểu đệ quy.

def fetch_comments(item_id, depth=0, max_depth=3):
    """Lấy bình luận đệ quy đến max_depth."""
    item = requests.get(f"{API_BASE}/item/{item_id}.json").json()
    if not item or item.get("type") != "comment":
        return []

    comments = [{
        "author": item.get("by", "[deleted]"),
        "text": item.get("text", ""),
        "depth": depth,
        "id": item.get("id"),
    }]

    if depth < max_depth and item.get("kids"):
        for kid_id in item["kids"]:
            comments.extend(fetch_comments(kid_id, depth + 1, max_depth))
            time.sleep(0.05)

    return comments

# Ví dụ: lấy bình luận của bài đứng đầu
if stories:
    top_story = stories[0]
    top_story_full = requests.get(f"{API_BASE}/item/{top_story['id']}.json").json()
    if top_story_full.get("kids"):
        print(f"\nComments for: {top_story['title']}")
        all_comments = []
        for kid_id in top_story_full["kids"][:5]:  # 5 bình luận cấp 1 đầu tiên
            all_comments.extend(fetch_comments(kid_id, depth=0, max_depth=2))
            time.sleep(0.1)

        for c in all_comments[:15]:
            indent = "  " * c["depth"]
            preview = c["text"][:80].replace("\n", " ") if c["text"] else "[no text]"
            print(f"{indent}[{c['author']}] {preview}...")

Cách đệ quy này dễ hơn rất nhiều so với việc cố phân tích thread bình luận HTML lồng nhau. Nếu bạn cần cây bình luận đầy đủ, API là lựa chọn nên dùng.

Bước 4: Chạy và xem kết quả

python scrape_hn_api.py

Bạn sẽ thấy dữ liệu bài viết có cấu trúc, rồi đến bản xem trước các bình luận lồng nhau. Dữ liệu sạch hơn, truy xuất bình luận dễ hơn, và bạn không phải lo scraper bị gãy chỉ vì HN đổi tên một CSS class.

Đi xa hơn trang 1: Phân trang và dữ liệu lịch sử

Phần lớn hướng dẫn scrape HN chỉ dừng ở trang 1 — 30 bài. Điều đó đủ cho demo nhanh, nhưng use case thực tế thường cần nhiều hơn.

Scrape nhiều trang bằng BeautifulSoup

Phân trang của HN dùng một pattern URL rất đơn giản: ?p=2, ?p=3, v.v. Mỗi trang trả về 30 bài, và site phục vụ tối đa khoảng trang 20 (tức khoảng 600 bài). Sau đó sẽ là trang trống.

import time

def scrape_hn_pages(num_pages=5):
    """Scrape nhiều trang bài viết trên trang chủ HN."""
    all_stories = []

    for page in range(1, num_pages + 1):
        url = f"https://news.ycombinator.com/news?p={page}"
        response = requests.get(url, headers=headers)
        soup = BeautifulSoup(response.text, "html.parser")

        story_rows = soup.select("tr.athing")
        if not story_rows:
            print(f"Page {page}: no stories found, stopping.")
            break

        for row in story_rows:
            title_tag = row.select_one("span.titleline > a")
            if not title_tag:
                continue
            meta_row = row.find_next_sibling("tr")
            score = 0
            if meta_row and (score_tag := meta_row.select_one("span.score")):
                score = int(score_tag.get_text().replace(" points", ""))

            all_stories.append({
                "title": title_tag.get_text(),
                "url": title_tag.get("href", ""),
                "score": score,
            })

        print(f"Page {page}: scraped {len(story_rows)} stories")

        # Tôn trọng crawl-delay 30 giây trong robots.txt
        if page < num_pages:
            time.sleep(30)

    return all_stories

stories = scrape_hn_pages(5)
print(f"\nTotal stories scraped: {len(stories)}")

time.sleep(30) rất quan trọng. robots.txt của HN yêu cầu rõ crawl delay 30 giây. Bỏ qua điều đó và bạn có thể bị giới hạn tốc độ (HTTP 429) hoặc tạm thời bị chặn. Năm trang với khoảng nghỉ 30 giây sẽ mất khoảng 2,5 phút — không tức thì, nhưng là cách làm có trách nhiệm.

Nếu bạn không muốn tự quản lý code phân trang, Thunderbit xử lý pagination kiểu click-based và infinite-scroll tự động. Nó bấm nút "More" ở cuối trang HN mà không cần bạn cấu hình gì cả.

Scrape Hacker News Pages with AI

Truy cập dữ liệu lịch sử Hacker News bằng Algolia API

Firebase API cho bạn dữ liệu hiện tại. Với phân tích lịch sử — như “Các bài Python nổi bật nhất trong năm 2023 là gì?” hoặc “Nội dung về AI đã thay đổi thế nào trong 5 năm qua?” — bạn cần HN Algolia Search API.

import requests

ALGOLIA_BASE = "https://hn.algolia.com/api/v1"

def search_hn(query, tags="story", page=0, hits_per_page=20):
    """Tìm kiếm HN qua Algolia API."""
    params = {
        "query": query,
        "tags": tags,
        "page": page,
        "hitsPerPage": hits_per_page,
    }
    resp = requests.get(f"{ALGOLIA_BASE}/search", params=params)
    return resp.json()

# Ví dụ: tìm các bài về Python scraping có từ 10 điểm trở lên từ tháng 1/2024
results = search_hn(
    query="python scraping",
    tags="story",
)
print(f"Found {results['nbHits']} total results")

for hit in results["hits"][:5]:
    print(f"  [{hit.get('points', 0)} pts] {hit['title']}")

Với truy vấn lọc theo ngày, dùng numericFilters:

import calendar, datetime

# Các bài từ ngày 1/1/2024
start_date = datetime.datetime(2024, 1, 1)
start_ts = int(calendar.timegm(start_date.timetuple()))

params = {
    "query": "python web scraping",
    "tags": "story",
    "numericFilters": f"created_at_i>{start_ts},points>10",
    "hitsPerPage": 50,
}
resp = requests.get(f"{ALGOLIA_BASE}/search_by_date", params=params)
data = resp.json()
print(f"Found {data['nbHits']} stories about Python web scraping since 2024 with >10 points")

Algolia API rất nhanh (thời gian xử lý phía server chỉ khoảng 5–9 ms), không cần API key, và hỗ trợ phân trang tới 500 trang. Với phân tích lịch sử quy mô lớn, đây là lựa chọn tốt nhất hiện có.

Xuất dữ liệu Hacker News sang CSV, Excel và Google Sheets

Hầu như bài hướng dẫn nào về scrape HN cũng kết thúc bằng output pprint() trong terminal. Việc đó ổn cho debug, nhưng nếu bạn đang xây bản tin hằng ngày hoặc phân tích xu hướng, bạn sẽ cần dữ liệu nằm trong file. Đây là cách làm điều đó.

Xuất sang CSV bằng Python

import csv

def export_to_csv(stories, filename="hn_stories.csv"):
    """Lưu các bài đã scrape vào file CSV."""
    fieldnames = ["title", "url", "score", "author", "comments"]
    with open(filename, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(stories)
    print(f"Saved {len(stories)} stories to {filename}")

export_to_csv(stories)

Xuất sang Excel bằng Python

import pandas as pd

def export_to_excel(stories, filename="hn_stories.xlsx"):
    """Lưu các bài đã scrape vào file Excel."""
    df = pd.DataFrame(stories)
    df.to_excel(filename, index=False, engine="openpyxl")
    print(f"Saved {len(stories)} stories to {filename}")

export_to_excel(stories)

Hãy chắc chắn rằng openpyxl đã được cài — pandas dùng nó làm engine cho Excel. Nếu thiếu, bạn sẽ gặp lỗi ImportError.

Đẩy lên Google Sheets (tùy chọn)

Với workflow tự động, bạn có thể muốn đẩy dữ liệu thẳng lên Google Sheets bằng thư viện gspread. Cách này cần thiết lập Google Cloud service account một lần:

import gspread

gc = gspread.service_account(filename="service_account.json")
sh = gc.open("HN Daily Digest")
worksheet = sh.sheet1

# Chuyển stories thành các dòng
header = list(stories[0].keys())
rows = [list(s.values()) for s in stories]

worksheet.clear()
worksheet.update([header] + rows)
print("Pushed to Google Sheets")

Lựa chọn xuất dữ liệu không cần code

Nếu việc thiết lập service account và viết code export còn phiền hơn cả bước scrape, tôi hiểu. Ở Thunderbit, chúng tôi xây dựng tính năng export dữ liệu miễn phí để bạn gửi dữ liệu đã scrape thẳng vào Excel, Google Sheets, Airtable hoặc Notion — không cần code, không cần credentials, không cần tự duy trì pipeline. Với một lần lấy dữ liệu đơn lẻ, cách này thật ra nhanh hơn rất nhiều. Tôi sẽ nói thêm ở phần dưới.

Biến scraper thành thứ sẵn sàng cho production: Xử lý lỗi, cache và lên lịch

Nếu bạn chỉ chạy scraper một lần cho vui, code ở trên là đủ. Nếu bạn chạy hằng ngày như một phần của workflow, bạn sẽ cần thêm vài mảnh ghép nữa.

Xử lý lỗi và logic retry

Mạng có thể lỗi. Server có thể giới hạn. Một request hỏng không nên làm cả quá trình scrape sập theo. Đây là một hàm retry với exponential backoff:

from tenacity import retry, stop_after_attempt, wait_exponential_jitter
import requests

@retry(stop=stop_after_attempt(5), wait=wait_exponential_jitter(initial=1, max=60))
def fetch_with_retry(url):
    """Lấy URL với cơ chế retry tự động và exponential backoff."""
    response = requests.get(url, timeout=10)
    response.raise_for_status()
    return response

# Cách dùng:
try:
    resp = fetch_with_retry("https://hacker-news.firebaseio.com/v0/topstories.json")
    story_ids = resp.json()
except Exception as e:
    print(f"Failed after retries: {e}")

Thư viện tenacity xử lý retry rất gọn. Nó sẽ thử lại tối đa 5 lần với backoff mũ có jitter — bắt đầu từ 1 giây và tối đa 60 giây. Cách này xử lý 429 (bị giới hạn tốc độ), 503 (dịch vụ không khả dụng), và lỗi mạng tạm thời khá tốt.

Cache phản hồi để không phải crawl lại

Trong lúc phát triển, bạn sẽ chạy scraper nhiều lần để chỉnh logic parsing. Không có cache, mỗi lần chạy lại gọi HN server cho cùng một dữ liệu. Thư viện requests-cache giải quyết việc này chỉ bằng hai dòng:

import requests_cache

requests_cache.install_cache("hn_cache", expire_after=3600)  # Cache trong 1 giờ

Sau khi thêm hai dòng này ở đầu script, mọi lệnh requests.get() sẽ tự động được cache trong cơ sở dữ liệu SQLite cục bộ. Chạy lại script 10 lần trong một giờ, chỉ lần đầu tiên mới thực sự chạm vào mạng. Đây là công cụ mà người dùng trên forum thường khuyên dùng, và có lý do cả.

Tách bước crawl và bước parsing

Một pattern mà những người làm scraping lâu năm rất thích: tải dữ liệu thô trước, parse sau. Như vậy, nếu logic parsing có lỗi, bạn chỉ cần sửa rồi parse lại mà không phải fetch lại từ đầu.

import os, json

def crawl_and_save(story_ids, output_dir="raw_data"):
    """Lấy dữ liệu bài viết và lưu JSON thô xuống đĩa."""
    os.makedirs(output_dir, exist_ok=True)
    for sid in story_ids:
        filepath = os.path.join(output_dir, f"{sid}.json")
        if os.path.exists(filepath):
            continue  # Bỏ qua item đã lấy rồi
        resp = fetch_with_retry(f"{API_BASE}/item/{sid}.json")
        with open(filepath, "w") as f:
            json.dump(resp.json(), f)

def parse_saved_data(input_dir="raw_data"):
    """Parse các file JSON đã lưu thành danh sách bài viết có cấu trúc."""
    stories = []
    for filename in os.listdir(input_dir):
        with open(os.path.join(input_dir, filename)) as f:
            item = json.load(f)
        if item and item.get("type") == "story":
            stories.append({
                "title": item.get("title", ""),
                "url": item.get("url", ""),
                "score": item.get("score", 0),
                "author": item.get("by", ""),
                "comments": item.get("descendants", 0),
            })
    return stories

Cách làm hai bước này đặc biệt hữu ích khi bạn scrape hàng trăm item và muốn chỉnh cách xử lý dữ liệu thật nhanh.

Tự động chạy scraper theo lịch

Với bản tin HN hằng ngày, scraper của bạn cần chạy tự động. Có hai lựa chọn phổ biến:

Lựa chọn 1: cron (Linux/Mac)

# Chạy mỗi ngày lúc 8:30 sáng UTC
30 8 * * * /usr/bin/python3 /home/user/scrape_hn.py >> /home/user/scrape.log 2>&1

Lựa chọn 2: GitHub Actions (miễn phí, không cần server)

name: Scrape Hacker News

on:
  schedule:
    - cron: '30 8 * * *'  # Hằng ngày lúc 8:30 sáng UTC
  workflow_dispatch:        # Nút chạy thủ công

jobs:
  scrape:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v6
        with:
          python-version: '3.12'
      - run: pip install requests beautifulsoup4 pandas openpyxl
      - run: python scrape_hn.py
      - run: |
          git config user.name "GitHub Actions Bot"
          git config user.email "actions@github.com"
          git add -A
          git diff --staged --quiet || git commit -m "Update HN data $(date -u +%Y-%m-%dT%H:%M:%SZ)"
          git push

Một vài lưu ý với lịch chạy của GitHub Actions: mọi cron time đều dùng UTC, thường có độ trễ 15–60 phút (nên dùng mốc lệch phút như :30 thay vì :00), và GitHub có thể tắt workflow theo lịch nếu repo không có hoạt động trong 60 ngày. Luôn thêm workflow_dispatch để có thể chạy thủ công khi test.

Nếu muốn đơn giản hơn, tính năng Scheduled Scraper của Thunderbit cho phép bạn mô tả lịch bằng ngôn ngữ tự nhiên — kiểu như “scrape mỗi sáng lúc 8 giờ” — mà không cần server hay cấu hình cron.

Khi Python là quá mức cần thiết: Cách no-code để scrape Hacker News

Nói thật nhé, dù tôi thích Python và team tôi cũng làm công cụ cho developer. Nếu bạn chỉ cần 100 bài top HN hôm nay trong bảng tính — ngay bây giờ, chỉ một lần — thì việc viết, debug và chạy một script Python là hơi thừa. Chỉ riêng phần setup (virtual environment, cài package, tìm selector) đã lâu hơn cả thời gian lấy dữ liệu.

Đó là lúc Thunderbit phát huy tác dụng. Workflow như sau:

  1. Mở news.ycombinator.com trong Chrome
  2. Nhấn icon extension Thunderbit, rồi chọn "AI Suggest Fields"
  3. AI đọc trang và đề xuất các cột: Title, URL, Score, Author, Comment Count, Time Posted
  4. Chỉnh lại field nếu muốn (đổi tên, xóa, hoặc thêm field tùy chỉnh — bạn thậm chí có thể thêm prompt AI như "Phân loại thành AI/DevTools/Web/Khác")
  5. Nhấn "Scrape" — dữ liệu xuất hiện trong bảng có cấu trúc
  6. Export sang Excel, Google Sheets, Airtable hoặc Notion

Hai cú nhấp là có dữ liệu có cấu trúc. Không selector, không code, không bảo trì.

Một lợi thế rất thực tế: AI của Thunderbit tự thích nghi với thay đổi layout. Scraper dùng CSS selector truyền thống sẽ gãy khi site đổi markup — và dù HTML của HN khá ổn định, nó vẫn từng thay đổi (class class="athing submission" đã được cập nhật, span.titleline thay cho a.storylink cũ). Scraper có AI sẽ đọc lại trang từ đầu mỗi lần, nên không quan tâm class name đổi thế nào.

python-vs-thunderbit-comparison.webp

Thunderbit cũng xử lý phân trang (tự động bấm nút "More" của HN) và scrape subpage (vào trang bình luận của từng bài để lấy dữ liệu thảo luận). Với use case làm giàu dữ liệu bình luận, nó tương đương với đoạn code API đệ quy ở Cách 2 — nhưng không cần viết một dòng nào.

Lựa chọn nào cũng có đánh đổi rõ ràng: Python phù hợp khi bạn cần logic tùy biến, biến đổi dữ liệu phức tạp, pipeline tự động theo lịch, hoặc đang học lập trình. Thunderbit phù hợp khi bạn cần dữ liệu nhanh, không muốn bảo trì code, hoặc không phải developer. Hãy chọn công cụ khớp với tình huống của bạn.

Python vs. API vs. No-Code: Nên chọn cách nào?

Đây là khung ra quyết định đầy đủ:

Tiêu chíBeautifulSoup (HTML)Firebase APIAlgolia APIThunderbit (No-Code)
Trình độ kỹ thuật cần cóPython trung cấpPython cơ bảnPython cơ bảnKhông cần
Thời gian thiết lập10–15 phút5–10 phút5–10 phút2 phút
Gánh nặng bảo trìTrung bình (selector có thể hỏng)Thấp (JSON ổn định)Thấp (JSON ổn định)Không có
Độ sâu dữ liệuChỉ trang chủBất kỳ item, userTìm kiếm + lịch sửTrang chủ + subpage
Bình luậnKhóDễ (đệ quy)Dễ (cây lồng nhau)Scrape subpage
Dữ liệu lịch sửKhôngKhôngCó (toàn bộ archive)Không
Tùy chọn exportTự codeTự codeTự codeCó sẵn (Excel, Sheets, v.v.)
Lên lịchcron / GitHub Actionscron / GitHub Actionscron / GitHub ActionsCó sẵn scheduler
Phù hợp nhất vớiHọc scrapingPipeline đáng tin cậyNghiên cứu & phân tíchLấy dữ liệu nhanh

Nếu bạn đang học Python hoặc xây thứ gì đó tùy biến, hãy chọn Cách 1 hoặc 2. Nếu bạn cần phân tích lịch sử, thêm Algolia API. Nếu bạn chỉ muốn dữ liệu mà không muốn code, hãy thử Thunderbit.

Thử Thunderbit để scrape Hacker News

Kết luận và những điểm chính cần nhớ

Giờ bạn đã có trong tay:

  • Hai phương pháp Python hoàn chỉnh để scrape Hacker News — BeautifulSoup cho phân tích HTML và Firebase API cho dữ liệu JSON sạch sẽ
  • Kỹ thuật phân trang để scrape vượt qua trang 1, bao gồm Algolia API cho dữ liệu lịch sử từ năm 2007
  • Code export sang CSV, Excel và Google Sheets — vì dữ liệu nằm trong terminal thì chẳng giúp ích gì cho cả team
  • Các pattern cho production — retry logic, caching, tách crawl và parse, và tự động hóa theo lịch qua cron hoặc GitHub Actions
  • Một lựa chọn no-code cho lúc Python là công cụ nhiều hơn mức bạn cần

Khuyến nghị của tôi: hãy bắt đầu với Firebase API (Cách 2) cho đa số use case. Nó sạch hơn, ổn định hơn, và cho bạn truy cập bình luận mà không phải vất vả phân tích HTML lồng nhau. Thêm Algolia API khi bạn cần dữ liệu lịch sử. Và nhớ bookmark Thunderbit cho những lúc bạn chỉ cần một file bảng tính nhanh mà không muốn dựng cả dự án Python.

Nếu muốn đào sâu hơn, hãy thử scrape bình luận HN cho phân tích cảm xúc, xây pipeline bản tin hằng ngày với GitHub Actions, hoặc khám phá Algolia API để theo dõi cách xu hướng công nghệ đã thay đổi trong thập kỷ qua.

Thử Thunderbit để scrape Hacker News nhanh hơn Get Started Free

Câu hỏi thường gặp

Có hợp pháp khi scrape Hacker News không?

Dữ liệu của HN là công khai, và Y Combinator có cung cấp API chính thức cho việc truy cập theo chương trình. robots.txt của site cho phép scrape nội dung chỉ đọc (trang chủ, trang item, trang user) nhưng yêu cầu crawl delay 30 giây. Hãy tôn trọng khoảng nghỉ đó, đừng scrape các endpoint tương tác (bỏ phiếu, đăng nhập), và bạn đang ở vùng an toàn. Để biết thêm về khía cạnh đạo đức scraping, xem hướng dẫn web scraping legal implications của chúng tôi.

Hacker News có API chính thức không?

Có. HN Firebase API tại hacker-news.firebaseio.com/v0/ là miễn phí, không cần xác thực, và cho phép truy cập stories, comments, hồ sơ người dùng, cùng tất cả loại feed (top, new, best, ask, show, jobs). API trả về JSON sạch sẽ và không công bố giới hạn tốc độ cụ thể, dù vậy việc gửi request có chừng mực luôn được khuyến nghị.

Làm sao scrape bình luận Hacker News bằng Python?

Dùng Firebase API, lấy một story item để có trường kids (mảng ID của comment cấp một). Mỗi comment cũng là một item riêng, với trường kids chứa các reply. Hãy đi cây dữ liệu theo đệ quy bằng một hàm lấy từng comment và con của nó. Xem phần “Scrape bình luận (đi qua cây đệ quy)” ở trên để có code đầy đủ. Ngoài ra, endpoint /items/<id> của Algolia API trả về toàn bộ cây bình luận lồng nhau chỉ trong một request — nhanh hơn nhiều với các bài nhiều bình luận.

Có thể scrape Hacker News mà không cần code không?

Có. AI web scraper của Thunderbit hoạt động như một Chrome extension — mở HN, nhấn "AI Suggest Fields", và nó tự nhận diện các cột như title, URL, score, author. Nhấn "Scrape" rồi export thẳng sang Excel, Google Sheets, Airtable hoặc Notion. Công cụ này xử lý phân trang và thậm chí có thể vào subpage để lấy dữ liệu bình luận. Không cần Python, không cần selector, không cần bảo trì.

Làm sao lấy dữ liệu Hacker News lịch sử?

HN Algolia Search API là công cụ tốt nhất cho việc này. Dùng endpoint search_by_date với numericFilters=created_at_i>TIMESTAMP để lọc theo khoảng thời gian. Bạn có thể tìm theo từ khóa, lọc theo loại story, và phân trang tới 500 trang kết quả. Với phân tích lịch sử quy mô lớn, cũng có các bộ dữ liệu công khai trên Google BigQuery (toàn bộ archive), ClickHouse (28 triệu bản ghi), và Hugging Face (4 triệu story).

Tìm hiểu thêm

Shuai Guan
Shuai Guan
CEO tại Thunderbit | Chuyên gia Tự động hóa Dữ liệu AI Shuai Guan là CEO của Thunderbit và là cựu sinh viên ngành Kỹ thuật của University of Michigan. Với gần một thập kỷ kinh nghiệm trong lĩnh vực công nghệ và kiến trúc SaaS, anh chuyên biến các mô hình AI phức tạp thành những công cụ trích xuất dữ liệu thực tiễn, không cần viết mã. Trên blog này, anh chia sẻ những góc nhìn thẳng thắn, đã được kiểm chứng qua thực chiến về web scraping và các chiến lược tự động hóa, giúp bạn xây dựng quy trình làm việc thông minh hơn, dựa trên dữ liệu. Khi không tối ưu hóa quy trình dữ liệu, anh áp dụng sự tỉ mỉ đó vào niềm đam mê nhiếp ảnh.
Mục lục
Thunderbit · Tác nhân dữ liệu web AI

Trích xuất dữ liệu từ bất kỳ trang nào trong 1 lần nhấp

Được hơn 250.000+ người dùng tin tưởng
có gói miễn phí
Từ trang web đến bảng tính
Mô tả điều bạn cần — AI Agent của Thunderbit sẽ scrape và xuất sang Excel, Google Sheets, Airtable hoặc Notion. Bắt đầu miễn phí.
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week