Der er få ting, der føles mere frustrerende end at have skrevet 30 linjer Python, køre din Goodreads-scraper og se den returnere []. En tom liste. Ingenting. Bare dig og en blinkende markør.
Jeg har set det ske igen og igen — i vores egne interne tests hos , i udviklerfora og i de GitHub-issues, der hober sig op i forladte scraper-repositorier. Klagerne er næsten altid de samme: "topanmeldelser-sektionen er tom, den viser bare []", "uanset hvilket sidetal jeg prøver, scraper den altid første side", "min kode virkede sidste år, nu er den i stykker." Og som om det ikke var nok, blev Goodreads API’et udfaset i december 2020, så rådet om bare at bruge API’et, som man ser i ældre guides, er en blindgyde.
Hvis du i dag vil have strukturerede bogdata fra Goodreads — titler, forfattere, ratings, anmeldelser, genrer, ISBN-numre — er scraping den primære vej. Denne guide viser dig en fungerende, komplet metode til at scrape Goodreads med Python, inklusive JS-renderet indhold, pagination, anti-blokering og eksport. Og hvis Python ikke lige er din ting, viser jeg også et no-code-alternativ, der klarer opgaven på et par klik.
Hvad er Goodreads-scraping, og hvorfor gøre det med Python?
Goodreads-scraping betyder, at man automatisk udtrækker bogdata — titler, forfattere, ratings, antal anmeldelser, genrer, ISBN-numre, sidetal, udgivelsesdatoer og mere — fra Goodreads-sider ved hjælp af kode i stedet for manuel copy-paste.
Goodreads er en af verdens største bogdatabaser med over og omkring . Over 18 millioner bøger ryger hver måned på hylden "Want to Read". Netop den slags løbende opdaterede, strukturerede data er grunden til, at forlag, data scientists, boghandlere og forskere bliver ved med at vende tilbage.
Python er det oplagte sprog til den slags arbejde — det driver omkring af alle scraping-projekter. Bibliotekerne er modne (requests, BeautifulSoup, Selenium, Playwright, pandas), syntaksen er begyndervenlig, og fællesskabet er enormt.
Hvis du aldrig har scraped et website før, er Python et rigtig godt sted at starte.
Hvorfor scrape Goodreads med Python? Konkrete use cases
Før vi går til koden, er det værd at spørge: Hvem har egentlig brug for de her data, og hvad bruger de dem til?
| Use case | Hvem får værdi | Hvad du scraper |
|---|---|---|
| Markedsanalyse for forlag | Forlag, litterære agenter | Populære genrer, topbedømte titler, nye forfattere, konkurrenters ratings |
| Anbefalingssystemer til bøger | Data scientists, hobbyister, app-udviklere | Ratings, genrer, brugerhylder, stemning i anmeldelser |
| Pris- og lagerovervågning | Webshops og boghandlere | Populære titler, antal anmeldelser, antal "Want to Read" |
| Akademisk forskning | Forskere, studerende | Anmeldertekst, fordeling af ratings, genrekategorisering |
| Læseanalyse | Bogbloggere, personlige projekter | Egen hyldedata, læsehistorik, årssummeringer |
Et par konkrete eksempler: UCSD Book Graph — et af de mest citerede akademiske datasæt inden for anbefalingsforskning — indeholder , alt sammen indsamlet fra offentligt tilgængelige Goodreads-hylder. Flere Kaggle-datasæt (goodbooks-10k, Best Books Ever osv.) stammer fra Goodreads-scraping. Og et studie fra 2025 i Big Data and Society kortlagde for at analysere, hvordan sponsorerede anmeldelser påvirker platformen.
Kommercielt sælger Bright Data færdigscrapede Goodreads-datasæt for helt ned til $0,50 pr. 1.000 poster — et klart tegn på, at dataene har reel markedsværdi.
Goodreads API’et er væk — her er det, der har overtaget
Hvis du har søgt efter "Goodreads API" for nylig, er du sikkert landet på en guide, der ikke længere er opdateret. Den 8. december 2020 stoppede Goodreads stille og roligt med at udstede nye udvikler-API-nøgler. Der kom hverken blogindlæg eller mailudsendelse — bare et lille banner på dokumentationssiden og en masse forvirrede udviklere.

