Cách Scrape Goodreads bằng Python (Hết Lỗi Kết Quả Rỗng)

Cập nhật lần cuối vào April 16, 2026

Không có gì “tụt mood” hơn việc bạn vừa viết 30 dòng Python, chạy Goodreads scraper, rồi nhận về []. Một danh sách trống. Không có gì. Chỉ còn mình bạn với con trỏ nhấp nháy.

Tôi đã thấy cảnh này lặp đi lặp lại hàng chục lần — trong các thử nghiệm nội bộ của chúng tôi tại , trên các diễn đàn dev, và trong những issue chất đống ở các repo scraper bị bỏ quên. Lời than phiền gần như luôn giống nhau: "phần top reviews bị trống, chỉ hiện ra []", "chạy page số nào thì nó cũng scrape đúng trang đầu tiên", "năm ngoái code vẫn chạy, giờ thì hỏng rồi." Tệ hơn nữa, Goodreads API đã bị ngừng từ tháng 12/2020, nên lời khuyên kiểu “cứ dùng API đi” trong các bài hướng dẫn cũ giờ đã hết đường áp dụng.

Nếu bạn muốn lấy dữ liệu sách có cấu trúc từ Goodreads hiện nay — tiêu đề, tác giả, đánh giá, review, thể loại, ISBN — thì scraping gần như là lựa chọn chính. Bài viết này sẽ dẫn bạn qua một quy trình hoàn chỉnh, có thể dùng thật để scrape Goodreads bằng Python, bao gồm nội dung render bằng JS, phân trang, chống chặn và xuất dữ liệu. Còn nếu bạn không muốn đụng tới Python, tôi cũng sẽ chỉ bạn một cách no-code để làm việc này chỉ trong khoảng hai cú click.

Scrape Goodreads là gì, và vì sao nên làm bằng Python?

Scrape Goodreads nghĩa là tự động trích xuất dữ liệu sách — tiêu đề, tác giả, đánh giá, số lượng review, thể loại, ISBN, số trang, ngày xuất bản và nhiều thông tin khác — từ các trang Goodreads bằng code, thay vì copy thủ công.

Goodreads là một trong những cơ sở dữ liệu sách lớn nhất thế giới, với hơn và khoảng . Mỗi tháng có hơn 18 triệu cuốn sách được thêm vào danh sách “Want to Read”. Chính nguồn dữ liệu có cấu trúc, được cập nhật liên tục như vậy khiến các nhà xuất bản, nhà khoa học dữ liệu, người bán sách và nhà nghiên cứu luôn quay lại.

Python là ngôn ngữ được ưu tiên cho kiểu công việc này — nó đang đứng sau khoảng các dự án scraping. Hệ sinh thái thư viện rất trưởng thành (requests, BeautifulSoup, Selenium, Playwright, pandas), cú pháp thân thiện với người mới, và cộng đồng thì cực lớn.

Nếu bạn chưa từng scrape website trước đây, Python là nơi đáng bắt đầu nhất.

Vì sao scrape Goodreads bằng Python? Các tình huống thực tế

Trước khi vào code, câu hỏi đáng hỏi là: ai thật sự cần dữ liệu này, và họ dùng nó để làm gì?

Trường hợp sử dụngAi hưởng lợiBạn scrape những gì
Nghiên cứu thị trường cho nhà xuất bảnNhà xuất bản, đại lý văn họcThể loại đang lên, đầu sách điểm cao, tác giả mới nổi, điểm số đối thủ
Hệ thống gợi ý sáchNhà khoa học dữ liệu, người làm hobby, nhà phát triển appĐiểm đánh giá, thể loại, kệ sách người dùng, cảm xúc trong review
Theo dõi giá và tồn khoNgười bán sách ecommerceĐầu sách hot, số lượng review, số lượt “Want to Read”
Nghiên cứu học thuậtNhà nghiên cứu, sinh viênNội dung review, phân bố điểm số, phân loại thể loại
Phân tích thói quen đọcBook blogger, dự án cá nhânDữ liệu kệ sách cá nhân, lịch sử đọc, thống kê theo năm

Một vài ví dụ rất cụ thể: UCSD Book Graph — một trong những bộ dữ liệu học thuật được trích dẫn nhiều nhất trong nghiên cứu recommender — chứa , tất cả đều được thu thập từ các shelf công khai trên Goodreads. Nhiều bộ dữ liệu trên Kaggle (goodbooks-10k, Best Books Ever, v.v.) cũng bắt nguồn từ việc scrape Goodreads. Và một nghiên cứu năm 2025 trên Big Data and Society đã tổng hợp theo phương pháp tính toán để phân tích cách review được tài trợ ảnh hưởng đến nền tảng.

Ở góc độ thương mại, Bright Data đang bán các bộ dữ liệu Goodreads đã scrape sẵn với giá chỉ từ 0,50 USD cho mỗi 1.000 bản ghi — đủ để thấy dữ liệu này có giá trị thị trường thật.

Goodreads API đã không còn — đây là thứ thay thế nó

Nếu gần đây bạn tìm “Goodreads API”, rất có thể bạn đã rơi vào một bài hướng dẫn lỗi thời. Ngày 8/12/2020, Goodreads âm thầm ngừng cấp API key mới cho developer. Không có bài blog thông báo, không có email hàng loạt — chỉ có một banner nhỏ trên trang docs và rất nhiều developer hoang mang.

