Python으로 Goodreads 스크래핑하는 방법 (빈 결과는 이제 그만)

최종 업데이트: April 16, 2026

Python 코드 30줄을 공들여 짜고 Goodreads 스크래퍼를 돌렸는데 결과가 []로만 돌아오는 것만큼 허탈한 일도 없습니다. 빈 리스트. 아무것도 없음. 그저 깜빡이는 커서만 덩그러니 남아 있죠.

이런 장면은 정말 자주 봤습니다. 내부 실험에서도 그랬고, 개발자 포럼에서도 그랬고, 방치된 스크래퍼 저장소에 쌓여 가는 GitHub 이슈에서도 똑같았습니다. 불만은 늘 비슷합니다. "상단 리뷰 섹션이 비어 있어요. 그냥 []만 나와요", "페이지 번호를 바꿔도 계속 첫 페이지만 수집돼요", "작년엔 잘 됐는데 지금은 고장 났어요." 게다가 Goodreads API는 2020년 12월에 폐기됐기 때문에, 예전 튜토리얼에서 흔히 보는 "그냥 API를 쓰세요"라는 말은 이제 완전히 막다른 길입니다.

지금 Goodreads에서 제목, 저자, 평점, 리뷰, 장르, ISBN 같은 구조화된 책 데이터를 얻으려면 스크래핑이 사실상 유일한 방법입니다. 이 가이드에서는 Python으로 Goodreads를 제대로 수집하는 실전 방법을 단계별로 설명합니다. JS로 렌더링되는 콘텐츠, 페이지네이션, 차단 회피, 내보내기까지 모두 다룹니다. 그리고 Python이 부담스럽다면, 클릭 두 번 정도로 끝나는 노코드 대안도 같이 보여드릴게요.

Goodreads 스크래핑이란? 그리고 왜 Python으로 해야 할까?

Goodreads 스크래핑이란, 손으로 복사해 붙여넣는 대신 코드를 사용해 Goodreads 웹페이지에서 책 데이터—제목, 저자, 평점, 리뷰 수, 장르, ISBN, 페이지 수, 출간일 등—를 자동으로 뽑아내는 것을 말합니다.

Goodreads는 세계 최대 규모의 도서 데이터베이스 중 하나로, 과 약 를 보유하고 있습니다. 매달 1,800만 권이 "읽고 싶어요" 목록에 추가됩니다. 이렇게 계속 갱신되는 구조화 데이터가 있기 때문에 출판사, 데이터 과학자, 서점 운영자, 연구자들이 계속 이 플랫폼을 찾는 겁니다.

Python은 이런 작업에 가장 많이 쓰이는 언어입니다. 전체 스크래핑 프로젝트의 약 를 Python이 맡는다고 보면 됩니다. requests, BeautifulSoup, Selenium, Playwright, pandas 같은 라이브러리도 성숙해 있고, 문법도 초보자에게 비교적 친숙하며, 커뮤니티도 아주 큽니다.

웹사이트 스크래핑이 처음이라면, 시작은 Python이 가장 좋습니다.

왜 Goodreads를 Python으로 스크래핑할까? 실제 활용 사례

코드로 들어가기 전에 먼저 한 번 생각해볼 질문이 있습니다. 이 데이터가 누가 필요하고, 실제로 어디에 쓰일까요?

활용 사례주요 사용자수집 데이터
출판 시장 조사출판사, 문학 에이전트인기 장르, 고평점 도서, 떠오르는 작가, 경쟁작 평점
도서 추천 시스템데이터 과학자, 취미 개발자, 앱 제작자평점, 장르, 사용자 서가, 리뷰 감성
가격 및 재고 모니터링이커머스 서점 운영자인기 도서, 리뷰 수, "읽고 싶어요" 수
학술 연구연구자, 학생리뷰 텍스트, 평점 분포, 장르 분류
독서 분석북 블로거, 개인 프로젝트개인 서가 데이터, 독서 기록, 연말 통계

구체적인 사례도 꽤 많습니다. 추천 연구에서 가장 많이 인용되는 학술 데이터셋 중 하나인 UCSD Book Graph에는 이 들어 있습니다. 이 데이터는 모두 공개된 Goodreads 서가에서 수집된 것입니다. Kaggle의 여러 데이터셋(goodbooks-10k, Best Books Ever 등)도 Goodreads 스크래핑에서 출발했습니다. 또 2025년 Big Data and Society 연구에서는 후원 리뷰가 플랫폼에 어떤 영향을 주는지 분석하기 위해 를 정리했습니다.

상업적인 측면에서도 수요가 있습니다. Bright Data는 Goodreads 데이터를 미리 수집한 형태로 1,000건당 $0.50부터 판매하고 있습니다. 이 데이터에 실제 시장 가치가 있다는 뜻이죠.

