# Douban 豆瓣 Scraper: Movies, Books, Music, Charts & Ratings (`deepurple/douban-scraper`) Actor

Douban (豆瓣电影, 豆瓣读书, 豆瓣音乐) movies, TV, books and music: ratings, cast, Top 250, charts and 29 genre rankings, short comments with stars, full reviews, search, rating and rank change tracking. No login, no browser. From $4.35 per 1,000 rows.

- **URL**: https://apify.com/deepurple/douban-scraper.md
- **Developed by:** [Misha Ershov](https://apify.com/deepurple) (community)
- **Categories:** Social media, Videos, Marketing
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.35 / 1,000 douban title cards

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Douban Scraper: Movies, Music, Books, Charts & Rating Tracker

Douban movies, TV series, books and music albums as clean rows: ratings, cast, tracklists, tags and Top 250 rank, the most
popular short comments with stars, reviews with full text, keyword search, the Top 250 lists, Douban charts (now showing,
weekly, hot, bestsellers, 29 genre rankings) and change tracking: rating and rank moves between two runs. One row per item.
Plain HTTP requests through the Apify datacenter proxy: no login, no cookies, no extra software.

### Modes

| Mode | What it does | Example input |
|---|---|---|
| `subjects` (default) | one `subject` row per film, series, book or album you give, plus its comments and reviews on request | `{"subjectUrls": ["https://movie.douban.com/subject/1292052/", "https://book.douban.com/subject/2567698/"], "includeComments": true}` |
| `search` | films and series (books with `contentType: "book"`, albums with `contentType: "music"`) for each keyword, one `searchResult` row each | `{"mode": "search", "keywords": ["三体"], "maxResults": 40}` |
| `top250` | the Douban Top 250 films (or books), one `top250` row each | `{"mode": "top250", "maxResults": 100}` |
| `charts` | Douban charts and genre rankings, one `chartItem` row per position | `{"mode": "charts", "charts": ["movie_showing", "movie_weekly_best"]}` |

`contentType` is `movie` (films and TV series; a series is given as a movie link), `book` or `music`. It applies to `search` and
`top250` and is ignored in `subjects`, where the link decides (`movie.`, `book.` or `music.douban.com`). Music has no Top 250.

### Charts

Mode `charts` reads the charts you pick in `charts` (one row per position, with `rank`, rating and rating count). Empty
`charts` and `genreCharts` mean now showing + weekly best. 12 collections:

| Value | Chart | Place and place change |
|---|---|---|
| `movie_showing` | Movies: now showing (正在热映) | no |
| `movie_hot_gaia` | Movies: hot (热门) | no |
| `movie_real_time_hotest` | Movies: trending now (实时热门) | yes |
| `movie_weekly_best` | Movies: weekly best (一周口碑榜) | yes |
| `movie_soon` | Movies: coming soon (即将上映) | no |
| `tv_hot` | TV: hot (热门剧集) | no |
| `tv_real_time_hotest` | TV: trending now (实时热门剧集) | yes |
| `tv_chinese_best_weekly` | TV: Chinese weekly (华语口碑剧集榜) | yes |
| `tv_global_best_weekly` | TV: global weekly (全球口碑剧集榜) | yes |
| `tv_variety_show` | TV: variety shows (综艺) | no |
| `book_bestseller` | Books: bestsellers (畅销图书) | no |
| `music_single` | Music: new singles (新碟/单曲) | no |

Sizes seen on 2026-10-13: now showing 54, hot movies 313, hot series 252, variety shows 63, book bestsellers 50, new singles 19;
the weekly and trending charts hold 10 places each. The weekly and trending charts also return `rankChange`, the move in
places Douban shows; the other charts have `rankChange: null`.

Genre rankings (`genreCharts`, films only) cover 29 genres: Documentary, Biography, Crime, History, Action, Erotic, Musical,
Kids, Mystery, Drama, Disaster, Romance, Music, Adventure, Fantasy, Sci-Fi, Sport, Thriller, Horror, War, Short, Comedy,
Animation, LGBT, Western, Family, Wuxia, Costume, Film Noir. `genreScoreBand` picks the slice of the genre to list. Douban splits
every genre into 10% slices by how a film scores against the rest of the genre: `100:90` = films better than 90-100% of the
genre (the best 10%), `90:80` = the next 10%, and so on down to `10:0`. It is a percentile inside the genre, not a range of
ratings. A slice holds about 1100 films, read in pages of 20, best rated first.

```json
{"mode": "charts", "charts": ["movie_weekly_best", "tv_chinese_best_weekly"], "genreCharts": ["Drama", "Sci-Fi"], "genreScoreBand": "100:90"}
```

### Track rating and rank changes

Set `trackChanges: true` and every `subject`, `top250` and `chartItem` row is compared with the previous run of the same
monitor. The snapshot lives in a named key-value store on your own Apify account, one record. The name is `trackerName` (default
`douban-tracker`, lower-case letters, digits and dashes); use a different name for every monitor so snapshots do not mix.

New fields on tracked rows (all `null` when tracking is off): `changeStatus`, `previousRating`, `ratingChange`,
`previousRatingCount`, `ratingCountChange`, `commentCountChange` (subjects), `previousRank`, `rankDelta` (previous place minus
current place, so a positive number means the item moved up), `firstSeenAt`, `lastSeenAt`.

| `changeStatus` | Meaning |
|---|---|
| `new` | not in the previous snapshot |
| `changed` | rating, rating count or (subjects) comment or review count differ |
| `same` | nothing differs |
| `up` / `down` | the place in a chart or in the Top 250 moved up or down |
| `dropped` | was in the previous snapshot of a chart, is not in it now (a row with `rank: null`, `previousRank` and the old values) |

`onlyChanges: true` (with `trackChanges`) leaves out rows with status `same`: they are not returned and not paid for, and are
counted in `RUN_SUMMARY.skippedUnchanged`. `maxResults` counts the skipped rows too. To run it as a monitor, open Apify Console,
Schedules, create a schedule (daily or weekly) and attach the Actor with the same input.

Example, weekly chart run with `{"mode": "charts", "charts": ["movie_weekly_best"], "trackChanges": true, "onlyChanges": true}`
(the row is computed by the actor's diff code against a previous snapshot in which the film stood 3 places lower):

```json
{
  "type": "chartItem",
  "chart": "movie_weekly_best",
  "chartTitle": "Movies: weekly best (一周口碑榜)",
  "rank": 2,
  "rankChange": 9,
  "id": "37068446",
  "kind": "movie",
  "title": "生化危机：爆发夜",
  "rating": 7.3,
  "ratingCount": 152283,
  "subtitle": "2026 / 德国 法国 美国 / 科幻 惊悚 恐怖 / 扎克·克雷格 / 奥斯汀·艾布拉姆斯 扎克·切利",
  "url": "https://movie.douban.com/subject/37068446/",
  "coverUrl": "https://img2.doubanio.com/view/photo/m_ratio_poster/public/p2936442661.jpg",
  "changeStatus": "up",
  "previousRating": 7.3,
  "ratingChange": 0.0,
  "previousRatingCount": 152283,
  "ratingCountChange": 0,
  "previousRank": 5,
  "rankDelta": 3,
  "firstSeenAt": "2026-10-06T09:00:00Z",
  "lastSeenAt": "2026-10-06T09:00:00Z"
}
```

### Input

| Field | Default | Meaning |
|---|---|---|
| `mode` | `subjects` | `subjects`, `search`, `top250` or `charts` |
| `subjectUrls` | `[]` | mode `subjects`: `movie.`, `book.` or `music.douban.com/subject/<id>/`, `m.douban.com/movie/subject/<id>` URLs or a bare numeric id (a bare id is tried as a film, then as a series). Entries without an id are skipped, logged and counted in `invalidInputs` |
| `keywords` | `[]` | mode `search`: Chinese or English words |
| `maxResults` | `50` | 1 to 1000, per keyword (search) or in total (top250, 250 exist); in charts per chart. With `onlyChanges` skipped unchanged rows count too |
| `includeComments` | `false` | add the most popular short comments of every subject |
| `maxCommentsPerSubject` | `50` | 1 to 400. Douban caps films and series at 150 (books at 100) and albums at about 400 |
| `includeReviews` | `false` | add the most popular reviews of every subject |
| `maxReviewsPerSubject` | `20` | 1 to 40 |
| `fullReviewText` | `true` | open every review for its full text, useful and useless votes, read count and the original flag |
| `fetchDetails` | `false` | modes `search` and `top250`: open the card of every result and return a `subject` row instead of the short list row. Comments and reviews then work as in mode `subjects` |
| `contentType` | `movie` | `movie` (Movies & TV), `book` or `music` (albums, keyword search only). Modes `search` and `top250`. Ignored in mode `subjects` |
| `minRating` | `0` | 0 to 10; list results rated lower (or unrated) are dropped. 0 = off |
| `minRatingCount` | `0` | list results with fewer ratings are dropped. 0 = off |
| `yearFrom`, `yearTo` | `0` | list results outside the years (or without a known year) are dropped. 0 = off; if `yearFrom` > `yearTo` they are swapped |
| `genres` | `[]` | keep list results with at least one of these genres (Chinese names, English labels in the form). Ignored for books and music |
| `charts` | `[]` | mode `charts`: the collections listed in "Charts". Empty with no `genreCharts` = `movie_showing` + `movie_weekly_best` |
| `genreCharts` | `[]` | mode `charts`: genre rankings of films, English genre names (see "Charts") |
| `genreScoreBand` | `100:90` | genre rankings: percentile slice inside the genre, `100:90` (best 10%) down to `10:0` |
| `sortBy` | `none` | `none` (source order), `rating`, `ratingCount` or `year`, high to low. Results are collected first (up to `maxResults`), then sorted; unknown values go last. Search sorts within one keyword |
| `extraDetails` | `false` | `subject` rows only: add rating distribution, want/done counts, genre ranks, IMDb id and awards. Slower (2 to 3 extra requests per card), the price is the same `subject` event |
| `trackChanges` | `false` | compare subject, Top 250 and chart rows with the previous run (see "Track rating and rank changes") |
| `onlyChanges` | `false` | with `trackChanges`: do not return (and do not pay for) rows that did not change |
| `trackerName` | `douban-tracker` | name of the key-value store with the snapshot; one name per monitor |
| `proxyConfiguration` | Apify datacenter | datacenter proxy is enough |

Filters (`minRating`, `minRatingCount`, `yearFrom`, `yearTo`, `genres`) run before anything is returned or opened, so dropped
results are neither charged nor fetched as cards; `RUN_SUMMARY.filtered` counts them by reason. Filters that drop everything
are not an error. Invalid values (a rating outside 0 to 10, negative numbers, text) switch that filter off.

Examples: books by keyword `{"mode": "search", "keywords": ["三体"], "contentType": "book"}`; the best-rated crime films since
2000 `{"mode": "top250", "genres": ["犯罪"], "yearFrom": 2000, "sortBy": "rating"}`; full cards with extras
`{"subjectUrls": ["1292052"], "extraDetails": true}`.

Mode `subjects` needs `subjectUrls`, mode `search` needs `keywords`. Otherwise the run ends with exit code 1 and a clear
status message; nothing is charged. In modes `search` and `top250` comments and reviews are returned only with `fetchDetails`. Mode `charts` needs `charts` or `genreCharts` (or neither, for the default pair).

### Honest limits

Douban shows visitors who are not logged in:

- short comments: at most 150 of a film or series (100 of a book), up to about 400 of an album; the most popular ones only;
- reviews: at most about 40 per title;
- newest-first order is not available without login. For albums it is closed outright (HTTP 403), so music comments are
  popular-first only;
- the desktop pages (movie.douban.com) are behind a sec.douban.com check, so full comment feeds beyond 150 are not reachable
  without a login or a browser. This actor uses neither, so these are the ceilings. The total counts (`commentCount`,
  `reviewCount`) are still returned on the subject row;
- genre rankings exist for films only;
- music has no Top 250 and `genres` filters do not apply to it.

Change tracking limits: `dropped` rows are returned only when the chart was read to its end (its total). A run that stops on
`maxResults`, on the budget or on an error emits none, because the missing items may simply not have been read.
`RUN_SUMMARY.skippedUnchanged` appears only with `trackChanges`. Mode `search` without `fetchDetails` is not tracked.

### Output

Rows from `extraDetails` add to the subject row: `ratingDistribution` (percent per star, keys `"5"` to `"1"`), `wishCount`,
`doneCount`, `typeRanks` (`[{"type": "犯罪片", "betterThanPercent": 99}]`), `imdbId` and `awards` (`ceremony`, `year`,
`category`, `won`, `url`). Without `extraDetails`, and for books (`imdbId`, `awards`), they are `null`. `searchResult` and
`top250` rows also carry `year` and `genres` (genres are `[]` for books); `top250` rows carry `kind`. A failed extra request
leaves its fields `null` and is counted in `RUN_SUMMARY.extraFailed`; the row is still returned.

Every row has a `type`: `subject`, `comment`, `review`, `searchResult`, `top250` or `chartItem`. Fields the source does not have are
`null`, never `0`. Times are ISO 8601 UTC (Douban shows Beijing time, UTC+8). Long fields below are shortened, the values
shown are real.

Subject row, film (`scrapedAt`, the UTC time of the scrape, and the tracking fields are left out here; a series has `kind: "tv"` and `episodes`):

```json
{
  "type": "subject",
  "id": "1292052",
  "kind": "movie",
  "title": "肖申克的救赎",
  "originalTitle": "The Shawshank Redemption",
  "year": 1994,
  "rating": 9.7,
  "ratingCount": 3350090,
  "genres": [
    "剧情",
    "犯罪"
  ],
  "countries": [
    "美国"
  ],
  "languages": [
    "英语"
  ],
  "durations": [
    "142分钟"
  ],
  "directors": [
    "弗兰克·德拉邦特"
  ],
  "actors": [
    "蒂姆·罗宾斯",
    "摩根·弗里曼",
    "鲍勃·冈顿"
  ],
  "releaseDates": [
    "2026-08-28(中国大陆)"
  ],
  "intro": "一场谋杀案使银行家安迪（蒂姆·罗宾斯 Tim Robbins 饰）蒙冤入狱，谋杀妻子及其情人的指控将囚禁他终生。在肖申克…",
  "commentCount": 668186,
  "reviewCount": 14990,
  "aka": [
    "月黑高飞(港)",
    "刺激1995(台)",
    "地狱诺言",
    "铁窗岁月",
    "消香克的救赎"
  ],
  "tags": [
    "犯罪",
    "越狱",
    "励志",
    "信念"
  ],
  "top250Rank": 1,
  "episodes": null,
  "authors": null,
  "translators": null,
  "publisher": null,
  "publishDate": null,
  "pages": null,
  "price": null,
  "subtitle": null,
  "coverUrl": "https://img3.doubanio.com/view/photo/m_ratio_poster/public/p2934829882.webp",
  "url": "https://movie.douban.com/subject/1292052/"
}
```

Subject row, book (film-only fields stay `null`, book-only fields are `null` on films):

```json
{
  "type": "subject",
  "id": "2567698",
  "kind": "book",
  "title": "三体",
  "originalTitle": null,
  "year": null,
  "rating": 8.9,
  "ratingCount": 518667,
  "genres": null,
  "countries": null,
  "languages": null,
  "durations": null,
  "directors": null,
  "actors": null,
  "releaseDates": null,
  "intro": "文化大革命如火如荼进行的同时。军方探寻外星文明的绝秘计划“红岸工程”取得了突破性进展。但在按下发射键的那一刻，历经劫难的…",
  "commentCount": 125005,
  "reviewCount": 4800,
  "aka": null,
  "tags": [],
  "top250Rank": null,
  "episodes": null,
  "authors": [
    "刘慈欣"
  ],
  "translators": [],
  "publisher": "重庆出版社",
  "publishDate": "2008-1",
  "pages": 302,
  "price": "23.00",
  "subtitle": "“地球往事”三部曲之一",
  "coverUrl": "https://img3.doubanio.com/view/subject/l/public/s2768378.jpg",
  "url": "https://book.douban.com/subject/2567698/"
}
```

Music album (`kind: "music"`; the tracking fields, all `null` here, are left out; lists are cut to 3 elements, as in every example):

```json
{
  "type": "subject",
  "id": "26812952",
  "kind": "music",
  "title": "周杰伦的床边故事",
  "year": 2016,
  "rating": 8.1,
  "ratingCount": 31074,
  "genres": [
    "流行"
  ],
  "intro": "夜深了\n猫头鹰出没\n数完第一千零一个尖叫声\n开始听周杰伦 的床边故事\nJay Chou’s Bedtime Stories\n今年最令人激动的乐坛震撼弹\n周杰伦2…",
  "commentCount": 9692,
  "reviewCount": 139,
  "tags": [],
  "publisher": "杰威尔音乐",
  "publishDate": "2016-06-24",
  "coverUrl": "https://img9.doubanio.com/view/subject/m/public/s28836865.jpg",
  "url": "https://music.douban.com/subject/26812952/",
  "artists": [
    "周杰伦"
  ],
  "media": "CD",
  "tracks": [
    "床边故事",
    "说走就走",
    "一点点"
  ]
}
```

Chart row from a collection (`rankChange` is the move in places Douban shows on the weekly and trending charts; the tracking fields and `scrapedAt` are left out):

```json
{
  "type": "chartItem",
  "chart": "movie_weekly_best",
  "chartTitle": "Movies: weekly best (一周口碑榜)",
  "rank": 1,
  "rankChange": 0,
  "id": "35322132",
  "kind": "movie",
  "title": "罗斯",
  "rating": 8.5,
  "ratingCount": 21268,
  "subtitle": "2026 / 奥地利 德国 / 剧情 / 马库斯·施莱泽 / 桑德拉·惠勒 卡洛·布劳恩",
  "year": null,
  "genres": null,
  "releaseDate": null,
  "url": "https://movie.douban.com/subject/35322132/",
  "coverUrl": "https://img3.doubanio.com/view/photo/m_ratio_poster/public/p2935294362.jpg"
}
```

Chart row from a genre ranking:

```json
{
  "type": "chartItem",
  "chart": "genre:Drama:100:90",
  "chartTitle": "Drama",
  "rank": 1,
  "rankChange": null,
  "id": "1292052",
  "kind": "movie",
  "title": "肖申克的救赎",
  "rating": 9.7,
  "ratingCount": 3350220,
  "subtitle": "美国 / 犯罪 / 剧情",
  "year": 2026,
  "genres": [
    "犯罪",
    "剧情"
  ],
  "releaseDate": "2026-08-28",
  "url": "https://movie.douban.com/subject/1292052/",
  "coverUrl": "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p2934829882.jpg"
}
```

A row with tracking is shown in "Track rating and rank changes".

Comment row:

```json
{
  "type": "comment",
  "subjectId": "1292052",
  "subjectKind": "movie",
  "commentId": "28737569",
  "text": "人的生命不过是从一个洞穴通往另一个世界..然后在那个世界的雨中继续颤抖.i hope",
  "stars": 4,
  "createdAt": "2008-01-13T17:53:08Z",
  "votes": 5238,
  "ipLocation": null,
  "userId": "1781395",
  "userName": "文泽尔",
  "userUid": "wenzel",
  "userLocation": "苏黎世"
}
```

Review row (with `fullReviewText`; without it `text`, `uselessCount`, `readCount` and `isOriginal` are `null`):

```json
{
  "type": "review",
  "subjectId": "1292052",
  "subjectKind": "movie",
  "reviewId": "1000369",
  "title": "十年·肖申克的救赎",
  "abstract": "距离斯蒂芬·金（Stephen King）和德拉邦特（Frank Darabont）们缔造这部伟大的作品已经有十年了。我…",
  "text": "距离斯蒂芬·金（Stephen King）和德拉邦特（Frank Darabont）们缔造这部伟大的作品已经有十年了。我…",
  "stars": 5,
  "createdAt": "2005-05-12T12:44:13Z",
  "usefulCount": 18956,
  "uselessCount": 735,
  "commentsCount": 1082,
  "readCount": 1428817,
  "isOriginal": true,
  "url": "https://movie.douban.com/review/1000369/",
  "userId": "1000152",
  "userName": "大头绿豆"
}
```

Search result row:

```json
{
  "type": "searchResult",
  "keyword": "三体",
  "position": 1,
  "id": "26647087",
  "kind": "tv",
  "title": "三体",
  "year": 2023,
  "rating": 8.7,
  "ratingCount": 540074,
  "subtitle": "中国大陆 / 剧情 科幻 / 杨磊 / 张鲁一 于和伟",
  "coverUrl": "https://qnmob3-sign.doubanio.com/view/photo/large/public/p2886492021.jpg",
  "url": "https://movie.douban.com/subject/26647087/",
  "genres": [
    "剧情",
    "科幻"
  ]
}
```

Top 250 row:

```json
{
  "type": "top250",
  "rank": 1,
  "id": "1292052",
  "title": "肖申克的救赎",
  "rating": 9.7,
  "ratingCount": 3350090,
  "subtitle": "1994 / 美国 / 剧情 犯罪 / 弗兰克·德拉邦特 / 蒂姆·罗宾斯 摩根·弗里曼",
  "quote": "希望让人自由。",
  "url": "https://movie.douban.com/subject/1292052/",
  "year": 1994,
  "genres": [
    "剧情",
    "犯罪"
  ],
  "kind": "movie"
}
```

### How the run works

Requests go to Douban's mobile JSON API and carry the `Referer` it requires. A request that fails (HTTP 403, 5xx or a
transport error) is retried up to 3 times, each on a new proxy session. A subject or list that still fails is logged,
counted in `RUN_SUMMARY.failed` and skipped; the rest goes on. A page after the first one that comes back empty, repeats
rows already seen or answers HTTP 403 is the end of what Douban shows to visitors: the source stops with `stoppedBy:
"sourceLimit"`, which is not a failure. An unknown id is counted in `notFound`. Rows are deduplicated by id within a run.
Requests are paced (one every 1 to 2 seconds). `RUN_SUMMARY` holds `bySource` (pages, items and `stoppedBy` per source),
`pushed` by type, `charged` per event, `failed`, `notFound`, `invalidInputs`, `skippedUnchanged` (only with `trackChanges`) and `stoppedBy` (`maxResults`, `sourceLimit`,
`budget` or `end`).

### FAQ

**Do I need a Douban account?** No. No login or cookies are used.

**Why not more comments or reviews?** See "Honest limits": that is all Douban shows to anonymous visitors.

**Why only 150 comments per movie?** Douban's mobile API gives visitors who are not logged in the 150 most popular short
comments of a film. The desktop pages with longer feeds are behind a sec.douban.com check, so more comments are not reachable
without a login or a browser. Albums go further, up to about 400.

**Can I get the newest comments instead of the most popular?** No. Douban answers the newest-first order with an error for
visitors who are not logged in, so only the popular order is used.

**How do I monitor a weekly chart?** Run mode `charts` with `charts: ["movie_weekly_best"]`, `trackChanges: true` and
`onlyChanges: true`, then put it on a weekly schedule (Console, Schedules). Every run returns only the films that are new, moved
or changed rating, plus `dropped` rows for films that left the chart.

**Can I get music comments sorted by newest?** No. Douban closes the newest-first order for albums to visitors who are not
logged in, so only the popular order is used.

**Are TV series supported?** Yes. A series is a `subject` row with `kind: "tv"` and `episodes`, and its comments and
reviews work like a film's.

**Pricing events:** one event per row: `subject`, `comment`, `review` (abstract only), `review-full` (full text, instead
of `review`), `list-row` (search and Top 250 rows).

Missing something? Open an issue. We add fields on request.

### Development

```
cd partner_actors/dima/douban-scraper
python3 -m pytest -q
```

# Actor input Schema

## `mode` (type: `string`):

subjects: given films, series, books or albums. search: by keyword. top250: the Douban Top 250 films or books. charts: Douban charts (now showing, weekly, hot, bestsellers, genre rankings).

## `subjectUrls` (type: `array`):

Mode subjects: movie.douban.com/subject/<id>/, book.douban.com/subject/<id>/, music.douban.com/subject/<id>/, m.douban.com/movie/subject/<id> URLs, or a bare numeric id (taken as a film or series).

## `keywords` (type: `array`):

Mode search: Chinese or English words, for example "三体". Films and series are searched; set Content type to Books for books.

## `maxResults` (type: `integer`):

Per keyword (search) or in total (top250, at most 250 exist). 1 to 1000.

## `includeComments` (type: `boolean`):

Also return the most popular short comments with stars of every subject. Search and top250 need Fetch full cards.

## `maxCommentsPerSubject` (type: `integer`):

1 to 400. Douban shows visitors who are not logged in at most the 150 most popular comments of a film, series or book (100 for books) and up to 400 of an album.

## `includeReviews` (type: `boolean`):

Also return the most popular reviews of every subject. Search and top250 need Fetch full cards.

## `maxReviewsPerSubject` (type: `integer`):

1 to 40. Douban shows visitors who are not logged in at most the 40 most popular reviews.

## `fullReviewText` (type: `boolean`):

Open every review for its full text, useful and useless votes and read count (one extra request per review, billed as review-full). Off: abstract only.

## `fetchDetails` (type: `boolean`):

Search and top250: open the card of every result and return a subject row instead of a short list row; comments and reviews then work as in mode subjects.

## `contentType` (type: `string`):

Modes search and top250: Movies & TV, Books (book Top 250) or Music (albums; keyword search only, music has no Top 250). Ignored in mode subjects.

## `minRating` (type: `number`):

Search and top250: drop results rated below this (0-10). Unrated results are dropped too. 0 = off.

## `minRatingCount` (type: `integer`):

Search and top250: drop results with fewer ratings. 0 = off.

## `yearFrom` (type: `integer`):

Search and top250: drop results released before this year. Results without a known year are dropped. 0 = off.

## `yearTo` (type: `integer`):

Search and top250: drop results released after this year. Results without a known year are dropped. 0 = off.

## `genres` (type: `array`):

Search and top250 of films and series: keep results with at least one of these genres. Empty = all. Ignored for books and music.

## `charts` (type: `array`):

Mode charts: Douban charts to scrape, one row per position with rank, rank change (when given), rating and rating count. Empty with no genre charts = now showing + weekly best.

## `genreCharts` (type: `array`):

Mode charts: film rankings by genre (29 genres), best rated first within the chosen score band. Films only.

## `genreScoreBand` (type: `string`):

Genre charts: which slice of the genre's Douban ranking to list. Douban splits every genre into 10% slices by how a film scores against the rest of the genre: 100:90 = films better than 90-100% of the genre (the best 10%), 90:80 = the next 10%, and so on.

## `sortBy` (type: `string`):

Search and top250: sort the results. Rows are collected first (up to Max results), then sorted; unknown values go last. Search sorts within one keyword.

## `extraDetails` (type: `boolean`):

Subject cards only: add the rating distribution by stars, want/done counts, IMDb id and awards (2-3 extra requests per card, slower, same price).

## `trackChanges` (type: `boolean`):

Compare every subject, Top 250 and chart row with the previous run: new or not, rating and ratings-count change, place shift; items that left a fully read chart come as dropped rows. The previous snapshot is kept in a named key-value store on your account.

## `onlyChanges` (type: `boolean`):

With Track changes: skip and do not pay for rows that did not change since the previous run. Use it to run the Actor on a schedule as a monitor.

## `trackerName` (type: `string`):

Name of the key-value store with the snapshot (lower-case letters, digits and dashes, up to 63). Use different names for different monitors.

## `proxyConfiguration` (type: `object`):

Apify datacenter proxy is enough.

## Actor input object example

```json
{
  "mode": "subjects",
  "subjectUrls": [
    "https://movie.douban.com/subject/1292052/",
    "https://book.douban.com/subject/2567698/"
  ],
  "keywords": [
    "三体"
  ],
  "maxResults": 50,
  "includeComments": false,
  "maxCommentsPerSubject": 50,
  "includeReviews": false,
  "maxReviewsPerSubject": 20,
  "fullReviewText": true,
  "fetchDetails": false,
  "contentType": "movie",
  "minRating": 0,
  "minRatingCount": 0,
  "yearFrom": 0,
  "yearTo": 0,
  "genres": [],
  "charts": [
    "movie_showing",
    "movie_weekly_best"
  ],
  "genreCharts": [],
  "genreScoreBand": "100:90",
  "sortBy": "none",
  "extraDetails": false,
  "trackChanges": false,
  "onlyChanges": false,
  "trackerName": "douban-tracker",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (type: `string`):

Douban rows: movie, TV and book cards, short comments, reviews, search results and Top 250 rows. The type field tells which.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "subjectUrls": [
        "https://movie.douban.com/subject/1292052/",
        "https://book.douban.com/subject/2567698/"
    ],
    "keywords": [
        "三体"
    ],
    "maxResults": 50,
    "charts": [
        "movie_showing",
        "movie_weekly_best"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("deepurple/douban-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "subjectUrls": [
        "https://movie.douban.com/subject/1292052/",
        "https://book.douban.com/subject/2567698/",
    ],
    "keywords": ["三体"],
    "maxResults": 50,
    "charts": [
        "movie_showing",
        "movie_weekly_best",
    ],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("deepurple/douban-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "subjectUrls": [
    "https://movie.douban.com/subject/1292052/",
    "https://book.douban.com/subject/2567698/"
  ],
  "keywords": [
    "三体"
  ],
  "maxResults": 50,
  "charts": [
    "movie_showing",
    "movie_weekly_best"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call deepurple/douban-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,deepurple/douban-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/OgTbS7umHHdei6abN/builds/xPrMFElKdgJZdLOBq/openapi.json