goodreads-data-access-tools.webp

Hệ quả đến rất nhanh. Một developer tên Kyle K đã làm một Discord bot để chia sẻ gợi ý sách — “đùng một cái POOF, nó ngừng hoạt động.” Một người khác, Matthew Jones, mất quyền truy cập API chỉ một tuần trước cuộc bỏ phiếu Reddit r/Fantasy Stabby Awards, buộc phải quay lại dùng Google Forms. Một nghiên cứu sinh tên Elena Neacsu thì dự án thesis thạc sĩ bị chệch hướng ngay giữa quá trình phát triển.

Vậy hiện giờ còn gì? Bức tranh hiện tại trông như sau:

Cách tiếp cậnDữ liệu có thể lấyMức độ dễ dùngGiới hạn tốc độTrạng thái
Goodreads APIMetadata đầy đủ, reviewDễ (đã từng)1 req/giâyĐã bị ngừng (12/2020) — không có key mới
Open Library APITiêu đề, tác giả, ISBN, ảnh bìa (~30M đầu sách)Dễ1-3 req/giâyĐang hoạt động, miễn phí, không cần auth
Google Books APIMetadata, bản xem trướcDễ1.000/ngày miễn phíĐang hoạt động (thiếu ISBN không phải tiếng Anh)
Python scraping (requests + BS4)Bất kỳ thứ gì có trong HTML ban đầuTrung bìnhTự quản lýDùng tốt cho nội dung tĩnh
Python scraping (Selenium/Playwright)Cả nội dung render bằng JSKhó hơnTự quản lýCần cho review, một số danh sách
Thunderbit (Chrome extension no-code)Mọi dữ liệu hiển thị trên trangRất dễ (2 cú click)Tính theo creditĐang hoạt động — không cần Python

Open Library là một nguồn bổ trợ rất tốt, nhất là khi cần tra ISBN và metadata cơ bản. Nhưng nếu bạn cần rating, review, tag thể loại, hoặc số lượt “Want to Read”, thì bạn vẫn sẽ phải scrape trực tiếp Goodreads — либо bằng Python, hoặc bằng một công cụ như Thunderbit, có thể scrape các trang Goodreads (kể cả các subpage chi tiết sách) với các trường gợi ý bởi AI và xuất thẳng sang Google Sheets, Notion hoặc Airtable.

Vì sao Python scraper của bạn trả về kết quả rỗng, và cách sửa

Đây là phần mà tôi ước đã tồn tại khi mới bắt đầu làm việc với dữ liệu Goodreads. Vấn đề “kết quả rỗng” là lời phàn nàn phổ biến nhất trên forum developer, và nó có nhiều nguyên nhân khác nhau — mỗi nguyên nhân lại có cách xử lý riêng.

Triệu chứngNguyên nhân gốcCách sửa
Review/đánh giá trả về []Nội dung render bằng JS (React/lazy-load)Dùng Selenium hoặc Playwright thay vì requests
Luôn chỉ scrape trang 1Bỏ qua tham số phân trang hoặc do JS điều khiểnTruyền đúng ?page=N; dùng automation trình duyệt cho infinite scroll
Code năm ngoái chạy được, giờ lỗiGoodreads đổi tên class HTMLDùng selector bền hơn (JSON-LD, thuộc tính data-testid)
Lỗi 403/bị chặn sau vài requestThiếu header / gửi request quá nhanhThêm User-Agent, time.sleep(), xoay proxy
Bị chặn đăng nhập ở trang shelf/listCần cookie/sessionDùng requests.Session() với cookie hoặc scrape bằng trình duyệt

Nội dung render bằng JS: review và rating hiện ra trống

Goodreads đang dùng frontend dựa trên React. Khi bạn gọi requests.get() tới một trang sách, bạn chỉ lấy được HTML ban đầu — còn review, phân bố rating và nhiều phần “more info” được tải bất đồng bộ bằng JavaScript. Nói cách khác, scraper của bạn không hề “nhìn thấy” chúng.

Cách khắc phục: với bất kỳ trang nào cần nội dung render bằng JS, hãy chuyển sang Selenium hoặc Playwright. Với dự án mới, tôi khuyên dùng Playwright — nó nhanh hơn nhờ giao thức dựa trên WebSocket, đồng thời có khả năng stealth và hỗ trợ async tốt hơn.

troubleshooting-empty-array-causes.webp

Phân trang chỉ trả về trang 1

Đây là lỗi khá khó chịu. Bạn viết một vòng lặp, tăng ?page=N, nhưng lần nào kết quả cũng y hệt nhau. Trên Goodreads, các trang shelf đôi khi lặng lẽ trả về nội dung của trang 1 bất kể tham số ?page= nếu bạn chưa xác thực. Không báo lỗi, không redirect — chỉ lặp lại đúng trang đầu tiên.

Cách sửa: thêm cookie session đã xác thực (cụ thể là _session_id2). Tôi sẽ nói kỹ hơn ở phần phân trang bên dưới.

Code chạy được năm ngoái, giờ lại lỗi

Goodreads thỉnh thoảng đổi tên class HTML và cấu trúc trang. Repo maria-antoniak/goodreads-scraper khá nổi tiếng trên GitHub hiện đã gắn thông báo cố định: "This project is unmaintained and no longer functioning." Cách sửa là dùng selector ổn định hơn — dữ liệu có cấu trúc JSON-LD (theo chuẩn schema.org và hiếm khi thay đổi) hoặc thuộc tính data-testid thay vì những class name dễ gãy.