Goodreads API는 사라졌습니다 — 지금은 무엇을 써야 할까요?

최근에 "Goodreads API"를 검색해 봤다면, 아마 오래된 튜토리얼을 만나셨을 겁니다. 2020년 12월 8일, Goodreads는 새 개발자 API 키 발급을 조용히 중단했습니다. 블로그 공지도 없었고, 대대적인 이메일도 없었고, 문서 페이지의 작은 배너 하나와 당황한 개발자들만 남았습니다.

goodreads-data-access-tools.webp

여파는 바로 나타났습니다. 책 추천 공유용 Discord 봇을 만들던 Kyle K는 *"갑자기 뿅 하고 그냥 작동을 멈췄다"*고 했고, Matthew Jones는 Reddit r/Fantasy Stabby Awards 투표 일주일 전에 API 접근 권한을 잃어 Google Forms로 되돌아갈 수밖에 없었습니다. Elena Neacsu라는 대학원생은 석사 논문 프로젝트가 개발 도중 중단되는 일을 겪었습니다.

그렇다면 남은 방법은 무엇일까요? 지금 가능한 선택지는 대략 이렇습니다.

방법사용 가능한 데이터사용 난이도속도 제한상태
Goodreads API전체 메타데이터, 리뷰쉬웠음초당 1 요청폐기됨(2020년 12월) — 새 키 발급 없음
Open Library API제목, 저자, ISBN, 표지(~3천만 권)쉬움초당 1~3 요청활성화, 무료, 인증 불필요
Google Books API메타데이터, 미리보기쉬움하루 1,000건 무료활성화됨(비영어 ISBN은 일부 누락)
Python 스크래핑(requests + BS4)초기 HTML에 있는 모든 것중간직접 관리정적 콘텐츠에 적합
Python 스크래핑(Selenium/Playwright)JS 렌더링 콘텐츠까지더 어려움직접 관리리뷰, 일부 목록 수집에 필요
Thunderbit (노코드 Chrome 확장)화면에 보이는 모든 페이지 데이터매우 쉬움(클릭 2번)크레딧 기반활성화 — Python 불필요

Open Library는 특히 ISBN 조회나 기본 메타데이터용으로 꽤 훌륭한 보완재입니다. 하지만 평점, 리뷰, 장르 태그, "읽고 싶어요" 수가 필요하다면 결국 Goodreads를 직접 스크래핑해야 합니다. Python으로 하든, 아니면 Thunderbit 같은 도구로 하든 마찬가지입니다. Thunderbit는 AI가 제안한 필드로 Goodreads 페이지와 책 상세 하위 페이지까지 수집할 수 있고, Google Sheets, Notion, Airtable로 바로 내보낼 수 있습니다.

왜 Goodreads Python 스크래퍼는 빈 결과를 돌려줄까? 그리고 어떻게 고칠까?

제가 Goodreads 데이터를 처음 다루기 시작했을 때 이런 설명이 있었으면 얼마나 좋았을지 모릅니다. "빈 결과" 문제는 개발자 포럼에서 가장 자주 나오는 불만이고, 원인도 여러 가지입니다. 각각에 맞는 해결책도 따로 있습니다.

This paragraph contains content that cannot be parsed and has been skipped.

JS로 렌더링되는 콘텐츠: 리뷰와 평점이 비어 보이는 이유

Goodreads는 React 기반 프런트엔드를 사용합니다. 책 페이지에 requests.get()을 보내면 초기 HTML만 받게 되고, 리뷰나 평점 분포, "추가 정보" 섹션 같은 내용은 JavaScript로 비동기 로딩됩니다. 즉, 스크래퍼가 아예 그 내용을 볼 수 없습니다.

해결책은 단순합니다. JS로 렌더링되는 콘텐츠가 필요한 페이지에서는 Selenium이나 Playwright로 바꾸세요. 새 프로젝트라면 Playwright를 추천합니다. WebSocket 기반 프로토콜 덕분에 , 기본 스텔스와 비동기 지원도 더 좋습니다.

troubleshooting-empty-array-causes.webp

페이지 1만 계속 가져오는 페이지네이션

이 문제는 꽤 교묘합니다. 루프를 만들고 ?page=N을 늘려 가는데도 매번 같은 결과만 나오는 경우죠. Goodreads에서는 인증되지 않은 상태라면 ?page= 파라미터를 바꿔도 서가 페이지가 조용히 1페이지만 돌려주는 일이 있습니다. 에러도 없고 리다이렉트도 없이, 그냥 첫 페이지가 반복됩니다.

해결책: 인증된 세션 쿠키, 특히 _session_id2를 포함하세요. 자세한 내용은 아래 페이지네이션 섹션에서 설명합니다.

