Goodreads, один из крупнейших публичных каталогов книг в интернете, и каждая публичная страница книги содержит читательские сигналы, которые используются в анализе трендов, рекомендательных системах и маркетинговых исследованиях: название, автор, средний рейтинг, количество оценивших и текст публичных отзывов. Проблема в том, что Goodreads отрисовывает большую часть этого контента на стороне клиента и загружает отзывы асинхронно, поэтому простой HTTP-запрос возвращает пустую оболочку вместо рейтингов и текстов отзывов.

В этом руководстве показан надёжный способ парсинга рейтингов и отзывов Goodreads с Python. Вы создадите небольшой рабочий парсер, который получает отрисованную страницу книги через Crawling API, парсит нужные поля с помощью BeautifulSoup и выводит чистую структурированную запись. Всё руководство ограничено публичными данными о книгах и отзывах, а раздел о легальности в конце не является шаблонным текстом, поэтому прочитайте его, прежде чем направлять это на реальный объём.

Что вы создадите

Python-скрипт, который принимает публичный URL книги Goodreads, получает отрисованный HTML через Crawling API и извлекает структурированную запись книги с видимыми отзывами. В качестве сквозного примера используем известное публичное издание и извлекаем следующие поля:

  • Book title название книги, например "The Great Gatsby".
  • Author автор, указанный на странице книги.
  • Average rating агрегированная оценка, которую Goodreads вычисляет из пользовательских рейтингов.
  • Ratings count количество людей, оценивших книгу.
  • Reviews публичный текст отзывов, показанный на странице, с отображаемым именем рецензента.

Почему обычный запрос не работает на Goodreads

Если запросить URL книги Goodreads с помощью обычного HTTP-клиента, вы получите ответ со статусом 200 и почти без содержимого отзывов в теле. Против вас работают два фактора. Во-первых, Goodreads отрисовывает большую часть раздела рейтингов и отзывов в браузере с помощью JavaScript, поэтому начальный HTML, это оболочка, которая заполняется лишь после выполнения скриптов страницы. Во-вторых, список отзывов загружается асинхронно и растёт за взаимодействием, поэтому даже частичная отрисовка может упустить нужные данные, если не дать странице время на загрузку.

Таким образом, рабочий парсер Goodreads должен объединять в одном запросе две вещи: браузер, который действительно отрисовывает страницу, и IP, воспринимаемый платформой как реальный посетитель. Можно собрать такой стек самостоятельно с помощью headless-браузера и пула ротирующих резидентных прокси, но склеивать их вместе и поддерживать в рабочем состоянии, это большая часть работы. Crawling API объединяет оба компонента в одном вызове: вы отправляете URL с JavaScript-токеном, API отрисовывает страницу за доверенным IP и возвращает готовый HTML для парсинга.

Why the JS token

Crawlbase предлагает два типа токенов. Обычный токен получает статический HTML; JavaScript (JS) токен сначала отрисовывает страницу в реальном браузере. Goodreads загружает рейтинги и отзывы на стороне клиента, поэтому здесь нужен JS-токен. Использование обычного токена возвращает ту же тонкую оболочку, что и простой запрос, и из неё практически нечего парсить.

Требования

Перед написанием кода необходимо подготовить несколько вещей. Это займёт немного времени.

Базовые знания Python. Вы должны уметь писать и запускать Python-скрипты и устанавливать пакеты через pip. Если вы новичок в парсинге HTML, наш вводный материал о том, как использовать BeautifulSoup в Python, охватывает основы селекторов, на которые опирается данное руководство.

Python 3.8 или новее. Проверьте версию командой python --version. Если Python не установлен, установите его с сайта python.org или через дистрибутив, например Anaconda.

Аккаунт Crawlbase и JS-токен. Зарегистрируйтесь, откройте дашборд и скопируйте свой JavaScript (JS) токен со страницы документации аккаунта. Относитесь к токену как к паролю: он аутентифицирует ваши запросы, поэтому не храните его в системе контроля версий.

Настройка проекта

Создайте виртуальное окружение, чтобы зависимости проекта оставались изолированными, затем установите две библиотеки, необходимые парсеру.

