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ụng | Kết quả nhận được |
|---|---|
| Bản tin công nghệ hằng ngày | Cá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ướng | Theo 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ụng | Phâ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 dung | Tì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:
- 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. - 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 API | Thunderbit (No-Code) |
|---|---|---|---|
| Độ phức tạp khi thiết lập | Trung 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ệu | Real-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ận | Khó (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 API | Không áp dụng |
| Phù hợp nhất với | Học scraping nền tảng | Pipeline dữ liệu ổn định | Ngườ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)

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à URLspan.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.subtextcó 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 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.scorecó 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:
| Endpoint | Trả về | Ví dụ |
|---|---|---|
/v0/topstories.json | Mảng tối đa 500 ID bài nổi bật | [47788542, 47787901, ...] |
/v0/newstories.json | Tối đa 500 ID bài mới nhất | Cùng định dạng |
/v0/beststories.json | Tối đa 500 ID bài tốt nhất | Cùng định dạng |
/v0/askstories.json | Tối đa 200 ID bài "Ask HN" | Cùng định dạng |
/v0/showstories.json | Tối đa 200 ID bài "Show HN" | Cùng định dạng |
/v0/jobstories.json | Tối đa 200 ID bài tuyển dụng | Cùng định dạng |
/v0/item/{id}.json | Chi tiết đầy đủ của bất kỳ item nào (story, comment, poll) | Đối tượng JSON |
/v0/user/{username}.json | Hồ sơ người dùng | Đối tượng JSON |
/v0/maxitem.json | ID item lớn nhất hiện tại | Số 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:
- Mở
news.ycombinator.comtrong Chrome - Nhấn icon extension Thunderbit, rồi chọn "AI Suggest Fields"
- AI đọc trang và đề xuất các cột: Title, URL, Score, Author, Comment Count, Time Posted
- 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")
- Nhấn "Scrape" — dữ liệu xuất hiện trong bảng có cấu trúc
- 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.

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 API | Algolia API | Thunderbit (No-Code) |
|---|---|---|---|---|
| Trình độ kỹ thuật cần có | Python trung cấp | Python cơ bản | Python cơ bản | Không cần |
| Thời gian thiết lập | 10–15 phút | 5–10 phút | 5–10 phút | 2 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ệu | Chỉ trang chủ | Bất kỳ item, user | Tìm kiếm + lịch sử | Trang chủ + subpage |
| Bình luận | Khó | Dễ (đệ quy) | Dễ (cây lồng nhau) | Scrape subpage |
| Dữ liệu lịch sử | Không | Không | Có (toàn bộ archive) | Không |
| Tùy chọn export | Tự code | Tự code | Tự code | Có sẵn (Excel, Sheets, v.v.) |
| Lên lịch | cron / GitHub Actions | cron / GitHub Actions | cron / GitHub Actions | Có sẵn scheduler |
| Phù hợp nhất với | Học scraping | Pipeline đáng tin cậy | Nghiên cứu & phân tích | Lấ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