Konsekvenserne kom hurtigt. En udvikler, Kyle K, byggede en Discord-bot til at dele boganbefalinger — "pludselig PUF, så stoppede den bare med at virke." En anden, Matthew Jones, mistede API-adgang en uge før Reddit-afstemningen til r/Fantasy Stabby Awards, hvilket tvang ham tilbage til Google Forms. En kandidatstuderende ved navn Elena Neacsu fik sit specialeprojekt bremset midt i udviklingen.
Hvad er der så tilbage? Landskabet ser sådan her ud i dag:
| Tilgang | Tilgængelige data | Brugervenlighed | Rate limits | Status |
|---|---|---|---|---|
| Goodreads API | Fuld metadata, anmeldelser | Let (var) | 1 req/sek. | Udfaset (dec. 2020) — ingen nye nøgler |
| Open Library API | Titler, forfattere, ISBN, covers (~30 mio. titler) | Let | 1-3 req/sek. | Aktiv, gratis, ingen auth |
| Google Books API | Metadata, previews | Let | 1.000/dag gratis | Aktiv (mangler nogle ikke-engelske ISBN’er) |
| Python-scraping (requests + BS4) | Alt i den initiale HTML | Moderat | Selvstyret | Virker til statisk indhold |
| Python-scraping (Selenium/Playwright) | Også JS-renderet indhold | Sværere | Selvstyret | Nødvendigt til anmeldelser og visse lister |
| Thunderbit (no-code Chrome-udvidelse) | Alle synlige sidedata | Meget let (2 klik) | Credit-baseret | Aktiv — ingen Python nødvendig |
Open Library er et stærkt supplement, især til ISBN-opslag og grundlæggende metadata. Men hvis du har brug for ratings, anmeldelser, genretags eller antal "Want to Read", scraper du Goodreads direkte — enten med Python eller med et værktøj som Thunderbit, der kan scrape Goodreads-sider (inklusive undersider for bogdetaljer) med AI-forslåede felter og direkte eksport til Google Sheets, Notion eller Airtable.
Hvorfor giver din Goodreads Python-scraper tomme resultater? Og hvordan fikser du det
Det her er den sektion, jeg ville ønske fandtes, da jeg selv begyndte at arbejde med Goodreads-data. Problemet med "tomme resultater" er den klart mest almindelige klage i udviklerfora, og det har flere forskellige årsager — hver med sin egen løsning.
| Symptom | Årsag | Løsning |
|---|---|---|
Anmeldelser/ratings returnerer [] | JS-renderet indhold (React/lazy-load) | Brug Selenium eller Playwright i stedet for requests |
| Scraper altid kun side 1 | Pagination-parametre ignoreres eller styres af JS | Send ?page=N korrekt; brug browserautomatisering til infinite scroll |
| Koden virkede sidste år, men fejler nu | Goodreads ændrede HTML-klassenavne | Brug robuste selektorer (JSON-LD, data-testid-attributter) |
| 403/blokeret efter få requests | Manglende headers / for hurtige requests | Tilføj User-Agent, brug time.sleep(), roter proxies |
| Login-mur på hylde-/listsider | Cookie/session påkrævet | Brug requests.Session() med cookies eller browser-scraping |
JS-renderet indhold: anmeldelser og ratings ser tomme ud
Goodreads kører en frontend baseret på React. Når du kalder requests.get() på en bogside, får du den indledende HTML — men anmeldelser, rating-fordelinger og mange "mere info"-sektioner bliver hentet asynkront via JavaScript. Din scraper kan bogstaveligt talt ikke se dem.
Løsningen: Hvis du har brug for JS-renderet indhold, skal du skifte til Selenium eller Playwright. Jeg anbefaler Playwright til nye projekter — det er takket være den WebSocket-baserede protokol, og det har bedre indbygget stealth og async-understøttelse.

