Niewiele rzeczy potrafi bardziej zniechęcić niż napisanie 30 linii kodu w Pythonie, uruchomienie scrapera Goodreads i zobaczenie wyniku []. Pusta lista. Nic. Tylko Ty i migający kursor.
Widziałem to już dziesiątki razy — w naszych wewnętrznych testach w , na forach dla programistów i w zgłoszeniach na GitHubie, które piętrzą się w porzuconych repozytoriach scraperów. Skargi są prawie zawsze takie same: „sekcja z top recenzjami jest pusta, pokazuje tylko []”, „bez względu na numer strony zawsze pobiera pierwszą”, „kod działał w zeszłym roku, teraz jest zepsuty”. A sytuacji nie poprawia fakt, że API Goodreads zostało wycofane w grudniu 2020 roku, więc rada „po prostu użyj API”, którą znajdziesz w starszych poradnikach, prowadzi donikąd.
Jeśli dziś potrzebujesz uporządkowanych danych książek z Goodreads — tytułów, autorów, ocen, recenzji, gatunków, ISBN — scraping jest główną drogą. Ten poradnik pokaże Ci działające, kompletne podejście do scrapowania Goodreads w Pythonie: od treści renderowanych przez JS, przez paginację i zabezpieczenia przed blokadą, po eksport danych. A jeśli Python to nie Twoja bajka, pokażę też alternatywę bez kodowania, która załatwia sprawę w dosłownie dwa kliknięcia.
Czym jest scrapowanie Goodreads i dlaczego robić to w Pythonie?
Scrapowanie Goodreads polega na automatycznym pobieraniu danych o książkach — tytułów, autorów, ocen, liczby recenzji, gatunków, ISBN, liczby stron, dat publikacji i wielu innych informacji — bezpośrednio ze stron Goodreads, zamiast kopiować je ręcznie.
Goodreads to jedna z największych baz książek na świecie, z ponad i około . Co miesiąc na półki „Want to Read” trafia ponad 18 milionów książek. Takie stale aktualizowane, uporządkowane dane są właśnie tym, po co wydawcy, analitycy danych, księgarze i badacze wracają do Goodreads.
Python to najczęściej wybierany język do takich zadań — odpowiada za około wszystkich projektów scrapujących. Biblioteki są dojrzałe (requests, BeautifulSoup, Selenium, Playwright, pandas), składnia przyjazna dla początkujących, a społeczność ogromna.
Jeśli jeszcze nigdy nie scrapowałeś strony internetowej, Python to świetny punkt startowy.
Dlaczego warto scrapować Goodreads w Pythonie? Praktyczne zastosowania
Zanim przejdziemy do kodu, warto zadać pytanie: komu właściwie są potrzebne te dane i co z nimi robi?
| Przypadek użycia | Kto korzysta | Co pobierasz |
|---|---|---|
| Badania rynku wydawniczego | Wydawcy, agenci literaccy | Popularne gatunki, wysoko oceniane tytuły, nowi autorzy, oceny konkurencji |
| Systemy rekomendacji książek | Analitycy danych, hobbyści, twórcy aplikacji | Oceny, gatunki, półki użytkowników, sentyment recenzji |
| Monitorowanie cen i stanów magazynowych | Sprzedawcy książek online | Popularne tytuły, liczba recenzji, liczba zapisów „Want to Read” |
| Badania akademickie | Naukowcy, studenci | Treść recenzji, rozkład ocen, klasyfikacje gatunków |
| Analiza czytelnicza | Blogerzy książkowi, projekty osobiste | Dane z własnych półek, historia czytania, statystyki roczne |
Kilka konkretnych przykładów: UCSD Book Graph — jeden z najczęściej cytowanych akademickich zbiorów danych w badaniach nad rekomendacjami — zawiera , zebranych z publicznie dostępnych półek Goodreads. Kilka zbiorów na Kaggle (goodbooks-10k, Best Books Ever itd.) również powstało na bazie scrapowania Goodreads. A badanie z 2025 roku w Big Data and Society skomputeryzowało i uporządkowało , aby przeanalizować wpływ recenzji sponsorowanych na platformę.
Po stronie komercyjnej Bright Data sprzedaje gotowe, wcześniej zeskrobane zbiory danych Goodreads już od 0,50 USD za 1000 rekordów — to najlepszy dowód, że te dane mają realną wartość rynkową.
API Goodreads zniknęło — co jest teraz zamiast niego?
Jeśli ostatnio szukałeś „Goodreads API”, zapewne trafiłeś na poradnik, który jest już nieaktualny. 8 grudnia 2020 roku Goodreads po cichu przestało wydawać nowe klucze API dla deweloperów. Bez wpisu na blogu, bez wiadomości e-mail — tylko mały baner na stronie dokumentacji i sporo zdezorientowanych programistów.