작년엔 돌아가던 코드가 지금은 깨지는 이유

Goodreads는 HTML 클래스명과 페이지 구조를 주기적으로 바꿉니다. GitHub의 유명한 maria-antoniak/goodreads-scraper 저장소에는 이제 영구 공지가 붙어 있습니다. "이 프로젝트는 유지보수되지 않으며 더 이상 작동하지 않습니다." 해결책은 더 튼튼한 선택자를 쓰는 것입니다. 쉽게 깨지는 클래스명 대신 JSON-LD 구조화 데이터(대개 schema.org 표준을 따르며 거의 바뀌지 않음)나 data-testid 속성을 사용하세요.

403 오류나 차단

Python의 requests 라이브러리는 Chrome과 TLS 지문이 다릅니다. Chrome User-Agent를 넣어도 Goodreads가 사용하는 Amazon 계열 AWS WAF 같은 봇 탐지 시스템은 이 불일치를 잡아낼 수 있습니다. 해결책은 현실적인 브라우저 헤더를 추가하고, 요청 사이에 3~8초 time.sleep()을 넣고, 대량 수집이라면 TLS 지문을 맞추기 위해 curl_cffi를 고려하는 것입니다.

서가와 목록 페이지의 로그인 벽

일부 Goodreads 서가/목록 페이지는 전체 콘텐츠를 보려면 인증이 필요하며, 특히 5페이지를 넘어서면 이런 현상이 자주 나타납니다. 브라우저에서 내보낸 쿠키를 requests.Session()에 넣거나, 로그인된 프로필로 Selenium/Playwright를 사용하세요. Thunderbit는 사용자의 로그인된 Chrome 브라우저 안에서 동작하므로 이런 부분을 자연스럽게 처리합니다.

시작하기 전에

  • 난이도: 중급 (기본 Python 지식 필요)
  • 소요 시간: 전체 과정 약 20~30분
  • 준비물:
    • Python 3.8 이상
    • Chrome 브라우저(DevTools 확인 및 Selenium/Playwright용)
    • 라이브러리: requests, beautifulsoup4, selenium 또는 playwright, pandas
    • (선택) Google Sheets 내보내기용 gspread
    • (선택) 노코드 대안용

goodreads-scraping-flow.webp

1단계: Python 환경 설정하기

필요한 라이브러리를 설치합니다. 터미널을 열고 아래를 실행하세요.

1pip install requests beautifulsoup4 selenium pandas lxml

Playwright를 선호한다면(새 프로젝트에 추천):

1pip install playwright
2playwright install chromium

Google Sheets로 내보내려면(선택):

1pip install gspread oauth2client

Python 3.8 이상인지 확인하세요. python --version으로 확인할 수 있습니다.

설치가 끝나면 모든 라이브러리를 오류 없이 import할 수 있어야 합니다. python -c "import requests, bs4, pandas; print('Ready')"로 확인해 보세요.

2단계: 올바른 헤더로 첫 요청 보내기

브라우저에서 Goodreads의 장르 서가나 목록 페이지로 이동하세요. 예를 들면 https://www.goodreads.com/list/show/1.Best_Books_Ever입니다. 이제 Python으로 그 페이지를 가져와 봅시다.

1import requests
2from bs4 import BeautifulSoup
3> This paragraph contains content that cannot be parsed and has been skipped.
4url = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
5response = requests.get(url, headers=headers, timeout=15)
6print(f"Status: {response.status_code}")

Status: 200가 보여야 합니다. 만약 403이 나오면 헤더를 다시 확인하세요. Goodreads의 AWS WAF는 현실적인 User-Agent를 검사하며, 너무 단순한 요청은 거부합니다. 위 헤더는 실제 Chrome 브라우저 세션을 흉내 냅니다.

3단계: 페이지 구조를 확인하고 올바른 선택자 찾기

Goodreads 목록 페이지에서 Chrome DevTools(F12)를 열어 보세요. 책 제목을 우클릭하고 "검사"를 누르면 각 책 항목의 DOM 구조를 볼 수 있습니다.

목록 페이지에서 각 책은 보통 itemtype="http://schema.org/Book"가 붙은 <tr> 요소로 감싸져 있습니다. 그 안에는 다음 항목이 있습니다.

  • 제목: a.bookTitle (링크 텍스트가 제목이고, href에는 책 URL이 들어 있음)
  • 저자: a.authorName
  • 평점: span.minirating (평균 평점과 평점 수가 들어 있음)
  • 표지 이미지: 책 행 내부의 img