Lỗi 403 hoặc bị chặn

Thư viện requests của Python có TLS fingerprint khác Chrome. Ngay cả khi bạn giả User-Agent của Chrome, các hệ thống chống bot như AWS WAF (Goodreads dùng vì là công ty con của Amazon) vẫn có thể phát hiện sự không khớp. Cách sửa: thêm header trình duyệt thật hơn, chèn độ trễ time.sleep() từ 3-8 giây giữa các request, và cân nhắc dùng curl_cffi để khớp TLS fingerprint nếu bạn scraping ở quy mô lớn.

Bị chặn đăng nhập ở shelf và list page

Một số trang shelf và list của Goodreads cần đăng nhập để xem đầy đủ nội dung, nhất là sau page 5. Hãy dùng requests.Session() với cookie export từ trình duyệt, hoặc dùng Selenium/Playwright với profile đã đăng nhập. Thunderbit xử lý chuyện này tự nhiên vì nó chạy ngay trong Chrome bạn đang đăng nhập sẵn.

Trước khi bắt đầu

  • Mức độ khó: Trung cấp (giả định bạn đã biết Python cơ bản)
  • Thời gian cần: Khoảng 20-30 phút cho toàn bộ hướng dẫn
  • Bạn cần có:
    • Python 3.8+
    • Chrome browser (để kiểm tra bằng DevTools và dùng Selenium/Playwright)
    • Thư viện: requests, beautifulsoup4, selenium hoặc playwright, pandas
    • (Tuỳ chọn) gspread để xuất sang Google Sheets
    • (Tuỳ chọn) cho phương án no-code

goodreads-scraping-flow.webp

Bước 1: Thiết lập môi trường Python

Cài các thư viện cần thiết. Mở terminal và chạy:

1pip install requests beautifulsoup4 selenium pandas lxml

Nếu bạn thích Playwright hơn (được khuyên dùng cho dự án mới):

1pip install playwright
2playwright install chromium

Nếu muốn xuất sang Google Sheets (tuỳ chọn):

1pip install gspread oauth2client

Hãy đảm bảo bạn đang dùng Python 3.8 trở lên. Có thể kiểm tra bằng python --version.

Sau khi cài xong, bạn phải import được toàn bộ thư viện mà không có lỗi. Thử lệnh python -c "import requests, bs4, pandas; print('Ready')" để xác nhận.

Bước 2: Gửi request đầu tiên với header phù hợp

Mở một trang genre shelf hoặc list của Goodreads trong trình duyệt — ví dụ https://www.goodreads.com/list/show/1.Best_Books_Ever. Giờ ta sẽ lấy trang đó bằng Python.

1import requests
2from bs4 import BeautifulSoup
3headers = {
4    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
5                  "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
6    "Accept-Language": "en-US,en;q=0.9",
7    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
8}
9url = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
10response = requests.get(url, headers=headers, timeout=15)
11print(f"Status: {response.status_code}")

Bạn nên thấy Status: 200. Nếu nhận 403, hãy kiểm tra lại header — AWS WAF của Goodreads sẽ kiểm tra User-Agent đủ giống trình duyệt thật hay chưa, và có thể chặn các request quá “sơ sài”. Header bên trên mô phỏng một phiên Chrome thật.

Bước 3: Kiểm tra trang và tìm selector đúng

Mở Chrome DevTools (F12) trên trang list của Goodreads. Click chuột phải vào một tiêu đề sách và chọn “Inspect”. Bạn sẽ thấy cấu trúc DOM của từng mục sách.

Với trang list, mỗi cuốn sách thường được bọc trong một thẻ <tr>itemtype="http://schema.org/Book". Bên trong sẽ có:

  • Tiêu đề: a.bookTitle (text của link là tiêu đề, href là URL của sách)
  • Tác giả: a.authorName
  • Đánh giá: span.minirating (chứa điểm trung bình và số lượt đánh giá)
  • Ảnh bìa: img trong hàng sách

Với trang chi tiết từng cuốn, đừng sa đà vào CSS selector — hãy đi thẳng vào JSON-LD. Goodreads nhúng dữ liệu có cấu trúc trong thẻ <script type="application/ld+json"> theo định dạng schema.org Book. Nó ổn định hơn rất nhiều so với class name, thứ mà Goodreads có thể đổi bất cứ lúc nào.

Bước 4: Trích xuất dữ liệu sách từ một trang list

Giờ hãy parse trang list và lấy thông tin cơ bản của từng cuốn:

1import requests
2from bs4 import BeautifulSoup
3headers = {
4    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
5                  "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
6    "Accept-Language": "en-US,en;q=0.9",
7}
8url = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
9response = requests.get(url, headers=headers, timeout=15)
10soup = BeautifulSoup(response.text, "lxml")
11books = []
12rows = soup.select('tr[itemtype="http://schema.org/Book"]')
13for row in rows:
14    title_tag = row.select_one("a.bookTitle")
15    author_tag = row.select_one("a.authorName")
16    rating_tag = row.select_one("span.minirating")
17    title = title_tag.get_text(strip=True) if title_tag else ""
18    book_url = "https://www.goodreads.com" + title_tag["href"] if title_tag else ""
19    author = author_tag.get_text(strip=True) if author_tag else ""
20    rating_text = rating_tag.get_text(strip=True) if rating_tag else ""
21    books.append({
22        "title": title,
23        "author": author,
24        "rating_info": rating_text,
25        "book_url": book_url,
26    })
27print(f"Found {len(books)} books on page 1")
28for b in books[:3]:
29    print(b)