bash
python --version

python -m venv goodreads_env
source goodreads_env/bin/activate

pip install crawlbase beautifulsoup4

На Windows активируйте окружение командой goodreads_env\Scripts\activate вместо строки source. Две зависимости выполняют работу: crawlbase, официальный клиент для Crawling API, а beautifulsoup4 парсит возвращённый HTML, позволяя извлекать отдельные поля по CSS-селектору.

Шаг 1: получение отрисованной страницы книги

Начните с получения готовой страницы. Импортируйте класс CrawlingAPI, инициализируйте его с помощью JS-токена и запросите URL книги. Проверка статуса перед парсингом позволяет обнаруживать ошибки явно, а не молча.

python
from crawlbase import CrawlingAPI

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"})

def crawl(page_url):
    options = {"ajax_wait": "true", "page_wait": 5000}
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['status_code']}")
    return None

if __name__ == "__main__":
    page_url = "https://www.goodreads.com/book/show/4671.The_Great_Gatsby"
    html = crawl(page_url)
    print(html[:500] if html else "No HTML returned")

Два параметра ожидания имеют значение для такой цели с клиентской отрисовкой. ajax_wait указывает API дождаться завершения загрузки асинхронного контента, а page_wait выдерживает фиксированное количество миллисекунд после загрузки, чтобы поздно отрисовываемые элементы появились до захвата страницы. Пять секунд, разумный старт; увеличьте значение, если поля отзывов возвращаются пустыми. Запустите скрипт командой python scraper.py, и вы должны увидеть реальную разметку книги, а не пустую оболочку, которую возвращает обычный запрос. Это подтверждает работоспособность отрисовки, прежде чем вы напишете хоть один селектор.

Crawlbase Crawling API

Goodreads требует отрисованной страницы за доверенным IP в одном вызове. Crawling API принимает JS-токен, запускает страницу в реальном браузере, ротирует резидентные IP на стороне сервера и возвращает готовый HTML, поэтому вам не нужно самостоятельно запускать headless-парк и пул прокси. Начните с публичной страницы книги на бесплатном тарифе.

Шаг 2: парсинг полей книги с помощью BeautifulSoup

Имея отрисованный HTML, загрузите его в BeautifulSoup и считайте каждое поле по его селектору. Страница книги Goodreads компонует основные детали в предсказуемой структуре, поэтому можно сопоставить название, автора, средний рейтинг и количество оценок с отдельными селекторами, затем пройтись по карточкам отзывов для сбора публичного текста. Вспомогательные функции, возвращающие None при отсутствующем элементе, не позволяют одному пропущенному полю прервать весь запуск.

python
from bs4 import BeautifulSoup

def text_of(node, selector):
    el = node.select_one(selector)
    return el.get_text(strip=True) if el else None

def scrape_book(html):
    soup = BeautifulSoup(html, "html.parser")

    rating_el = soup.select_one("div.RatingStatistics span.RatingStars")
    average_rating = rating_el["aria-label"] if rating_el else None

    reviews = []
    for card in soup.select("div.ReviewsList article.ReviewCard"):
        reviews.append({
            "user": text_of(card, 'div[data-testid="name"]'),
            "review": text_of(card, "section.ReviewText span.Formatted"),
        })

    return {
        "title": text_of(soup, 'h1.H1Title a[data-testid="title"]'),
        "author": text_of(soup, "span.ContributorLink__name"),
        "average_rating": average_rating,
        "ratings_count": text_of(soup, 'span[data-testid="ratingsCount"]'),
        "reviews": reviews,
    }

Вспомогательная функция text_of делает две полезные вещи одновременно: она запрашивает один элемент в рамках заданного узла и возвращает None при отсутствующем элементе, вместо того чтобы выбрасывать исключение при вызове .get_text() для несуществующего объекта. Явная передача узла важна, поскольку каждая карточка отзыва является собственной областью видимости и вы хотите считывать имя рецензента и текст отзыва из этой карточки, а не из первого совпадения на всей странице. Средний рейтинг считывается из атрибута aria-label, а не из видимого текста, поэтому он обрабатывается отдельно.

Selectors drift