Skutki były natychmiastowe. Jeden z deweloperów, Kyle K, zbudował bota na Discorda do dzielenia się rekomendacjami książek — „nagle PUF i przestaje działać”. Inny, Matthew Jones, stracił dostęp do API tydzień przed głosowaniem do Reddit r/Fantasy Stabby Awards, co zmusiło go do powrotu do Google Forms. Elena Neacsu, doktorantka, miała po drodze rozjechany projekt pracy magisterskiej.
Co więc zostało? Dzisiejszy krajobraz wygląda tak:
| Podejście | Dostępne dane | Łatwość użycia | Limity | Status |
|---|---|---|---|---|
| Goodreads API | Pełne metadane, recenzje | Łatwe (kiedyś) | 1 req/s | Wycofane (grudzień 2020) — brak nowych kluczy |
| Open Library API | Tytuły, autorzy, ISBN, okładki (~30 mln tytułów) | Łatwe | 1–3 req/s | Aktywne, darmowe, bez autoryzacji |
| Google Books API | Metadane, podglądy | Łatwe | 1000/dzień za darmo | Aktywne (braki w ISBN dla nieangielskich) |
| Scrapowanie w Pythonie (requests + BS4) | Wszystko w początkowym HTML | Średnie | Zarządzane samodzielnie | Działa dla treści statycznych |
| Scrapowanie w Pythonie (Selenium/Playwright) | Także treści renderowane przez JS | Trudniejsze | Zarządzane samodzielnie | Wymagane dla recenzji i części list |
| Thunderbit (rozszerzenie Chrome bez kodu) | Dowolne widoczne dane ze strony | Bardzo łatwe (2 kliknięcia) | System punktowy | Aktywne — bez Pythona |
Open Library to dobre uzupełnienie, szczególnie przy wyszukiwaniu ISBN i podstawowych metadanych. Ale jeśli potrzebujesz ocen, recenzji, tagów gatunków albo liczby zapisów „Want to Read”, pobierasz dane bezpośrednio z Goodreads — albo w Pythonie, albo narzędziem takim jak Thunderbit, które potrafi scrapować strony Goodreads (w tym podstrony z detalami książek) z polami podpowiadanymi przez AI i eksportem prosto do Google Sheets, Notion lub Airtable.
Dlaczego Twój scraper Goodreads w Pythonie zwraca puste wyniki i jak to naprawić?
To jest sekcja, którą chciałbym mieć przed sobą, kiedy pierwszy raz pracowałem z danymi Goodreads. Problem „pustych wyników” to najczęstsza skarga na forach deweloperskich i ma kilka różnych przyczyn — każda z własnym rozwiązaniem.
| Objaw | Przyczyna | Rozwiązanie |
|---|---|---|
Recenzje/oceny zwracają [] | Treść renderowana przez JS (React/lazy-load) | Użyj Selenium albo Playwright zamiast requests |
| Zawsze pobiera tylko stronę 1 | Parametry paginacji ignorowane lub sterowane przez JS | Prawidłowo przekazuj ?page=N; przy infinite scroll użyj automatyzacji przeglądarki |
| Kod działał w zeszłym roku, teraz nie działa | Goodreads zmieniło nazwy klas HTML | Używaj odpornych selektorów (JSON-LD, atrybutów data-testid) |
| 403 / blokada po kilku żądaniach | Brak nagłówków / zbyt szybkie żądania | Dodaj User-Agent, time.sleep(), rotuj proxy |
| Logowanie wymagane na stronach półek/list | Potrzebny cookie/sesja | Użyj requests.Session() z ciasteczkami albo scrapingu w przeglądarce |
Treść renderowana przez JS: recenzje i oceny są puste
Frontend Goodreads działa w oparciu o React. Gdy wywołasz requests.get() na stronie książki, dostajesz początkowy HTML — ale recenzje, rozkład ocen i wiele sekcji „więcej informacji” ładuje się asynchronicznie przez JavaScript. Twój scraper po prostu nie jest w stanie tego zobaczyć.
Rozwiązanie: dla każdej strony, na której potrzebujesz treści renderowanej przez JS, przejdź na Selenium albo Playwright. Playwright polecam do nowych projektów — jest dzięki protokołowi opartemu na WebSocketach i ma lepsze wbudowane mechanizmy ukrywania automatyzacji oraz obsługi async.