개별 책 상세 페이지에서는 CSS 선택자를 억지로 찾기보다 곧바로 JSON-LD를 보세요. Goodreads는 <script type="application/ld+json"> 태그 안에 schema.org Book 형식의 구조화 데이터를 넣어 둡니다. Goodreads가 수시로 바꾸는 클래스명보다 훨씬 안정적입니다.

4단계: 단일 목록 페이지에서 책 데이터 추출하기

이제 목록 페이지를 파싱해서 각 책의 기본 정보를 뽑아 봅시다.

1import requests
2from bs4 import BeautifulSoup
3> This paragraph contains content that cannot be parsed and has been skipped.
4url = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
5response = requests.get(url, headers=headers, timeout=15)
6soup = BeautifulSoup(response.text, "lxml")
7books = []
8rows = soup.select('tr[itemtype="http://schema.org/Book"]')
9for row in rows:
10    title_tag = row.select_one("a.bookTitle")
11    author_tag = row.select_one("a.authorName")
12    rating_tag = row.select_one("span.minirating")
13    title = title_tag.get_text(strip=True) if title_tag else ""
14    book_url = "https://www.goodreads.com" + title_tag["href"] if title_tag else ""
15    author = author_tag.get_text(strip=True) if author_tag else ""
16    rating_text = rating_tag.get_text(strip=True) if rating_tag else ""
17> This paragraph contains content that cannot be parsed and has been skipped.
18print(f"페이지 1에서 {len(books)}권을 찾았습니다")
19for b in books[:3]:
20    print(b)

목록 페이지당 대략 100권이 보일 겁니다. 각 항목에는 제목, 저자, "4.28 avg rating — 9,031,257 ratings" 같은 평점 문자열, 그리고 책 상세 페이지 URL이 들어 있습니다.

5단계: 하위 페이지에서 상세 책 정보 스크래핑하기

목록 페이지는 기본 정보만 보여 줍니다. 하지만 진짜 유용한 데이터—ISBN, 전체 설명, 장르 태그, 페이지 수, 출간일—는 각 책의 상세 페이지에 있습니다. 여기서 JSON-LD가 빛을 발합니다.

1import json
2import time
3def scrape_book_detail(book_url, headers):
4    """단일 책 페이지에 접속해 JSON-LD로 상세 메타데이터를 추출합니다."""
5    resp = requests.get(book_url, headers=headers, timeout=15)
6    if resp.status_code != 200:
7        return {}
8> This paragraph contains content that cannot be parsed and has been skipped.
9    data = json.loads(script.string)
10    agg = data.get("aggregateRating", {})
11    # 장르 태그는 JSON-LD에 없으므로 HTML에서 보완 추출
12    genres = [g.get_text(strip=True) for g in soup.select('span.BookPageMetadataSection__genreButton a span')]
13> This paragraph contains content that cannot be parsed and has been skipped.
14# 예시: 처음 3권 보강하기
15for book in books[:3]:
16    details = scrape_book_detail(book["book_url"], headers)
17    book.update(details)
18    print(f"수집 완료: {book['title']} — ISBN: {book.get('isbn', 'N/A')}")
19    time.sleep(4)  # 속도 제한을 고려

요청 사이에는 time.sleep()으로 3~8초 정도 쉬어 주세요. Goodreads는 한 IP에서 분당 20~30회 정도부터 속도 제한을 걸기 시작하며, 너무 빨리 보내면 403이나 CAPTCHA가 나올 수 있습니다.

이렇게 먼저 목록 페이지에서 모든 책 URL을 모은 뒤, 그다음 상세 페이지를 방문하는 2단계 방식은 더 안정적이고 중간에 멈춰도 재개하기 쉽습니다. 성공적인 Goodreads 스크래퍼가 대체로 쓰는 전략이기도 합니다.

참고: 는 하위 페이지 스크래핑을 자동으로 처리할 수 있습니다. AI가 각 책 상세 페이지를 방문해 ISBN, 설명, 장르 등을 표에 자동으로 채워 줍니다. 코드도, 루프도, sleep 타이머도 필요 없습니다.

6단계: Selenium으로 JavaScript 렌더링 콘텐츠 처리하기

리뷰, 평점 분포, "더 많은 정보" 섹션처럼 JavaScript로 로드되는 콘텐츠가 필요하면 브라우저 자동화 도구가 있어야 합니다. 아래는 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# 리뷰가 로드될 때까지 대기
14try:
15    WebDriverWait(driver, 10).until(
16        EC.presence_of_element_located((By.CSS_SELECTOR, "article.ReviewCard"))
17    )
18except:
19    print("리뷰가 로드되지 않았습니다 — 로그인 필요 또는 JS 타임아웃일 수 있습니다")
20# 이제 완전히 렌더링된 페이지 파싱
21page_source = driver.page_source
22soup = BeautifulSoup(page_source, "lxml")
23> This paragraph contains content that cannot be parsed and has been skipped.
24driver.quit()