Bạn sẽ thấy khoảng 100 cuốn mỗi trang list. Mỗi mục sẽ có tiêu đề, tác giả, chuỗi rating kiểu “4.28 avg rating — 9,031,257 ratings,” và URL dẫn đến trang chi tiết của sách.

Bước 5: Scrape subpage để lấy thông tin chi tiết về sách

Trang list chỉ cho bạn thông tin cơ bản, nhưng dữ liệu giá trị nhất — ISBN, mô tả đầy đủ, tag thể loại, số trang, ngày xuất bản — lại nằm trên trang riêng của từng cuốn. Đây là lúc JSON-LD phát huy sức mạnh.

1import json
2import time
3def scrape_book_detail(book_url, headers):
4    """Truy cập một trang sách và trích xuất metadata chi tiết qua JSON-LD."""
5    resp = requests.get(book_url, headers=headers, timeout=15)
6    if resp.status_code != 200:
7        return {}
8    soup = BeautifulSoup(resp.text, "lxml")
9    script = soup.find("script", {"type": "application/ld+json"})
10    if not script:
11        return {}
12    data = json.loads(script.string)
13    agg = data.get("aggregateRating", {})
14    # Tag thể loại không nằm trong JSON-LD; nên dùng HTML fallback
15    genres = [g.get_text(strip=True) for g in soup.select('span.BookPageMetadataSection__genreButton a span')]
16    return {
17        "isbn": data.get("isbn", ""),
18        "pages": data.get("numberOfPages", ""),
19        "language": data.get("inLanguage", ""),
20        "format": data.get("bookFormat", ""),
21        "avg_rating": agg.get("ratingValue", ""),
22        "rating_count": agg.get("ratingCount", ""),
23        "review_count": agg.get("reviewCount", ""),
24        "description": data.get("description", "")[:200],  # cắt ngắn để xem trước
25        "genres": ", ".join(genres[:5]),
26    }
27# Ví dụ: làm giàu dữ liệu cho 3 cuốn đầu tiên
28for book in books[:3]:
29    details = scrape_book_detail(book["book_url"], headers)
30    book.update(details)
31    print(f"Scraped: {book['title']} — ISBN: {book.get('isbn', 'N/A')}")
32    time.sleep(4)  # tôn trọng rate limit

Hãy chèn time.sleep() từ 3-8 giây giữa các request. Goodreads bắt đầu giới hạn khoảng 20-30 request mỗi phút từ một IP, và bạn sẽ thấy 403 hoặc CAPTCHA nếu đi quá nhanh.

Cách làm hai bước này — đầu tiên thu thập toàn bộ URL sách từ trang list, sau đó truy cập từng trang chi tiết — đáng tin cậy hơn và dễ tiếp tục nếu bị gián đoạn. Đây cũng là chiến lược mà các Goodreads scraper thành công thường dùng.

Lưu ý thêm: có thể làm việc này tự động bằng tính năng scrape subpage. AI sẽ vào từng trang chi tiết của sách và làm giàu bảng dữ liệu của bạn với ISBN, mô tả, thể loại và nhiều hơn nữa — không code, không vòng lặp, không cần sleep timer.

Bước 6: Xử lý nội dung render bằng JavaScript với Selenium

Với những trang mà nội dung bạn cần được tải bằng JavaScript — review, phân bố rating, phần “more details” — bạn sẽ cần một công cụ automation trình duyệt. Ví dụ dưới đây dùng Selenium:

1from selenium import webdriver
2from selenium.webdriver.chrome.options import Options
3from selenium.webdriver.common.by import By
4from selenium.webdriver.support.ui import WebDriverWait
5from selenium.webdriver.support import expected_conditions as EC
6options = Options()
7options.add_argument("--headless=new")
8options.add_argument("--disable-blink-features=AutomationControlled")
9options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
10                     "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36")
11driver = webdriver.Chrome(options=options)
12driver.get("https://www.goodreads.com/book/show/5907.The_Hobbit")
13# Chờ review tải xong
14try:
15    WebDriverWait(driver, 10).until(
16        EC.presence_of_element_located((By.CSS_SELECTOR, "article.ReviewCard"))
17    )
18except:
19    print("Reviews did not load — page may require login or JS timed out")
20# Parse trang sau khi đã render đầy đủ
21page_source = driver.page_source
22soup = BeautifulSoup(page_source, "lxml")
23reviews = soup.select("article.ReviewCard")
24for rev in reviews[:3]:
25    text = rev.select_one("span.Formatted")
26    stars = rev.select_one("span.RatingStars")
27    print(f"Rating: {stars.get_text(strip=True) if stars else 'N/A'}")
28    print(f"Review: {text.get_text(strip=True)[:150] if text else 'N/A'}...")
29    print()
30driver.quit()