Имена классов Goodreads (маркеры RatingStars и ReviewCard, атрибуты data-testid и обёртки секций) меняются без предупреждения. Относитесь к приведённым выше селекторам как к начальному шаблону, а не к контракту. Когда поле возвращается как None, заново проверьте живую страницу в инструментах разработчика браузера и обновите селектор. Периодическое обслуживание селекторов, это норма для любого производственного парсера, а не признак неисправности.

Шаг 3: загрузка большего числа отзывов

При первой отрисовке показывается только верхняя часть отзывов. Goodreads раскрывает остальные за кнопкой «Show more reviews», а не через нумерованный пейджер, поэтому для получения более глубокого текста отзывов необходимо кликнуть эту кнопку до захвата страницы. Crawling API предоставляет параметр css_click_selector, который кликает по совпадающему элементу во время отрисовки, позволяя получить больший набор отзывов в рамках одного запроса.

python
def crawl_with_more_reviews(page_url):
    options = {
        "ajax_wait": "true",
        "page_wait": 5000,
        "css_click_selector": 'button:has(span[data-testid="loadMore"])',
    }
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['status_code']}")
    return None

Селектор нацелен на кнопку, которая оборачивает элемент управления «загрузить ещё». Один клик по ней расширяет видимый список отзывов до захвата HTML, поэтому тот же парсер scrape_book видит больше карточек без каких-либо изменений. Если вам нужно ещё больше отзывов, чем даёт один клик, увеличьте page_wait, чтобы дать расширенному списку время на отрисовку. Подробнее о том, почему контент, управляемый взаимодействием, ведёт себя именно так, рассказывает наше руководство по обходу JavaScript-сайтов.

Шаг 4: сборка полного скрипта

Теперь соедините получение и парсинг в один запускаемый скрипт. Получите отрисованный HTML с кликом «загрузить ещё», передайте его в парсер и запишите структурированную запись в JSON-файл.

python
import json
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"})

def crawl(page_url):
    options = {
        "ajax_wait": "true",
        "page_wait": 5000,
        "css_click_selector": 'button:has(span[data-testid="loadMore"])',
    }
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['status_code']}")
    return None

def text_of(node, selector):
    el = node.select_one(selector)
    return el.get_text(strip=True) if el else None

def scrape_book(html):
    soup = BeautifulSoup(html, "html.parser")
    rating_el = soup.select_one("div.RatingStatistics span.RatingStars")
    average_rating = rating_el["aria-label"] if rating_el else None

    reviews = []
    for card in soup.select("div.ReviewsList article.ReviewCard"):
        reviews.append({
            "user": text_of(card, 'div[data-testid="name"]'),
            "review": text_of(card, "section.ReviewText span.Formatted"),
        })

    return {
        "title": text_of(soup, 'h1.H1Title a[data-testid="title"]'),
        "author": text_of(soup, "span.ContributorLink__name"),
        "average_rating": average_rating,
        "ratings_count": text_of(soup, 'span[data-testid="ratingsCount"]'),
        "reviews": reviews,
    }