requests와 Selenium 중 언제 무엇을 써야 할까?

  • 책 메타데이터(JSON-LD), 목록 페이지, 서가 페이지(1페이지), Choice Awards 데이터는 requests + BeautifulSoup 사용
  • 리뷰, 평점 분포, 원시 HTML에 없는 콘텐츠는 Selenium 또는 Playwright 사용

새 프로젝트라면 보통 Playwright가 더 낫습니다. 더 빠르고, 메모리 사용량도 낮고, 기본 스텔스도 좋습니다. 다만 Goodreads 관련 예제는 Selenium 쪽이 커뮤니티가 더 크고 기존 코드도 많습니다.

실제로 작동하는 페이지네이션: Goodreads 전체 목록 스크래핑하기

페이지네이션은 Goodreads 스크래퍼가 가장 많이 실패하는 지점이며, 제대로 설명한 튜토리얼도 거의 못 봤습니다. 여기서는 올바르게 처리하는 방법을 알려드리겠습니다.

Goodreads 페이지네이션 URL 구조

Goodreads는 대부분의 페이지네이션 페이지에서 단순한 ?page=N 파라미터를 사용합니다.

  • 목록: https://www.goodreads.com/list/show/1.Best_Books_Ever?page=2
  • 서가: https://www.goodreads.com/shelf/show/thriller?page=2
  • 검색: https://www.goodreads.com/search?q=fantasy&page=2

목록 페이지는 보통 100권, 서가 페이지는 50권씩 보여 줍니다.

언제 멈춰야 하는지 아는 페이지네이션 루프 작성하기

1import time
2all_books = []
3base_url = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
4for page_num in range(1, 50):  # 안전을 위해 최대 50페이지로 제한
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_num}페이지: 상태 코드 {resp.status_code}를 받았습니다. 중단합니다.")
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_num}페이지: 책을 찾지 못했습니다. 마지막 페이지에 도달했습니다.")
14        break
15> This paragraph contains content that cannot be parsed and has been skipped.
16    print(f"{page_num}페이지: {len(rows)}권 수집 (누적: {len(all_books)}권)")
17    time.sleep(5)  # 페이지 간 5초 대기
18print(f"\n완료. 총 {len(all_books)}권 수집")

마지막 페이지는 결과 목록이 비어 있는지(tr[itemtype="http://schema.org/Book"] 요소가 없는지), 또는 "next" 링크(a.next_page)가 없는지로 판별할 수 있습니다.

예외 케이스: 5페이지 이후 로그인 필요

거의 다들 여기서 걸립니다. 일부 Goodreads 서가/목록 페이지는 인증 없이 6페이지 이상을 요청하면 조용히 1페이지 내용을 다시 돌려줍니다. 에러도 없고 리다이렉트도 없이 같은 데이터가 반복됩니다.

이 문제를 해결하려면 브라우저에서 _session_id2 쿠키를 내보내서(쿠키 export 확장 프로그램이나 Chrome DevTools > Application > Cookies 사용) 요청에 포함하세요.

1session = requests.Session()
2session.headers.update(headers)
3session.cookies.set("_session_id2", "YOUR_SESSION_COOKIE_VALUE_HERE", domain=".goodreads.com")
4# 이제 requests.get() 대신 session.get() 사용
5resp = session.get(f"{base_url}?page=6", timeout=15)

Thunderbit는 클릭 기반 페이지네이션과 무한 스크롤을 모두 기본적으로 처리합니다. 코드도, 쿠키 관리도 필요 없습니다. 페이지네이션 로직이 자꾸 깨진다면 충분히 고려해볼 만합니다.

복붙해서 바로 쓸 수 있는 완성형 Python 스크립트

아래는 헤더, 페이지네이션, JSON-LD 하위 페이지 수집, 속도 제한, CSV 내보내기를 모두 포함한 통합 스크립트입니다. 2025년 중반 기준 실제 Goodreads 페이지에서 테스트했습니다.