Khi nào nên dùng Selenium, khi nào dùng requests:

  • Dùng requests + BeautifulSoup cho metadata sách (JSON-LD), trang list, trang shelf (page 1), và dữ liệu Choice Awards
  • Dùng Selenium hoặc Playwright cho review, phân bố rating, và mọi nội dung không xuất hiện trong HTML thô

Playwright thường là lựa chọn tốt hơn cho dự án mới — nhanh hơn, ít tốn bộ nhớ hơn, mặc định stealth tốt hơn. Nhưng Selenium có cộng đồng lớn hơn và nhiều ví dụ code sẵn cho Goodreads hơn.

Phân trang đúng cách: scrape toàn bộ danh sách Goodreads

Phân trang là điểm fail phổ biến nhất của Goodreads scraper, và tôi chưa thấy tutorial nào khác giải thích nó đúng cách. Đây là cách làm chuẩn.

URL phân trang của Goodreads hoạt động thế nào

Goodreads dùng tham số ?page=N khá rõ ràng cho hầu hết trang có phân trang:

  • Lists: https://www.goodreads.com/list/show/1.Best_Books_Ever?page=2
  • Shelves: https://www.goodreads.com/shelf/show/thriller?page=2
  • Search: https://www.goodreads.com/search?q=fantasy&page=2

Mỗi trang list thường hiển thị 100 cuốn. Shelf thường hiển thị 50 cuốn mỗi trang.

Viết vòng lặp phân trang biết khi nào dừng

1import time
2all_books = []
3base_url = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
4for page_num in range(1, 50):  # giới hạn an toàn ở 50 trang
5    url = f"{base_url}?page={page_num}"
6    resp = requests.get(url, headers=headers, timeout=15)
7    if resp.status_code != 200:
8        print(f"Page {page_num}: got status {resp.status_code}, stopping.")
9        break
10    soup = BeautifulSoup(resp.text, "lxml")
11    rows = soup.select('tr[itemtype="http://schema.org/Book"]')
12    if not rows:
13        print(f"Page {page_num}: no books found, reached the end.")
14        break
15    for row in rows:
16        title_tag = row.select_one("a.bookTitle")
17        author_tag = row.select_one("a.authorName")
18        title = title_tag.get_text(strip=True) if title_tag else ""
19        book_url = "https://www.goodreads.com" + title_tag["href"] if title_tag else ""
20        author = author_tag.get_text(strip=True) if author_tag else ""
21        all_books.append({"title": title, "author": author, "book_url": book_url})
22    print(f"Page {page_num}: scraped {len(rows)} books (total: {len(all_books)})")
23    time.sleep(5)  # nghỉ 5 giây giữa mỗi trang
24print(f"\nDone. Total books collected: {len(all_books)}")

Bạn có thể nhận ra trang cuối bằng cách kiểm tra danh sách kết quả rỗng (không còn phần tử tr[itemtype="http://schema.org/Book"]) hoặc kiểm tra sự vắng mặt của link “next” (a.next_page).

Trường hợp đặc biệt: cần đăng nhập sau page 5

Đây là cái bẫy khiến gần như ai cũng vấp: một số trang shelf và list của Goodreads sẽ âm thầm trả về nội dung page 1 khi bạn request page 6+ mà chưa xác thực. Không lỗi, không redirect — chỉ lặp lại cùng một dữ liệu.

Để sửa, hãy export cookie _session_id2 từ trình duyệt của bạn (dùng extension xuất cookie hoặc Chrome DevTools > Application > Cookies) rồi gắn vào request:

1session = requests.Session()
2session.headers.update(headers)
3session.cookies.set("_session_id2", "YOUR_SESSION_COOKIE_VALUE_HERE", domain=".goodreads.com")
4# Sau đó dùng session.get() thay cho requests.get()
5resp = session.get(f"{base_url}?page=6", timeout=15)

Thunderbit xử lý cả pagination kiểu click và infinite scroll một cách tự nhiên, không cần code hay quản lý cookie. Nếu logic phân trang của bạn cứ liên tục bị gãy, đáng để cân nhắc.

Script Python đầy đủ, có thể copy-paste

Dưới đây là script tổng hợp hoàn chỉnh. Nó xử lý header, phân trang, scrape subpage qua JSON-LD, giới hạn tốc độ và xuất CSV. Tôi đã kiểm thử nó với các trang Goodreads thực tế vào giữa năm 2025.