Paginacja, która zwraca tylko stronę 1
To podstępny problem. Piszesz pętlę, zwiększasz ?page=N, a mimo to za każdym razem dostajesz te same wyniki. Na Goodreads strony półek potrafią po cichu zwracać zawartość strony 1 niezależnie od parametru ?page=, jeśli nie jesteś zalogowany. Bez błędu, bez przekierowania — po prostu ta sama pierwsza strona w kółko.
Rozwiązanie: dodaj uwierzytelnione ciasteczko sesyjne (konkretnie _session_id2). Więcej o tym w sekcji o paginacji niżej.
Kod działał rok temu, a teraz nie działa
Goodreads okresowo zmienia nazwy klas HTML i strukturę stron. Popularne repozytorium maria-antoniak/goodreads-scraper na GitHubie ma dziś stałą informację: „This project is unmaintained and no longer functioning.” Rozwiązanie to korzystanie z bardziej odpornych selektorów — struktur JSON-LD (zgodnych ze schema.org i rzadko zmienianych) albo atrybutów data-testid zamiast kruchych nazw klas.
Błędy 403 lub blokada
Biblioteka requests w Pythonie ma inny odcisk TLS niż Chrome. Nawet przy nagłówku User-Agent udającym Chrome systemy wykrywania botów, takie jak AWS WAF (które Goodreads wykorzystuje jako spółka zależna Amazona), mogą wychwycić różnicę. Rozwiązanie: dodaj realistyczne nagłówki przeglądarki, wprowadź opóźnienia time.sleep() rzędu 3–8 sekund między żądaniami i przy większej skali rozważ curl_cffi, jeśli zależy Ci na zgodności odcisku TLS.
Ściany logowania na półkach i listach
Niektóre strony półek i list Goodreads wymagają uwierzytelnienia, by zobaczyć pełną zawartość, zwłaszcza po stronie 5. Użyj requests.Session() z ciasteczkami wyeksportowanymi z przeglądarki albo Selenium/Playwright z zalogowanym profilem. Thunderbit radzi sobie z tym naturalnie, ponieważ działa w Twojej własnej, zalogowanej przeglądarce Chrome.
Zanim zaczniesz
- Poziom trudności: średnio zaawansowany (zakładamy podstawy Pythona)
- Czas potrzebny: ok. 20–30 minut na pełny walkthrough
- Czego potrzebujesz:
- Python 3.8+
- Przeglądarka Chrome (do inspekcji DevTools i Selenium/Playwright)
- Biblioteki:
requests,beautifulsoup4,seleniumlubplaywright,pandas - (Opcjonalnie)
gspreaddo eksportu do Google Sheets - (Opcjonalnie) jako alternatywa bez kodu