def main():
    page_url = "https://www.goodreads.com/book/show/4671.The_Great_Gatsby"
    html = crawl(page_url)
    if not html:
        return
    data = scrape_book(html)
    with open("goodreads_book.json", "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
    print(json.dumps(data, indent=2, ensure_ascii=False)[:600])

if __name__ == "__main__":
    main()

Как выглядят результаты

Запустите полный скрипт командой python scraper.py и получите чистую структурированную запись книги, готовую для записи в JSON, CSV или базу данных.

json
{
  "title": "The Great Gatsby",
  "author": "F. Scott Fitzgerald",
  "average_rating": "Rating 3.93 out of 5",
  "ratings_count": "5,432,109 ratings",
  "reviews": [
    {
      "user": "Alex",
      "review": "Charms you with some of the most elegant English prose ever published."
    },
    {
      "user": "Inge",
      "review": "There was one thing I really liked about The Great Gatsby. It was short."
    }
  ]
}

Масштабирование на множество книг

Одна книга, это демонстрация; реальная задача охватывает список названий. Структура остаётся той же: ведите список URL книг, получайте каждую через Crawling API, парсите той же функцией и собирайте строки. Поскольку каждая страница книги имеет одинаковую структуру, уже написанный парсер работает со всеми ними без изменений.

python
import time

books = [
    "https://www.goodreads.com/book/show/4671.The_Great_Gatsby",
    "https://www.goodreads.com/book/show/5470.1984",
]

results = []
for url in books:
    html = crawl(url)
    if html:
        results.append(scrape_book(html))
    time.sleep(2)

with open("goodreads_books.json", "w", encoding="utf-8") as f:
    json.dump(results, f, ensure_ascii=False, indent=2)

time.sleep(2) между запросами задаёт темп цикла, чтобы страницы книг не запрашивались подряд. Для сбора URL книг в масштабе можно парсить публичные списки и страницы полок Goodreads с тем же паттерном получение-парсинг, собирая ссылки на книги, а затем посещая каждую из них. Просто сохраняйте разумный объём и соблюдайте ограничения скорости, описанные ниже.

Как оставаться незаблокированным

Даже при обработке отрисовки Goodreads отслеживает трафик, характерный для парсеров. Несколько привычек помогают поддерживать работоспособность запуска и применимы к любой крупной публичной цели.

  • Задавайте темп запросов. Интенсивный обход страниц книг в плотном цикле, самый быстрый способ получить ограничение. Распределяйте запросы и варьируйте цели вместо того, чтобы обходить один путь на полной скорости.
  • Опирайтесь на ротацию. Пул резидентных IP распределяет запросы по множеству реальных пользовательских адресов, чтобы ни один не активировал ограничение скорости. Crawling API берёт это на себя; если вы создаёте собственный стек, именно это место стоит сделать правильно.
  • Читайте коды статусов. Запуск, при котором начинают возвращаться задания или ошибки, сигнализирует о том, что текущая частота или уровень IP уже недостаточны. Относитесь к этому как к сигналу снизить нагрузку, а не как к шуму, который нужно игнорировать.

Общий план описан в статье как парсить сайты без блокировок и в более подробном материале как обходить CAPTCHA при парсинге. Если вы предпочитаете маршрутизировать собственный трафик через ротирующий пул вместо использования управляемого API, Smart AI Proxy (также называемый AI Proxy) предоставляет ту же ротацию резидентных IP в виде прокси-эндпоинта.

Законно ли парсить Goodreads?

Допустимость парсинга Goodreads зависит от условий использования Goodreads и Amazon, вашей юрисдикции и того, что вы делаете с данными. Goodreads принадлежит Amazon, и его условия ограничивают автоматизированный доступ, поэтому парсинг может нарушать эти условия независимо от тщательности вашего подхода. Ни один из приведённых здесь кодов этого не меняет; он лишь обеспечивает техническую работоспособность. Ознакомьтесь с Условиями использования Goodreads и его robots.txt и относитесь к обоим как к границам того, что можно собирать.

Несколько правил, которые стоит соблюдать. Собирайте только публичные данные о книгах и отзывах: название, автора, средний рейтинг, количество оценок и текст отзывов, который любой может видеть без аккаунта. Соблюдайте ожидаемую частоту запросов, заявленную Goodreads, и поддерживайте объём запросов достаточно низким, чтобы не перегружать серверы. Избегайте персональных данных рецензентов, выходящих за рамки публично отображаемых на странице, и не составляйте профили идентифицируемых лиц на их основе. Если вы планируете коммерческое использование данных, получите разрешение или лицензированный источник, а не считайте, что молчание означает согласие.

Одна практическая заметка, специфичная для Goodreads: официальный API Goodreads фактически устарел и закрыт для новых ключей, поэтому нет живого первичного источника данных, к которому можно обратиться, как предлагают некоторые платформы. Это оставляет два реалистичных варианта для публичных данных: парсинг публичных страниц книг, как показано здесь, или получение данных через лицензированного провайдера. Данное руководство намеренно ограничено публичным содержимым страниц книг, потому что именно это делает работу обоснованной. Оно не охватывает ничего за авторизационной стеной, приватных полок или данных аккаунтов пользователей, персональных данных рецензентов, выходящих за рамки публичного отображения, или любых попыток обойти аутентификацию. Если ваш проект требует большего, чем публичные данные о книгах и отзывах, лицензированный источник данных является правильным путём, а не более умный парсер.

Итоги

Ключевые выводы

  • Goodreads отрисовывает на стороне клиента. Обычный запрос возвращает пустую оболочку, поэтому необходимо отрисовать страницу перед парсингом рейтингов и отзывов.
  • Вам нужны отрисовка и доверенный IP вместе. Crawling API с JS-токеном делает оба в одном вызове; ajax_wait и page_wait управляют временем ожидания контента.
  • Отзывы загружаются за кнопкой. Используйте css_click_selector для расширения списка отзывов во время отрисовки, чтобы тот же парсер видел больше карточек.
  • BeautifulSoup выполняет извлечение. Сопоставьте название, автора, средний рейтинг, количество оценок и текст отзыва с текущими селекторами, ожидая их дрейфа.
  • Оставайтесь в рамках публичных данных. Соблюдайте Условия использования Goodreads и Amazon, а также robots.txt; предпочитайте лицензированный источник для массового или коммерческого использования; никогда не работайте с аккаунтами, приватными полками или персональными данными рецензентов.

Часто задаваемые вопросы

Почему обычный запрос не возвращает отзывы с Goodreads?

Потому что Goodreads отрисовывает рейтинги и отзывы на стороне клиента с помощью JavaScript и загружает список отзывов асинхронно. Начальный HTML, это оболочка, которая заполняется лишь после выполнения скриптов страницы в браузере, поэтому обычный HTTP-запрос возвращает статус 200 с пустыми полями отзывов. Для получения реальных данных необходимо сначала отрисовать страницу, что и обеспечивает JS-токен Crawling API.

Нужен ли обычный токен или JS-токен для Goodreads?

JS-токен. Обычный токен получает статический HTML, который на Goodreads является той же пустой оболочкой, что и при обычном запросе. JS-токен отрисовывает страницу в реальном браузере перед возвратом HTML, поэтому рейтинги и текст отзывов присутствуют при их парсинге BeautifulSoup.

Как загрузить больше нескольких первых отзывов?

Goodreads раскрывает дополнительные отзывы за кнопкой «Show more reviews», а не через нумерованный пейджер. Передайте параметр css_click_selector в Crawling API, нацеленный на эту кнопку, и она будет кликнута во время отрисовки, так что захваченный HTML будет включать расширенный список. Увеличьте page_wait, если вновь открытые отзывы должны завершить отрисовку до захвата.

Мои селекторы возвращают None. Что изменилось?

Почти наверняка разметка Goodreads. Его классы RatingStars и ReviewCard, атрибуты data-testid и обёртки секций меняются без предупреждения, поэтому селекторы, работавшие в прошлом месяце, могут перестать работать. Заново проверьте живую страницу книги в инструментах разработчика браузера и обновите селекторы. Периодическое обслуживание селекторов, это норма для любого производственного парсера.

Можно ли использовать официальный API Goodreads вместо парсинга?

На практике нет. Официальный API Goodreads фактически устарел и закрыт для новых ключей, поэтому нет живого первичного источника данных. Для публичных данных реалистичны два варианта: парсинг публичных страниц книг с подходом из этого руководства или получение данных через лицензированного провайдера. В любом случае соблюдайте условия использования, robots.txt и ограничения скорости.

Как не получить блокировку при парсинге Goodreads?

Поддерживайте низкую частоту запросов на IP, варьируйте цели вместо цикличного обхода одного пути и направляйте трафик через ротирующие резидентные IP, чтобы ни один адрес не активировал ограничение скорости. Crawling API управляет ротацией и доверенным пулом IP за вас; если вы создаёте собственный стек, именно в эту часть стоит инвестировать. Следите за кодами статусов и снижайте нагрузку при появлении заданий.

Начать создавать

Обходите любой сайт в масштабе, без борьбы с инфраструктурой.

Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.

Самообслуживание · Звонок отдела продаж не требуется · Доступны корпоративные объёмы краулинга