1"""
2goodreads_scraper.py — Scrape một danh sách Goodreads có phân trang và làm giàu dữ liệu chi tiết sách.
3Cách dùng: python goodreads_scraper.py
4Output: goodreads_books.csv
5"""
6import csv, json, time, requests
7from bs4 import BeautifulSoup
8HEADERS = {
9    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
10                  "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
11    "Accept-Language": "en-US,en;q=0.9",
12    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
13}
14BASE_URL = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
15MAX_PAGES = 3          # chỉnh theo nhu cầu
16DELAY_LISTING = 5      # số giây giữa các trang list
17DELAY_DETAIL = 4       # số giây giữa các trang chi tiết
18OUTPUT_FILE = "goodreads_books.csv"
19def scrape_listing_page(url):
20    """Trả về danh sách dict gồm title, author, book_url từ một trang list."""
21    resp = requests.get(url, headers=HEADERS, timeout=15)
22    if resp.status_code != 200:
23        return []
24    soup = BeautifulSoup(resp.text, "lxml")
25    rows = soup.select('tr[itemtype="http://schema.org/Book"]')
26    books = []
27    for row in rows:
28        t = row.select_one("a.bookTitle")
29        a = row.select_one("a.authorName")
30        if t:
31            books.append({
32                "title": t.get_text(strip=True),
33                "author": a.get_text(strip=True) if a else "",
34                "book_url": "https://www.goodreads.com" + t["href"],
35            })
36    return books
37def scrape_book_detail(book_url):
38    """Truy cập trang sách và trích xuất metadata qua JSON-LD + HTML fallback."""
39    resp = requests.get(book_url, headers=HEADERS, timeout=15)
40    if resp.status_code != 200:
41        return {}
42    soup = BeautifulSoup(resp.text, "lxml")
43    script = soup.find("script", {"type": "application/ld+json"})
44    if not script:
45        return {}
46    data = json.loads(script.string)
47    agg = data.get("aggregateRating", {})
48    genres = [g.get_text(strip=True)
49              for g in soup.select("span.BookPageMetadataSection__genreButton a span")]
50    return {
51        "isbn": data.get("isbn", ""),
52        "pages": data.get("numberOfPages", ""),
53        "avg_rating": agg.get("ratingValue", ""),
54        "rating_count": agg.get("ratingCount", ""),
55        "review_count": agg.get("reviewCount", ""),
56        "description": (data.get("description", "") or "")[:300],
57        "genres": ", ".join(genres[:5]),
58        "language": data.get("inLanguage", ""),
59        "format": data.get("bookFormat", ""),
60        "published": data.get("datePublished", ""),
61    }
62def main():
63    all_books = []
64    # --- Pass 1: thu thập URL sách từ các trang list ---
65    for page in range(1, MAX_PAGES + 1):
66        url = f"{BASE_URL}?page={page}"
67        page_books = scrape_listing_page(url)
68        if not page_books:
69            print(f"Page {page}: empty — stopping pagination.")
70            break
71        all_books.extend(page_books)
72        print(f"Page {page}: {len(page_books)} books (total: {len(all_books)})")
73        time.sleep(DELAY_LISTING)
74    # --- Pass 2: làm giàu từng sách bằng dữ liệu trang chi tiết ---
75    for i, book in enumerate(all_books):
76        details = scrape_book_detail(book["book_url"])
77        book.update(details)
78        print(f"[{i+1}/{len(all_books)}] {book['title']} — ISBN: {book.get('isbn', 'N/A')}")
79        time.sleep(DELAY_DETAIL)
80    # --- Xuất CSV ---
81    if all_books:
82        fieldnames = list(all_books[0].keys())
83        with open(OUTPUT_FILE, "w", newline="", encoding="utf-8") as f:
84            writer = csv.DictWriter(f, fieldnames=fieldnames)
85            writer.writeheader()
86            writer.writerows(all_books)
87        print(f"\nSaved {len(all_books)} books to {OUTPUT_FILE}")
88    else:
89        print("No books scraped.")
90if __name__ == "__main__":
91    main()

Với MAX_PAGES = 3, script này sẽ thu thập khoảng 300 cuốn từ danh sách “Best Books Ever”, ghé từng trang chi tiết của sách, rồi ghi toàn bộ ra CSV. Trên máy của tôi, nó mất khoảng 25 phút (chủ yếu do độ trễ 4 giây giữa các request trang chi tiết). File CSV đầu ra sẽ có các cột như title, author, book_url, isbn, pages, avg_rating, rating_count, review_count, description, genres, language, format, và published.

Xuất ngoài CSV: Google Sheets với gspread

Nếu bạn muốn dữ liệu nằm trong Google Sheets thay vì CSV, hoặc thêm vào đó, hãy thêm đoạn này sau phần xuất CSV:

1import gspread
2from oauth2client.service_account import ServiceAccountCredentials
3scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"]
4creds = ServiceAccountCredentials.from_json_keyfile_name("credentials.json", scope)
5client = gspread.authorize(creds)
6sheet = client.open("Goodreads Scrape").sheet1
7header = list(all_books[0].keys())
8sheet.append_row(header)
9for book in all_books:
10    sheet.append_row([str(book.get(k, "")) for k in header])
11print("Data pushed to Google Sheets.")

Bạn sẽ cần một Google Cloud service account đã bật Sheets API và Drive API. hướng dẫn phần này chỉ mất khoảng 5 phút. Nếu bạn đẩy hơn vài trăm dòng, hãy dùng batch operations (append_rows() với danh sách các list) — giới hạn API của Google là 300 request mỗi 60 giây cho mỗi project.

Tất nhiên, nếu mọi thao tác setup này khiến bạn thấy quá rườm rà, Thunderbit có thể xuất sang Google Sheets, Airtable, Notion, Excel, CSV và JSON chỉ với — không cần cài thư viện, không cần file credentials, không cần lo quota API.

Phương án no-code: scrape Goodreads bằng Thunderbit

Không phải ai cũng muốn duy trì một Python script. Có thể bạn là nhà xuất bản cần làm một phân tích thị trường một lần, hoặc một book blogger chỉ muốn có file bảng tính của các bestseller năm nay. Đó chính là tình huống Thunderbit được tạo ra để giải quyết.