1"""
2goodreads_scraper.py — Goodreads 목록을 페이지네이션과 상세 페이지 보강까지 포함해 스크래핑합니다.
3사용법: python goodreads_scraper.py
4출력: goodreads_books.csv
5"""
6> This paragraph contains content that cannot be parsed and has been skipped.
7> This paragraph contains content that cannot be parsed and has been skipped.
8> This paragraph contains content that cannot be parsed and has been skipped.
9> This paragraph contains content that cannot be parsed and has been skipped.
10def main():
11    all_books = []
12    # --- 1차: 목록 페이지에서 책 URL 수집 ---
13    for page in range(1, MAX_PAGES + 1):
14        url = f"{BASE_URL}?page={page}"
15        page_books = scrape_listing_page(url)
16        if not page_books:
17            print(f"{page}페이지: 비어 있음 — 페이지네이션 중단.")
18            break
19        all_books.extend(page_books)
20        print(f"{page}페이지: {len(page_books)}권 (누적: {len(all_books)}권)")
21        time.sleep(DELAY_LISTING)
22    # --- 2차: 각 책 상세 정보 보강 ---
23    for i, book in enumerate(all_books):
24        details = scrape_book_detail(book["book_url"])
25        book.update(details)
26        print(f"[{i+1}/{len(all_books)}] {book['title']} — ISBN: {book.get('isbn', 'N/A')}")
27        time.sleep(DELAY_DETAIL)
28    # --- CSV 내보내기 ---
29    if all_books:
30        fieldnames = list(all_books[0].keys())
31        with open(OUTPUT_FILE, "w", newline="", encoding="utf-8") as f:
32            writer = csv.DictWriter(f, fieldnames=fieldnames)
33            writer.writeheader()
34            writer.writerows(all_books)
35        print(f"\n총 {len(all_books)}권을 {OUTPUT_FILE}에 저장했습니다")
36    else:
37        print("수집된 책이 없습니다.")
38if __name__ == "__main__":
39    main()

MAX_PAGES = 3 상태에서 이 스크립트는 "Best Books Ever" 목록에서 약 300권을 수집하고, 각 책의 상세 페이지를 방문한 뒤 CSV로 저장합니다. 제 환경에서는 약 25분 정도 걸렸습니다(대부분 상세 페이지 요청 사이의 4초 지연 때문입니다). 출력 CSV에는 title, author, book_url, isbn, pages, avg_rating, rating_count, review_count, description, genres, language, format, published 같은 열이 들어갑니다.

CSV를 넘어 Google Sheets로: gspread 사용하기

CSV 대신 또는 CSV와 함께 Google Sheets로 보내고 싶다면, 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("데이터가 Google Sheets로 전송되었습니다.")

Sheets와 Drive API가 활성화된 Google Cloud 서비스 계정이 필요합니다. 에 따라 설정하면 약 5분이면 끝납니다. 수백 행 이상을 보낼 때는 append_rows()처럼 배치 작업을 쓰세요. Google의 요청 한도는 프로젝트당 60초에 300건입니다.

물론 이 모든 설정이 번거롭다면, Thunderbit는 Google Sheets, Airtable, Notion, Excel, CSV, JSON으로 내보낼 수 있습니다. 라이브러리 설치도, 자격 증명 파일도, API 쿼터도 필요 없습니다.

노코드 대안: Thunderbit로 Goodreads 스크래핑하기

모두가 Python 스크립트를 직접 관리하고 싶어 하는 건 아닙니다. 한 번만 하는 시장 조사일 수도 있고, 올해 베스트셀러 목록만 필요할 수도 있죠. 바로 이런 상황을 위해 Thunderbit를 만들었습니다.

Thunderbit로 Goodreads 스크래핑하는 방법

  1. 에서 Thunderbit Chrome 확장 프로그램을 설치하고 Goodreads 목록, 서가, 검색 결과 페이지로 이동하세요.
  2. Thunderbit 사이드바에서 **"AI 필드 제안"**을 클릭합니다. AI가 페이지를 읽고 보통 제목, 저자, 평점, 표지 이미지 URL, 책 링크 같은 열을 추천합니다.
  3. **"스크래핑"**을 클릭하면 몇 초 만에 구조화된 테이블로 데이터가 추출됩니다.
  4. Google Sheets, Excel, Airtable, Notion, CSV, JSON으로 내보냅니다.

ISBN, 설명, 장르, 페이지 수 같은 상세 책 데이터가 필요하다면 Thunderbit의 하위 페이지 스크래핑 기능이 각 책 상세 페이지를 방문해 표를 자동으로 보강합니다. 루프도, sleep 타이머도, 디버깅도 없습니다.

Thunderbit는 여러 페이지로 나뉜 목록도 자연스럽게 처리합니다. "다음"을 클릭하거나 스크롤하도록 지시하면, 코드 없이 모든 페이지의 데이터를 모아 줍니다.

트레이드오프는 단순합니다. Python 스크립트는 완전한 제어와 무료 사용(시간 제외)을 제공하지만, Thunderbit는 조금의 유연성을 덜어내는 대신 엄청난 시간 절약과 유지보수 제로를 제공합니다. 300권짜리 목록이라면 Python 스크립트는 실행 시간만 약 25분이고, 작성과 디버깅 시간은 별도입니다. Thunderbit는 클릭 두 번 정도로 같은 데이터를 약 3분 만에 얻을 수 있습니다.