Krok 1: Skonfiguruj środowisko Pythona
Zainstaluj potrzebne biblioteki. Otwórz terminal i uruchom:
1pip install requests beautifulsoup4 selenium pandas lxml
Jeśli wolisz Playwright (polecany w nowych projektach):
1pip install playwright
2playwright install chromium
Do eksportu do Google Sheets (opcjonalnie):
1pip install gspread oauth2client
Upewnij się, że masz Python 3.8 lub nowszy. Sprawdzisz to poleceniem python --version.
Po instalacji powinieneś móc zaimportować wszystkie biblioteki bez błędów. Spróbuj python -c "import requests, bs4, pandas; print('Ready')", żeby to potwierdzić.
Krok 2: Wyślij pierwsze żądanie z poprawnymi nagłówkami
Wejdź w przeglądarce na stronę listy lub półki Goodreads — na przykład https://www.goodreads.com/list/show/1.Best_Books_Ever. Teraz pobierzmy tę stronę w Pythonie.
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}")
Powinieneś zobaczyć Status: 200. Jeśli pojawi się 403, sprawdź nagłówki — AWS WAF Goodreads oczekuje realistycznego User-Agent i odrzuci zbyt „gołe” żądania. Powyższe nagłówki imitują prawdziwą sesję Chrome.
Krok 3: Zbadaj stronę i znajdź właściwe selektory
Otwórz DevTools w Chrome (F12) na stronie listy Goodreads. Kliknij prawym na tytuł książki i wybierz „Inspect”. Zobaczysz strukturę DOM dla każdego wpisu.
Na stronach list pojedyncza książka jest zwykle otoczona elementem <tr> z itemtype="http://schema.org/Book". W środku znajdziesz:
- Tytuł:
a.bookTitle(tekst linku to tytuł, ahrefdaje URL książki) - Autor:
a.authorName - Ocena:
span.minirating(zawiera średnią ocenę i liczbę ocen) - Okładka:
imgwewnątrz wiersza książki
Na stronach pojedynczych książek lepiej pominąć selektory CSS i przejść od razu do JSON-LD. Goodreads osadza ustrukturyzowane dane w tagu <script type="application/ld+json">, zgodnym z formatem Book ze schema.org. Jest znacznie stabilniejszy niż nazwy klas, które Goodreads zmienia wedle uznania.
Krok 4: Pobierz dane o książkach z jednej strony listy
Przeanalizujmy stronę listy i wyciągnijmy podstawowe informacje o każdej książce:
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"Znaleziono {len(books)} książek na stronie 1")
28for b in books[:3]:
29 print(b)
Powinieneś zobaczyć około 100 książek na stronę listy. Każdy wpis będzie miał tytuł, autora, ciąg z oceną typu „4.28 avg rating — 9,031,257 ratings” oraz URL do strony szczegółów książki.
Krok 5: Zeskrob podstrony z pełnymi informacjami o książce
Strona listy daje podstawy, ale prawdziwe „złoto” — ISBN, pełny opis, tagi gatunków, liczba stron, data publikacji — znajduje się na indywidualnej stronie każdej książki. Tu właśnie błyszczy JSON-LD.
1import json
2import time
3def scrape_book_detail(book_url, headers):
4 """Wejdź na stronę pojedynczej książki i pobierz szczegółowe metadane przez 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 # Tagi gatunków nie są w JSON-LD; używamy zapasowo HTML
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], # skrót podglądowy
25 "genres": ", ".join(genres[:5]),
26 }
27# Przykład: wzbogacenie pierwszych 3 książek
28for book in books[:3]:
29 details = scrape_book_detail(book["book_url"], headers)
30 book.update(details)
31 print(f"Zeskrobano: {book['title']} — ISBN: {book.get('isbn', 'N/A')}")
32 time.sleep(4) # respektuj limity
Dodaj time.sleep() od 3 do 8 sekund między żądaniami. Goodreads zaczyna ograniczać ruch mniej więcej przy 20–30 żądaniach na minutę z jednego IP i przy szybszym tempie zaczniesz widzieć 403 albo CAPTCHA.
To dwuetapowe podejście — najpierw zbierasz wszystkie URL-e książek ze stron list, a dopiero potem wchodzisz na strony szczegółów — jest bardziej niezawodne i łatwiejsze do wznowienia, jeśli coś przerwie proces. To strategia, z której korzysta większość skutecznych scraperów Goodreads.
Uwaga: potrafi zrobić to automatycznie dzięki scrapowaniu podstron. AI odwiedza każdą stronę książki i wzbogaca tabelę o ISBN, opis, gatunki i wiele więcej — bez kodu, bez pętli, bez timerów.
Krok 6: Obsłuż treści renderowane przez JavaScript za pomocą Selenium
W przypadku stron, na których potrzebne dane ładowane są przez JavaScript — recenzji, rozkładu ocen, sekcji „więcej szczegółów” — potrzebujesz narzędzia do automatyzacji przeglądarki. Oto przykład w 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# Poczekaj, aż recenzje się załadują
14try:
15 WebDriverWait(driver, 10).until(
16 EC.presence_of_element_located((By.CSS_SELECTOR, "article.ReviewCard"))
17 )
18except:
19 print("Recenzje się nie załadowały — strona może wymagać logowania albo JS przekroczył limit czasu")
20# Teraz parsujemy w pełni wyrenderowaną stronę
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"Ocena: {stars.get_text(strip=True) if stars else 'N/A'}")
28 print(f"Recenzja: {text.get_text(strip=True)[:150] if text else 'N/A'}...")
29 print()
30driver.quit()
Kiedy używać Selenium, a kiedy requests:
- Użyj
requests+ BeautifulSoup dla metadanych książek (JSON-LD), stron list, półek (strona 1) i danych z Choice Awards - Użyj Selenium albo Playwright dla recenzji, rozkładu ocen i wszelkiej treści, której nie ma w surowym HTML
Playwright to zwykle lepszy wybór do nowych projektów — szybszy, mniej pamięciożerny, lepsze domyślne mechanizmy ukrywania automatyzacji. Ale Selenium ma większą społeczność i więcej gotowych przykładów dotyczących Goodreads.
Paginacja, która naprawdę działa: jak zeskrobać całe listy Goodreads
Paginacja to najczęstszy punkt awarii scraperów Goodreads i nie widziałem jeszcze jednego konkurencyjnego poradnika, który tłumaczyłby ją poprawnie. Oto jak zrobić to dobrze.
Jak działają adresy paginacji w Goodreads
Goodreads używa prostego parametru ?page=N dla większości stronicowanych stron:
- Listy:
https://www.goodreads.com/list/show/1.Best_Books_Ever?page=2 - Półki:
https://www.goodreads.com/shelf/show/thriller?page=2 - Wyszukiwanie:
https://www.goodreads.com/search?q=fantasy&page=2
Każda strona listy pokazuje zwykle 100 książek. Półki pokazują 50 na stronę.
Jak napisać pętlę paginacji, która wie, kiedy się zatrzymać
1import time
2all_books = []
3base_url = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
4for page_num in range(1, 50): # bezpieczny limit do 50 stron
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"Strona {page_num}: status {resp.status_code}, kończę.")
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"Strona {page_num}: brak książek, to koniec.")
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"Strona {page_num}: zeskrobano {len(rows)} książek (łącznie: {len(all_books)})")
23 time.sleep(5) # 5 sekund przerwy między stronami
24print(f"\nGotowe. Łącznie pobrano {len(all_books)} książek")
Ostatnią stronę wykrywasz, sprawdzając, czy lista wyników jest pusta (brak elementów tr[itemtype="http://schema.org/Book"]) albo czy nie ma linku „next” (a.next_page).
Trudny przypadek: wymagane logowanie po stronie 5
To pułapka, na którą wpada prawie każdy: niektóre strony półek i list Goodreads po cichu zwracają treść strony 1, jeśli poprosisz o stronę 6+ bez autoryzacji. Bez błędu, bez przekierowania — tylko te same dane w kółko.
Żeby to naprawić, wyeksportuj ciasteczko _session_id2 z przeglądarki (użyj rozszerzenia do eksportu cookies albo Chrome DevTools > Application > Cookies) i dodaj je do żądań:
1session = requests.Session()
2session.headers.update(headers)
3session.cookies.set("_session_id2", "TWOJA_WARTOŚĆ_CIACHETKA_SESJI", domain=".goodreads.com")
4# Teraz używaj session.get() zamiast requests.get()
5resp = session.get(f"{base_url}?page=6", timeout=15)
Thunderbit obsługuje zarówno paginację klikaną, jak i infinite scroll natywnie, bez kodu i bez zarządzania ciasteczkami. Jeśli logika paginacji ciągle Ci się psuje, warto to rozważyć.
Kompletny skrypt w Pythonie gotowy do skopiowania
Poniżej znajdziesz pełny, scalony skrypt. Obsługuje nagłówki, paginację, scrapowanie podstron przez JSON-LD, limitowanie tempa i eksport do CSV. Testowałem go na działających stronach Goodreads w połowie 2025 roku.
1"""
2goodreads_scraper.py — zeskrob listę Goodreads z paginacją i wzbogacaniem danych z podstron.
3Użycie: python goodreads_scraper.py
4Wyjście: 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 # dostosuj według potrzeb
16DELAY_LISTING = 5 # sekundy między stronami listy
17DELAY_DETAIL = 4 # sekundy między stronami szczegółów
18OUTPUT_FILE = "goodreads_books.csv"
19def scrape_listing_page(url):
20 """Zwraca listę słowników z tytułem, autorem i book_url z jednej strony listy."""
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 """Odwiedza stronę książki i pobiera metadane przez JSON-LD + zapasowy HTML."""
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 # --- Etap 1: zbierz URL-e książek ze stron listy ---
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"Strona {page}: pusta — kończę paginację.")
70 break
71 all_books.extend(page_books)
72 print(f"Strona {page}: {len(page_books)} książek (łącznie: {len(all_books)})")
73 time.sleep(DELAY_LISTING)
74 # --- Etap 2: wzbogac każdy rekord danymi ze strony szczegółów ---
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 # --- Eksport do 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"\nZapisano {len(all_books)} książek do {OUTPUT_FILE}")
88 else:
89 print("Nie udało się zeskrobać żadnych książek.")
90if __name__ == "__main__":
91 main()
Przy MAX_PAGES = 3 ten skrypt zbiera około 300 książek z listy „Best Books Ever”, odwiedza stronę szczegółów każdej z nich i zapisuje wszystko do CSV. Na moim komputerze zajmuje to około 25 minut (głównie przez 4-sekundowe przerwy między żądaniami stron szczegółów). Wynikowy plik CSV będzie miał kolumny takie jak title, author, book_url, isbn, pages, avg_rating, rating_count, review_count, description, genres, language, format i published.
Eksport poza CSV: Google Sheets z gspread
Jeśli chcesz mieć dane w Google Sheets zamiast CSV albo dodatkowo obok niego, dodaj to po eksporcie 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("Dane przesłane do Google Sheets.")
Potrzebujesz konta usługi Google Cloud z włączonymi API Sheets i Drive. Dokumentacja prowadzi przez konfigurację w około 5 minut. Przy większej liczbie niż kilkaset wierszy używaj operacji zbiorczych (append_rows() z listą list) — limit Google to 300 żądań na 60 sekund na projekt.
Oczywiście, jeśli cała ta konfiguracja brzmi jak przesada, Thunderbit eksportuje do Google Sheets, Airtable, Notion, Excela, CSV i JSON — bez instalowania bibliotek, pliku z credentialami i limitów API.
Alternatywa bez kodu: scrapowanie Goodreads w Thunderbit
Nie każdy chce utrzymywać skrypt w Pythonie. Może jesteś wydawcą robiącym jednorazową analizę rynku albo blogerem książkowym, który po prostu chce arkusz z bestsellerami tego roku. Właśnie na takie sytuacje powstał Thunderbit.
Jak zeskrobać Goodreads w Thunderbit
- Zainstaluj rozszerzenie Thunderbit Chrome z i przejdź na stronę listy, półki lub wyników wyszukiwania Goodreads.
- Kliknij „AI Suggest Fields” w bocznym panelu Thunderbit. AI analizuje stronę i proponuje kolumny — zwykle tytuł, autor, URL okładki i link do książki.
- Kliknij „Scrape” — dane trafiają do uporządkowanej tabeli w kilka sekund.
- Wyeksportuj do Google Sheets, Excela, Airtable, Notion, CSV lub JSON.
Jeśli potrzebujesz szczegółowych danych książki (ISBN, opis, gatunki, liczba stron), funkcja subpage scraping w Thunderbit odwiedzi każdą stronę szczegółów i automatycznie wzbogaci tabelę — bez pętli, bez opóźnień, bez debugowania.
Thunderbit obsługuje też listy stronicowane natywnie. Wskazujesz „Next” albo przewijanie, a narzędzie zbiera dane ze wszystkich stron bez kodu.
Kompromis jest prosty: skrypt w Pythonie daje pełną kontrolę i jest darmowy (poza Twoim czasem), a Thunderbit wymienia trochę elastyczności na ogromną oszczędność czasu i brak konieczności utrzymania. Przy liście 300 książek skrypt w Pythonie to około 25 minut działania plus czas napisania i naprawiania błędów. Thunderbit pobiera te same dane w około 3 minuty, w dwóch kliknięciach.
Odpowiedzialne scrapowanie Goodreads: robots.txt, Warunki korzystania i etyka
Tu potrzebna jest szczera odpowiedź, a nie rzucenie lakonicznego zastrzeżenia.
Co naprawdę mówi robots.txt Goodreads
Aktualny robots.txt Goodreads jest zaskakująco szczegółowy. Strony szczegółów książek (/book/show/), publiczne listy (/list/show/), publiczne półki (/shelf/show/) i strony autorów (/author/show/) nie są blokowane. Zablokowane są: /api, /book/reviews/, /review/list, /review/show, /search i kilka innych ścieżek. GPTBot i CCBot (Common Crawl) są całkowicie blokowane przez Disallow: /. Dla bingbot istnieje dyrektywa Crawl-delay: 5, ale nie ma globalnego opóźnienia.
Warunki korzystania z Goodreads prostymi słowami
Regulamin (ostatnio zaktualizowany 28 kwietnia 2021) zabrania „jakiegokolwiek używania narzęd