Cách scrape Goodreads bằng Thunderbit

  1. Cài tiện ích Thunderbit Chrome extension từ rồi mở một trang list, shelf hoặc search results của Goodreads.
  2. Nhấn “AI Suggest Fields” trong thanh bên của Thunderbit. AI sẽ đọc trang và đề xuất các cột — thường là title, author, rating, URL ảnh bìa và link sách.
  3. Nhấn “Scrape” — dữ liệu sẽ được trích xuất thành bảng có cấu trúc chỉ trong vài giây.
  4. Xuất dữ liệu sang Google Sheets, Excel, Airtable, Notion, CSV hoặc JSON.

Với dữ liệu chi tiết từng sách (ISBN, mô tả, thể loại, số trang), tính năng subpage scraping của Thunderbit sẽ tự động vào từng trang chi tiết và làm giàu bảng dữ liệu cho bạn — không vòng lặp, không sleep timer, không cần debug.

Thunderbit cũng hỗ trợ phân trang sẵn. Bạn chỉ cần bảo nó click “Next” hoặc scroll, và nó sẽ gom dữ liệu từ mọi trang mà không cần viết code.

Đổi lại thì khá đơn giản: script Python cho bạn toàn quyền kiểm soát và miễn phí (trừ thời gian của bạn), còn Thunderbit hy sinh một chút linh hoạt để đổi lấy tiết kiệm thời gian cực lớn và gần như không phải bảo trì. Với một list 300 cuốn, script Python mất khoảng 25 phút chạy thực tế cộng thêm thời gian bạn viết và gỡ lỗi. Thunderbit lấy cùng dữ liệu đó trong khoảng 3 phút, chỉ với hai cú click.

Scrape Goodreads có trách nhiệm: robots.txt, Điều khoản sử dụng và đạo đức

Phần này xứng đáng được trả lời thẳng thắn, chứ không phải một đoạn disclaimer cho có.

robots.txt của Goodreads thực sự nói gì

robots.txt hiện tại của Goodreads khá cụ thể. Các trang chi tiết sách (/book/show/), list công khai (/list/show/), shelf công khai (/shelf/show/) và trang tác giả (/author/show/) không bị chặn. Những gì bị chặn là: /api, /book/reviews/, /review/list, /review/show, /search, cùng một số đường dẫn khác. GPTBot và CCBot (Common Crawl) bị chặn hoàn toàn bằng Disallow: /. Có directive Crawl-delay: 5 cho bingbot, nhưng không có độ trễ chung cho mọi bot.

Điều khoản sử dụng của Goodreads nói gì, theo cách dễ hiểu

ToS (bản sửa gần nhất ngày 28/4/2021) cấm “bất kỳ việc sử dụng data mining, robots hoặc các công cụ thu thập và trích xuất dữ liệu tương tự.” Ngôn ngữ này khá rộng, và đáng để tôn trọng — nhưng các phán quyết của tòa án nhìn chung không xem vi phạm ToS đơn thuần là “truy cập trái phép” theo nghĩa hình sự. Phán quyết từng nói rằng “hình sự hóa các vi phạm điều khoản sử dụng có nguy cơ biến mỗi website thành một khu vực tài phán hình sự riêng.”

Thực hành tốt nhất

  • Chậm request lại: 3-8 giây giữa mỗi request (robots.txt của Goodreads chính họ cũng gợi ý 5 giây cho bot)
  • Giữ dưới 5.000 request/ngày từ một IP
  • Chỉ scrape các trang công khai — tránh thu thập hàng loạt dữ liệu chỉ hiển thị sau đăng nhập
  • Đừng phân phối lại text review thô vì mục đích thương mại — review là tác phẩm sáng tạo có bản quyền
  • Chỉ lưu những gì cần thiết và đặt lịch lưu trữ/xóa dữ liệu hợp lý
  • Mục đích nghiên cứu cá nhân vs. thương mại: Scrape dữ liệu công khai cho phân tích cá nhân hoặc nghiên cứu học thuật thường được chấp nhận hơn. Rủi ro pháp lý tăng lên khi bạn tái phân phối thương mại.

Dùng một công cụ như Thunderbit (scrape qua phiên trình duyệt của chính bạn) khiến thao tác trông giống hệt việc duyệt web bình thường, nhưng các nguyên tắc đạo đức vẫn áp dụng như nhau, bất kể bạn chọn công cụ nào. Nếu bạn muốn tìm hiểu sâu hơn về , chúng tôi đã viết riêng một bài về chủ đề này.

Mẹo và những lỗi thường gặp

Mẹo: Luôn bắt đầu từ JSON-LD. Trước khi viết selector CSS phức tạp, hãy kiểm tra xem dữ liệu bạn cần có nằm trong thẻ <script type="application/ld+json"> hay không. Nó ổn định hơn, dễ parse hơn và ít vỡ khi Goodreads cập nhật frontend.

Mẹo: Dùng chiến lược hai bước. Đầu tiên thu thập toàn bộ URL sách từ các trang list, sau đó vào từng trang chi tiết. Cách này giúp scraper dễ resume nếu bị crash giữa chừng, và bạn có thể lưu danh sách URL ra disk làm checkpoint.

Lỗi thường gặp: Quên xử lý field bị thiếu. Không phải trang sách nào cũng có ISBN, tag thể loại hay mô tả. Hãy luôn dùng .get() với giá trị mặc định, hoặc bọc selector trong điều kiện if. Chỉ một lỗi NoneType cũng đủ làm sập một job scraping kéo dài 3 tiếng.