책임감 있게 Goodreads 스크래핑하기: robots.txt, 이용약관, 윤리

이 부분은 대충 넘길 면책 문구가 아니라, 분명하게 짚고 가야 합니다.

Goodreads robots.txt가 실제로 말하는 것

Goodreads의 최신 robots.txt는 의외로 구체적입니다. 책 상세 페이지(/book/show/), 공개 목록(/list/show/), 공개 서가(/shelf/show/), 저자 페이지(/author/show/)는 차단되어 있지 않습니다. 반대로 /api, /book/reviews/, /review/list, /review/show, /search 등은 차단됩니다. GPTBot과 CCBot(Common Crawl)은 Disallow: /로 전면 차단되어 있습니다. bingbot에 대해서는 Crawl-delay: 5가 있지만 전역 지연 설정은 없습니다.

Goodreads 이용약관을 쉽게 말하면

이용약관(2021년 4월 28일 최종 수정)은 "데이터 마이닝, 로봇, 또는 유사한 데이터 수집 및 추출 도구의 사용"을 금지합니다. 표현이 매우 넓기 때문에 진지하게 볼 필요가 있습니다. 다만 법원은 지금까지 이용약관 위반만으로는 형사상 "무단 접근"이 되지 않는다고 일관되게 봐 왔습니다. 판결은 "이용약관 위반을 형사처벌로 보는 것은 각 웹사이트를 자체 형사 관할구역으로 만들어 버릴 위험이 있다"고 밝혔습니다.

권장 사항

  • 요청 간 지연 적용: 3~8초 간격(robots.txt에는 봇 기준 5초가 제시됨)
  • 하루 5,000 요청 이하 유지: 단일 IP 기준
  • 공개 페이지만 수집: 로그인 전용 데이터를 대량 수집하지 않기
  • 리뷰 원문을 상업적으로 재배포하지 않기: 리뷰는 저작권이 있는 창작물입니다
  • 필요한 데이터만 저장하고 보관 정책 수립: 무분별한 저장은 피하기
  • 개인 연구 vs 상업적 이용: 공개 데이터를 개인 분석이나 학술 연구용으로 수집하는 것은 대체로 받아들여집니다. 상업적 재배포가 법적 리스크를 키우는 지점입니다.

Thunderbit처럼 사용자의 브라우저 세션을 통해 수집하는 도구를 쓰더라도, 화면상으로는 일반 브라우징과 크게 다르지 않게 보입니다. 다만 어떤 도구를 쓰든 윤리 원칙은 같습니다. 가 더 궁금하다면 별도로 정리해 두었습니다.

팁과 흔한 함정

팁: 항상 JSON-LD부터 확인하세요. 복잡한 CSS 선택자를 만들기 전에, 필요한 데이터가 <script type="application/ld+json"> 안에 있는지 먼저 확인하세요. 더 안정적이고, 파싱도 쉽고, Goodreads가 프런트엔드를 바꿔도 덜 깨집니다.

팁: 2단계 전략을 사용하세요. 먼저 목록 페이지에서 모든 책 URL을 수집하고, 그다음 각 상세 페이지를 방문하세요. 중간에 스크래퍼가 죽어도 재개하기 쉬워지고, URL 목록을 디스크에 체크포인트로 저장할 수도 있습니다.

함정: 누락된 필드를 처리하지 않기. 모든 책 페이지에 ISBN, 장르 태그, 설명이 있는 것은 아닙니다. 항상 기본값과 함께 .get()을 사용하거나 선택자를 if로 감싸세요. NoneType 오류 하나로 3시간짜리 수집이 날아갈 수 있습니다.

함정: 너무 빨리 실행하기. time.sleep(0.5)로 빨리 끝내고 싶은 마음은 이해합니다. 하지만 Goodreads는 빠른 요청 20~30번 정도부터 403을 내기 시작하고, 한 번 걸리면 몇 시간 기다리거나 IP를 바꿔야 할 수도 있습니다. 4~5초 지연이 가장 무난합니다.

함정: 오래된 튜토리얼을 믿기. Goodreads API를 언급하거나 .field.value, #bookTitle 같은 클래스명을 쓰는 가이드는 오래됐을 가능성이 큽니다. 스크래퍼를 만들기 전에 항상 실제 페이지에서 선택자를 다시 확인하세요.

적절한 스크래핑 도구와 프레임워크를 고르는 방법이 궁금하다면, 가이드도 참고해 보세요.

결론 및 핵심 정리