Pagination, der kun returnerer side 1
Den her er lumsk. Du laver en loop, øger ?page=N, og får stadig de samme resultater hver gang. På Goodreads returnerer hyldesider stille og roligt indholdet fra side 1, uanset ?page=-parameteren, hvis du ikke er autentificeret. Ingen fejl, ingen redirect — bare den samme første side igen og igen.
Løsningen: Inkludér en autentificeret session-cookie (specifikt _session_id2). Mere om det i pagination-afsnittet nedenfor.
Kode, der virkede sidste år, virker ikke længere
Goodreads ændrer jævnligt HTML-klassenavne og sidestruktur. Det populære GitHub-repo maria-antoniak/goodreads-scraper har nu en permanent advarsel: "This project is unmaintained and no longer functioning." Løsningen er at bruge mere robuste selektorer — JSON-LD-strukturerede data (som følger schema.org-standarder og sjældent ændrer sig) eller data-testid-attributter i stedet for skrøbelige klassenavne.
403-fejl eller blokering
Python-biblioteket requests har et andet TLS-fingerprint end Chrome. Selv med en Chrome User-Agent-streng kan bot-detektion som AWS WAF (som Goodreads bruger, da de er et Amazon-datterselskab) spotte forskellen. Løsningen: tilføj realistiske browser-headers, indfør time.sleep()-pauser på 3-8 sekunder mellem requests, og overvej curl_cffi til at matche TLS-fingerprint, hvis du scraper i større skala.
Login-mur på hylder og listsider
Nogle Goodreads-hylde- og listsider kræver autentificering for at vise fuldt indhold, især efter side 5. Brug requests.Session() med cookies eksporteret fra din browser, eller brug Selenium/Playwright med en logget ind-profil. Thunderbit håndterer dette naturligt, fordi det kører i din egen loggede Chrome-browser.
Før du går i gang
- Sværhedsgrad: Mellem (grundlæggende Python-kendskab antages)
- Tidsforbrug: Ca. 20-30 minutter for hele gennemgangen
- Det skal du bruge:
- Python 3.8+
- Chrome-browser (til DevTools-inspektion og Selenium/Playwright)
- Biblioteker:
requests,beautifulsoup4,seleniumellerplaywright,pandas - (Valgfrit)
gspreadtil eksport til Google Sheets - (Valgfrit) som no-code-alternativ