Lỗi thường gặp: Chạy quá nhanh. Tôi biết là bạn rất muốn đặt time.sleep(0.5) rồi chạy vèo qua. Nhưng Goodreads sẽ bắt đầu trả về 403 sau khoảng 20-30 request liên tiếp, và một khi đã bị gắn cờ, bạn có thể phải chờ hàng giờ hoặc đổi IP. Mức 4-5 giây là khá tối ưu.

Lỗi thường gặp: Tin vào tutorial cũ. Nếu hướng dẫn nào đó còn nhắc tới Goodreads API, hoặc dùng class name kiểu .field.value hay #bookTitle, thì nhiều khả năng nó đã lỗi thời. Hãy luôn đối chiếu selector với trang live trước khi xây scraper.

Nếu bạn muốn đọc thêm về cách chọn công cụ và framework phù hợp, hãy xem các hướng dẫn của chúng tôi về .

Kết luận và các ý chính cần nhớ

Scrape Goodreads bằng Python hoàn toàn khả thi — bạn chỉ cần biết những chỗ dễ “dính bẫy”. Tóm lại:

  • Goodreads API đã biến mất (từ tháng 12/2020). Scraping là cách chính để lấy dữ liệu sách có cấu trúc từ nền tảng này.
  • Kết quả rỗng gần như luôn do nội dung render bằng JS, selector cũ, thiếu header, hoặc vấn đề xác thực khi phân trang — chứ không phải do code của bạn sai hẳn.
  • JSON-LD là bạn tốt nhất khi lấy metadata sách. Nó ổn định, có cấu trúc và hiếm khi thay đổi.
  • Phân trang ở nhiều shelf và list page sau page 5 cần xác thực. Hãy thêm cookie _session_id2.
  • Giới hạn tốc độ là có thật. Hãy dùng độ trễ 3-8 giây và giữ dưới 5.000 request/ngày.
  • Chiến lược hai bước (lấy URL trước, rồi scrape trang chi tiết sau) đáng tin cậy hơn và dễ resume hơn.
  • Với người không code (hoặc ai muốn tiết kiệm cả buổi chiều), xử lý gần như toàn bộ việc này — render JS, phân trang, làm giàu subpage và xuất dữ liệu — chỉ trong khoảng hai cú click.

Scrape có trách nhiệm, tôn trọng robots.txt, và mong rằng dữ liệu sách của bạn sẽ luôn trả về nhiều hơn [].

FAQs

Còn dùng Goodreads API được không?

Không. Goodreads đã ngừng API công khai từ tháng 12/2020 và không còn cấp developer key mới. Những key cũ nếu không hoạt động trong 30 ngày cũng bị vô hiệu hóa tự động. Hiện nay, web scraping hoặc các API thay thế (như Open Library hoặc Google Books) là lựa chọn khả thi để truy cập dữ liệu sách bằng code.

Vì sao Goodreads scraper của tôi lại trả về kết quả rỗng?

Nguyên nhân phổ biến nhất là nội dung render bằng JavaScript. Goodreads tải review, phân bố rating và nhiều phần chi tiết qua React/JavaScript, nên một lệnh requests.get() đơn giản không thể thấy chúng. Hãy chuyển sang Selenium hoặc Playwright cho những trang đó. Các nguyên nhân khác gồm selector CSS đã cũ (do Goodreads đổi HTML), thiếu header User-Agent (gây chặn 403), hoặc request không xác thực ở các trang shelf có phân trang.

Scrape Goodreads có hợp pháp không?

Scrape dữ liệu công khai cho mục đích cá nhân hoặc nghiên cứu thường được xem là chấp nhận được theo các tiền lệ pháp lý hiện nay (hiQ v. LinkedIn, Meta v. Bright Data). Tuy nhiên, Điều khoản sử dụng của Goodreads cấm thu thập dữ liệu tự động, và bạn nên luôn xem lại robots.txt của họ. Tránh tái phân phối thương mại nội dung review có bản quyền, và giới hạn tần suất request để không làm quá tải hệ thống.

Làm sao scrape nhiều trang trên Goodreads?

Chỉ cần thêm ?page=N vào URL shelf hoặc list rồi lặp qua các số trang. Hãy kiểm tra kết quả rỗng hoặc việc không còn link “next” để biết trang cuối. Lưu ý: một số trang shelf yêu cầu xác thực (cookie _session_id2) để trả kết quả sau page 5 — nếu không có cookie này, bạn sẽ âm thầm nhận lại dữ liệu của trang 1 lặp đi lặp lại.

Có thể scrape Goodreads mà không cần viết code không?

Có. là một Chrome extension cho phép bạn scrape Goodreads chỉ trong hai cú click — AI gợi ý các trường dữ liệu, bạn nhấn “Scrape”, rồi export thẳng sang Google Sheets, Excel, Airtable hoặc Notion. Nó xử lý nội dung render bằng JavaScript, phân trang và làm giàu subpage tự động, không cần Python hay kiến thức lập trình.

Tìm hiểu thêm

Mục lục

Thử Thunderbit

Trích xuất lead và dữ liệu khác chỉ trong 2 cú nhấp. Powered by AI.

Nhận Thunderbit Miễn phí
Trích xuất dữ liệu bằng AI
Dễ dàng chuyển dữ liệu sang Google Sheets, Airtable hoặc Notion
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week