Python으로 Goodreads를 스크래핑하는 것은 충분히 가능합니다. 다만 어디에 함정이 있는지만 알고 있으면 됩니다. 짧게 정리하면:

  • Goodreads API는 사라졌습니다(2020년 12월 이후). 구조화된 책 데이터를 얻는 주된 방법은 스크래핑입니다.
  • 빈 결과는 대개 코드가 틀려서가 아니라, JS 렌더링 콘텐츠, 오래된 선택자, 헤더 누락, 페이지네이션 인증 문제 때문에 발생합니다.
  • JSON-LD는 책 메타데이터의 가장 좋은 친구입니다. 안정적이고 구조화되어 있으며 거의 바뀌지 않습니다.
  • 페이지네이션은 많은 서가/목록 페이지에서 5페이지 이후 인증이 필요합니다. _session_id2 쿠키를 포함하세요.
  • 속도 제한은 실제로 존재합니다. 3~8초 지연을 쓰고 하루 5,000 요청 이하로 유지하세요.
  • 2단계 전략(먼저 URL 수집, 그다음 상세 페이지 스크래핑)이 더 안정적이고 재개도 쉽습니다.
  • 코딩이 부담스러운 분(또는 오후 시간을 아끼고 싶은 분)이라면, 가 JS 렌더링, 페이지네이션, 하위 페이지 보강, 내보내기까지 거의 두 번 클릭으로 해결합니다.

책 데이터는 책임감 있게 수집하고, robots.txt를 존중하세요. 그리고 여러분의 결과가 더 이상 []가 아니길 바랍니다.

자주 묻는 질문(FAQs)

아직 Goodreads API를 사용할 수 있나요?

아니요. Goodreads는 2020년 12월에 공개 API를 폐기했고, 더 이상 새 개발자 키를 발급하지 않습니다. 30일 동안 비활성 상태였던 기존 키도 자동 비활성화되었습니다. 지금 책 데이터에 프로그래밍 방식으로 접근하려면 웹 스크래핑 또는 Open Library, Google Books 같은 대체 API를 사용해야 합니다.

왜 Goodreads 스크래퍼가 빈 결과를 반환하나요?

가장 흔한 원인은 JavaScript 렌더링 콘텐츠입니다. Goodreads는 리뷰, 평점 분포, 많은 상세 섹션을 React/JavaScript로 불러오는데, 단순한 requests.get()으로는 이 내용을 볼 수 없습니다. 이런 페이지에는 Selenium이나 Playwright를 사용하세요. 그 외에도 오래된 CSS 선택자(Goodreads가 HTML을 바꾼 경우), User-Agent 헤더 누락(403 차단 유발), 인증되지 않은 페이지네이션 서가 페이지 등이 원인일 수 있습니다.

Goodreads를 스크래핑하는 것은 합법인가요?

공개 데이터를 개인용이나 연구용으로 스크래핑하는 것은 현재의 법적 판례(hiQ v. LinkedIn, Meta v. Bright Data)에서 대체로 허용되는 편입니다. 다만 Goodreads 이용약관은 자동 수집을 금지하고 있으며, robots.txt도 반드시 확인해야 합니다. 저작권이 있는 리뷰 텍스트의 상업적 재배포는 피하고, 사이트 자원에 부담을 주지 않도록 요청량을 제한하세요.

Goodreads에서 여러 페이지를 어떻게 스크래핑하나요?

서가나 목록 URL에 ?page=N을 붙여 페이지 번호를 반복하면 됩니다. 마지막 페이지는 결과가 비어 있거나 "next" 링크가 없는지로 감지합니다. 중요한 점은 일부 서가 페이지는 5페이지 이후 결과를 얻으려면 인증이 필요하며, _session_id2 쿠키가 없으면 페이지 1 데이터가 조용히 반복된다는 것입니다.

코딩 없이 Goodreads를 스크래핑할 수 있나요?

네. 는 Goodreads를 클릭 두 번으로 스크래핑할 수 있는 Chrome 확장 프로그램입니다. AI가 데이터 필드를 제안하고, 사용자는 "스크래핑"만 클릭하면 Google Sheets, Excel, Airtable, Notion으로 바로 내보낼 수 있습니다. JS 렌더링 콘텐츠, 페이지네이션, 하위 페이지 보강까지 자동으로 처리하며, Python이나 코딩이 전혀 필요하지 않습니다.

더 알아보기

Shuai Guan
Shuai Guan
Co-founder/CEO @ Thunderbit. Passionate about cross section of AI and Automation. He's a big advocate of automation and loves making it more accessible to everyone. Beyond tech, he channels his creativity through a passion for photography, capturing stories one picture at a time.
목차

Thunderbit 사용해보기

리드와 기타 데이터를 단 2번 클릭으로 수집하세요. AI로 구동됩니다.

Thunderbit 받기 무료예요
AI로 데이터 추출하기
Google Sheets, Airtable, Notion으로 데이터를 쉽게 옮기세요
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week