Trin 1: Sæt dit Python-miljø op
Installer de nødvendige biblioteker. Åbn din terminal og kør:
1pip install requests beautifulsoup4 selenium pandas lxml
Hvis du foretrækker Playwright (anbefalet til nye projekter):
1pip install playwright
2playwright install chromium
Til Google Sheets-eksport (valgfrit):
1pip install gspread oauth2client
Sørg for, at du bruger Python 3.8 eller nyere. Du kan tjekke det med python --version.
Efter installationen bør du kunne importere alle biblioteker uden fejl. Prøv python -c "import requests, bs4, pandas; print('Ready')" for at bekræfte det.
Trin 2: Send din første request med de rigtige headers
Åbn en Goodreads-genrehylde eller listside i din browser — for eksempel https://www.goodreads.com/list/show/1.Best_Books_Ever. Nu henter vi siden med 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}")
Du bør se Status: 200. Hvis du får 403, så dobbelttjek dine headers — Goodreads’ AWS WAF kigger efter en realistisk User-Agent og afviser meget simple requests. Headers ovenfor efterligner en rigtig Chrome-session.
Trin 3: Inspicér siden og find de rigtige selektorer
Åbn Chrome DevTools (F12) på Goodreads-listsiden. Højreklik på en bogtitel og vælg "Inspect." Så kan du se DOM-strukturen for hver bogpost.
På listsider er hver bog typisk pakket ind i et <tr>-element med itemtype="http://schema.org/Book". Inde i det finder du:
- Titel:
a.bookTitle(linkteksten er titlen,hrefgiver dig bogens URL) - Forfatter:
a.authorName - Rating:
span.minirating(indeholder gennemsnitsrating og antal ratings) - Forsidebillede:
imginde i bogrækken
På individuelle bogdetaljesider bør du springe CSS-selektorer over og gå direkte til JSON-LD. Goodreads indlejrer strukturerede data i et <script type="application/ld+json">-tag, der følger schema.org Book-formatet. Det er langt mere stabilt end klassenavne, som Goodreads ændrer efter forgodtbefindende.
Trin 4: Udtræk bogdata fra en enkelt listsideside
Lad os parse listsiden og udtrække grundlæggende oplysninger for hver bog:
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"Fandt {len(books)} bøger på side 1")
28for b in books[:3]:
29 print(b)
Du bør se omkring 100 bøger pr. listsideside. Hver post får en titel, forfatter, en rating-streng som "4.28 avg rating — 9,031,257 ratings," og en URL til bogens detaljeside.
Trin 5: Scrape undersider for detaljerede bogoplysninger
Listsiden giver dig det grundlæggende, men det egentlige guld — ISBN, fuld beskrivelse, genretags, sidetal, udgivelsesdato — ligger på hver bogs individuelle side. Det er her JSON-LD virkelig skinner.
1import json
2import time
3def scrape_book_detail(book_url, headers):
4 """Besøg en bogside og udtræk detaljerede metadata via 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 # Genretags findes ikke i JSON-LD; brug HTML som 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], # afkort til preview
25 "genres": ", ".join(genres[:5]),
26 }
27# Eksempel: berig de første 3 bøger
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) # respekter rate limits
Tilføj en time.sleep()-pause på 3-8 sekunder mellem requests. Goodreads’ rate limiting slår typisk ind omkring 20-30 requests pr. minut fra én IP, og du vil begynde at se 403’er eller CAPTCHA’er, hvis du kører hurtigere.
Denne totrinsmetode — først at hente alle bog-URL’er fra listsider, derefter besøge hver detaljeside — er mere stabil og lettere at genoptage, hvis processen bliver afbrudt. Det er den strategi, de mest succesfulde Goodreads-scrapere bruger.
Sidebemærkning: kan gøre dette automatisk med subpage scraping. AI’en besøger hver bogs detaljeside og beriger din tabel med ISBN, beskrivelse, genrer og mere — ingen kode, ingen loops, ingen sleep-timere.
Trin 6: Håndter JavaScript-renderet indhold med Selenium
Til sider, hvor indholdet hentes via JavaScript — anmeldelser, rating-fordelinger, "mere detaljer"-sektioner — skal du bruge et browser-automatiseringsværktøj. Her er et Selenium-eksempel:
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# Vent på, at anmeldelserne loader
14try:
15 WebDriverWait(driver, 10).until(
16 EC.presence_of_element_located((By.CSS_SELECTOR, "article.ReviewCard"))
17 )
18except:
19 print("Anmeldelser blev ikke indlæst — siden kræver måske login, eller JS timed out")
20# Parse nu den fuldt renderede side
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"Anmeldelse: {text.get_text(strip=True)[:150] if text else 'N/A'}...")
29 print()
30driver.quit()
Hvornår skal du bruge Selenium vs. requests:
- Brug
requests+ BeautifulSoup til bogmetadata (JSON-LD), listsider, hyldesider (side 1) og Choice Awards-data - Brug Selenium eller Playwright til anmeldelser, rating-fordelinger og alt indhold, der ikke findes i den rå HTML
Playwright er generelt det bedste valg til nye projekter — hurtigere, mindre hukommelsesforbrug og bedre stealth-standarder. Men Selenium har et større fællesskab og flere eksisterende kodeeksempler specifikt til Goodreads.
Pagination, der faktisk virker: Scraping af komplette Goodreads-lister
Pagination er det mest almindelige sted, Goodreads-scrapere fejler, og jeg har endnu ikke set en konkurrent-guide forklare det ordentligt. Sådan gør du det rigtigt.
Sådan fungerer Goodreads’ pagination-URL’er
Goodreads bruger typisk en enkel ?page=N-parameter på de fleste paginerede sider:
- Lister:
https://www.goodreads.com/list/show/1.Best_Books_Ever?page=2 - Hylder:
https://www.goodreads.com/shelf/show/thriller?page=2 - Søgning:
https://www.goodreads.com/search?q=fantasy&page=2
Hver listside viser typisk 100 bøger. Hylder viser 50 pr. side.
Skriv en pagination-loop, der ved, hvornår den skal stoppe
1import time
2all_books = []
3base_url = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
4for page_num in range(1, 50): # sikkerhedsloft på 50 sider
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"Side {page_num}: fik status {resp.status_code}, stopper.")
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"Side {page_num}: ingen bøger fundet, nåede slutningen.")
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"Side {page_num}: scraped {len(rows)} bøger (i alt: {len(all_books)})")
23 time.sleep(5) # 5 sekunders pause mellem sider
24print(f"\nFærdig. Total antal indsamlede bøger: {len(all_books)}")
Du kan opdage sidste side ved at tjekke, om resultatlisten er tom (ingen tr[itemtype="http://schema.org/Book"]-elementer) eller ved at se, om der mangler et "næste"-link (a.next_page).
Specialtilfælde: login påkrævet efter side 5
Det her er fælden, der fanger næsten alle: nogle Goodreads-hylde- og listsider returnerer stille og roligt indholdet fra side 1, når du beder om side 6+ uden autentificering. Ingen fejl, ingen redirect — bare de samme data igen.
For at løse det skal du eksportere _session_id2-cookien fra din browser (brug en cookie-eksport-udvidelse eller Chrome DevTools > Application > Cookies) og inkludere den i dine requests:
1session = requests.Session()
2session.headers.update(headers)
3session.cookies.set("_session_id2", "DIN_SESSION_COOKIE_VÆRDI_HER", domain=".goodreads.com")
4# Brug nu session.get() i stedet for requests.get()
5resp = session.get(f"{base_url}?page=6", timeout=15)
Thunderbit håndterer både klikbaseret og infinite-scroll-pagination naturligt, uden kode og uden cookie-håndtering. Hvis din pagination-logik bliver ved med at bryde, er det værd at overveje.
Den komplette Python-scriptversion, klar til copy-paste
Her er det samlede script. Det håndterer headers, pagination, scraping af undersider via JSON-LD, rate limiting og CSV-eksport. Jeg har testet det mod live Goodreads-sider pr. midten af 2025.
1"""
2goodreads_scraper.py — Scrape en Goodreads-liste med pagination og berigelse af bogdetaljer.
3Brug: 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 # justér efter behov
16DELAY_LISTING = 5 # sekunder mellem listsider
17DELAY_DETAIL = 4 # sekunder mellem detaljesider
18OUTPUT_FILE = "goodreads_books.csv"
19def scrape_listing_page(url):
20 """Returnér en liste af dicts med titel, forfatter, book_url fra én listsideside."""
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 """Besøg en bogside og udtræk metadata via 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: indsamling af bog-URL’er fra listsider ---
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"Side {page}: tom — stopper pagination.")
70 break
71 all_books.extend(page_books)
72 print(f"Side {page}: {len(page_books)} bøger (i alt: {len(all_books)})")
73 time.sleep(DELAY_LISTING)
74 # --- Pass 2: berig hver bog med data fra detaljesiden ---
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ér til 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"\nGemte {len(all_books)} bøger til {OUTPUT_FILE}")
88 else:
89 print("Ingen bøger blev scraped.")
90if __name__ == "__main__":
91 main()
Med MAX_PAGES = 3 indsamler scriptet omkring 300 bøger fra listen "Best Books Ever", besøger hver bogs detaljeside og skriver alt til en CSV. På min maskine tager det omtrent 25 minutter (mest på grund af de 4 sekunders pauser mellem detaljesiderne). Din output-CSV vil have kolonner som titel, forfatter, book_url, isbn, pages, avg_rating, rating_count, review_count, description, genres, language, format og published.
Eksport ud over CSV: Google Sheets med gspread
Hvis du vil have dataene i Google Sheets i stedet for — eller oveni — en CSV, så tilføj dette efter CSV-eksporten:
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 sendt til Google Sheets.")
Du skal bruge en Google Cloud-servicekonto med Sheets- og Drive-API’erne aktiveret. gennemgår opsætningen på cirka 5 minutter. Brug batch-operationer (append_rows() med en liste af lister), hvis du sender mere end nogle hundrede rækker — Googles rate limit er 300 requests pr. 60 sekunder pr. projekt.
Hvis alt det her setup føles som overkill, eksporterer Thunderbit til Google Sheets, Airtable, Notion, Excel, CSV og JSON med — ingen bibliotekopsætning, ingen credentials-fil, ingen API-kvoter.
No-code-alternativet: Scrape Goodreads med Thunderbit
Ikke alle har lyst til at vedligeholde et Python-script. Måske er du et forlag, der skal lave en engangs-markedsanalyse, eller en bogblogger, der bare vil have et regneark med årets bedst sælgende titler. Det er præcis den situation, vi byggede Thunderbit til.
Sådan scraper du Goodreads med Thunderbit
- Installer Thunderbit Chrome-udvidelsen fra og gå til en Goodreads-liste, hylde eller søgeresultatside.
- Klik på "AI Suggest Fields" i Thunderbit-sidepanelet. AI’en læser siden og foreslår kolonner — typisk titel, forfatter, rating, cover-URL og boglink.
- Klik på "Scrape" — dataene trækkes ud i en struktureret tabel på få sekunder.
- Eksportér til Google Sheets, Excel, Airtable, Notion, CSV eller JSON.
Hvis du vil have detaljerede bogdata (ISBN, beskrivelse, genrer, sidetal), besøger Thunderbits subpage scraping-funktion automatisk hver bogs detaljeside og beriger tabellen — uden loops, uden sleep-timere, uden debugging.
Thunderbit håndterer også paginerede lister naturligt. Du siger bare, at den skal klikke på "Next" eller scrolle, og så samler den data fra alle sider uden kode.
Afvejningen er enkel: Python-scriptet giver dig fuld kontrol og er gratis (minus din tid), mens Thunderbit bytter en smule fleksibilitet for massiv tidsbesparelse og ingen vedligeholdelse. For en liste med 300 bøger tager Python-scriptet cirka 25 minutters runtime plus den tid, du brugte på at skrive og debugge det. Thunderbit får de samme data på cirka 3 minutter med to klik.
Scraping af Goodreads ansvarligt: robots.txt, vilkår og etik
Det her fortjener et ærligt svar — ikke bare en standard disclaimer-paragraf.
Hvad siger Goodreads’ robots.txt egentlig?
Goodreads’ live robots.txt er overraskende specifik. Bogdetaljesider (/book/show/), offentlige lister (/list/show/), offentlige hylder (/shelf/show/) og forfattersider (/author/show/) er ikke blokeret. Det, der er blokeret, er: /api, /book/reviews/, /review/list, /review/show, /search og flere andre stier. GPTBot og CCBot (Common Crawl) er fuldstændigt blokeret med Disallow: /. Der er en Crawl-delay: 5-direktiv for bingbot, men ingen global forsinkelse.
Goodreads’ vilkår på almindeligt dansk
Vilkårene (senest revideret 28. april 2021) forbyder "any use of data mining, robots, or similar data gathering and extraction tools." Det er bredt formuleret og værd at tage seriøst — men domstole har konsekvent fastslået, at brud på vilkår alene ikke udgør kriminel "unauthorized access". I blev det slået fast, at "criminalizing terms-of-service violations risks turning each website into its own criminal jurisdiction."
Bedste praksis
- Vent mellem requests: 3-8 sekunder mellem requests (Goodreads’ egen robots.txt foreslår 5 sekunder for bots)
- Hold dig under 5.000 requests pr. dag fra én IP
- Scrape kun offentligt tilgængelige sider — undgå mass scraping af data, der kun er tilgængelige efter login
- Genudgiv ikke rå anmeldelsestekst kommercielt — anmeldelser er ophavsretligt beskyttede, kreative værker
- Gem kun det, du har brug for, og lav en plan for datalagring og sletning
- Personlig forskning vs. kommerciel brug: Scraping af offentlige data til personlig analyse eller akademisk forskning er generelt accepteret. Kommerciel videredistribution er dér, den juridiske risiko stiger.
At bruge et værktøj som Thunderbit (der scraper via din egen browsersession) gør oplevelsen visuelt identisk med normal browsing, men de samme etiske principper gælder uanset værktøj. Hvis du vil gå dybere ned i , har vi behandlet emnet separat.
Tips og typiske faldgruber
Tip: Start altid med JSON-LD. Før du skriver komplekse CSS-selektorer, så tjek, om de data du skal bruge, ligger i <script type="application/ld+json">-tagget. Det er mere stabilt, lettere at parse og mindre tilbøjeligt til at gå i stykker, når Goodreads opdaterer frontend’en.
Tip: Brug totrinsstrategien. Hent først alle bog-URL’er fra listsiderne, og besøg derefter hver detaljeside. Det gør din scraper nemmere at genoptage, hvis den crasher midtvejs, og du kan gemme URL-listen på disk som checkpoint.
Faldgrube: At glemme manglende felter. Ikke alle bogsider har ISBN, genretags eller beskrivelse. Brug altid .get() med en standardværdi, eller omslut selektorer i if-checks. Én NoneType-fejl kan vælte et 3-timers scraping-run.
Faldgrube: At køre for hurtigt. Jeg ved godt, det er fristende at sætte time.sleep(0.5) og bare buldre derudad. Men Goodreads begynder at returnere 403’er efter cirka 20-30 hurtige requests, og når du først er flagget, kan du være nødt til at vente i timevis eller skifte IP. En pause på 4-5 sekunder er ofte det bedste kompromis.
Faldgrube: At stole på gamle guides. Hvis en guide refererer til Goodreads API’et eller bruger klassenavne som .field.value eller #bookTitle, er den sandsynligvis forældet. Tjek altid selektorerne mod den live side, før du bygger din scraper.
Hvis du vil vide mere om at vælge de rigtige scraping-værktøjer og frameworks, kan du se vores guides til og .
Konklusion og vigtigste pointer
Det er helt muligt at scrape Goodreads med Python — du skal bare vide, hvor fælderne ligger. Den korte version:
- Goodreads API’et er væk (siden december 2020). Scraping er den primære måde at få strukturerede bogdata fra platformen på.
- Tomme resultater skyldes næsten altid JS-renderet indhold, forældede selektorer, manglende headers eller autentificeringsproblemer ved pagination — ikke at din kode er forkert.
- JSON-LD er din bedste ven til bogmetadata. Det er stabilt, struktureret og ændrer sig sjældent.
- Pagination kræver autentificering på mange hylde- og listsider efter side 5. Husk
_session_id2-cookien. - Rate limiting er reel. Brug 3-8 sekunders pauser og hold dig under 5.000 requests om dagen.
- Totrinsstrategien (samle URL’er først, scrape detaljesider bagefter) er mere robust og lettere at genoptage.
- For ikke-kodere (eller alle, der værdsætter deres eftermiddag) håndterer det hele — JS-rendering, pagination, berigelse af undersider og eksport — på omkring to klik.
Scrape ansvarligt, respekter robots.txt, og må dine bogdata altid komme tilbage med mere end [].
FAQs
Kan man stadig bruge Goodreads API’et?
Nej. Goodreads udfasede sit offentlige API i december 2020 og udsteder ikke længere nye udviklernøgler. Eksisterende nøgler, der var inaktive i 30 dage, blev automatisk deaktiveret. Web scraping eller alternative API’er (som Open Library eller Google Books) er de nuværende muligheder, hvis du vil tilgå bogdata programmatisk.
Hvorfor returnerer min Goodreads-scraper tomme resultater?
Den mest almindelige årsag er JavaScript-renderet indhold. Goodreads indlæser anmeldelser, rating-fordelinger og mange detaljesektioner via React/JavaScript, som en simpel requests.get()-kald ikke kan se. Skift til Selenium eller Playwright til de sider. Andre årsager er forældede CSS-selektorer (Goodreads har ændret HTML’en), manglende User-Agent-headers (som udløser 403-blokeringer) eller uautentificerede requests på paginerede hyldesider.
Er det lovligt at scrape Goodreads?
Scraping af offentligt tilgængelige data til personlig brug eller forskningsbrug er generelt accepteret ud fra nuværende retspraksis (hiQ v. LinkedIn, Meta v. Bright Data). Goodreads’ vilkår forbyder dog automatiseret dataindsamling, og du bør altid gennemgå deres robots.txt. Undgå kommerciel videredistribution af ophavsretligt beskyttet anmeldelsestekst, og begræns din request-mængde for ikke at belaste sitet unødigt.
Hvordan scraper jeg flere sider på Goodreads?
Tilføj ?page=N til shelf- eller liste-URL’en, og loop gennem sidetallene. Tjek for tomme resultater eller fraværet af et "next"-link for at finde sidste side. Vigtigt: nogle hyldesider kræver autentificering (_session_id2-cookien) for at returnere resultater efter side 5 — uden den får du stille og roligt side 1-data gentaget.
Kan jeg scrape Goodreads uden at skrive kode?
Ja. er en Chrome-udvidelse, der lader dig scrape Goodreads på to klik — AI’en foreslår felterne, du klikker på "Scrape", og eksporterer direkte til Google Sheets, Excel, Airtable eller Notion. Den håndterer JS-renderet indhold, pagination og berigelse af undersider automatisk, uden Python eller kodning.
Læs mere