# China Hot Lists: Douyin, Weibo, Baidu, Bilibili, Zhihu (`sergeyfaraday/china-hot-lists`) Actor

Full top-N snapshots of Chinese trending boards with no keywords needed. Per-board status, heat in native units, ad and pinned flags, board update time and cache age.

- **URL**: https://apify.com/sergeyfaraday/china-hot-lists.md
- **Developed by:** [Sergey Faraday](https://apify.com/sergeyfaraday) (community)
- **Categories:** Social media, News
- **Stats:** 2 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## 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

## China Hot Lists: Douyin, Weibo, Baidu, Bilibili, Zhihu, Toutiao

Full top-N snapshots of Chinese trending boards, no keywords needed. Use it to see what China is talking about right now and to track how topics rise and fall between snapshots.

Each board is fetched and reported separately, so one failing board never hides behind an overall success.

### Boards

| Board | Value in `boards` | Ranking signal |
|---|---|---|
| Douyin hot search | `douyin_hot` | heat |
| Douyin rising | `douyin_rising` | position (the source reports heat as 0, returned as null) |
| Weibo hot search | `weibo_hot` | heat |
| Baidu hot search | `baidu_hot` | heat |
| Bilibili hot search | `bilibili_hot_search` | heat |
| Bilibili popular | `bilibili_popular` | video counters (views, likes, coins, shares, comments) |
| Zhihu hot list | `zhihu_hot` | heat |
| Toutiao hot board | `toutiao_hot` | heat |

### How to use

1. Run it as is to get six boards, or choose them in `boards`.
2. Schedule it a few times a day with a `snapshotId` per run and compare snapshots on your side to see topics rise and fall. The actor does not compare runs itself: a run cannot read other runs' datasets under limited permissions.

### Input

| Field | Default | Meaning |
|---|---|---|
| `boards` | 6 boards | which boards to snapshot |
| `topN` | 50 | ranked rows with position up to N; pinned and ad rows are always kept |
| `staleAfterMinutes` | 180 | a board older than this is reported as `partial` and its rows carry `data.stale: true`; set `staleIsPartial` to false to only flag it |
| `snapshotId` | `<actor>.<runId>` | your id for this snapshot; re-running with the same id gives the same row keys, so it does not create duplicates |
| `maxItems` | none | hard cap on rows. It is a cut, not a pause: a board the budget cannot reach is not fetched at all. A cut keeps ranked rows first, pinned and ad rows after them |

```json
{ "boards": ["douyin_hot", "weibo_hot", "baidu_hot"], "topN": 50 }
```

### Output

One `rank_entry` row per topic:

```json
{
  "recordType": "rank_entry",
  "platform": "douyin",
  "sourceRecordId": "douyin_hot:s2724502",
  "canonicalUrl": "https://www.douyin.com/hot/2724502",
  "time": {
    "publishedAt": null,
    "sourceUpdatedAt": "2026-11-01T17:41:56.000Z",
    "sourceUpdatedAtBasis": "data.active_time (Asia/Shanghai)",
    "observedAt": "2026-11-01T17:43:33.007Z",
    "observedAtBasis": "response_received",
    "availableAt": "2026-11-01T17:41:56.000Z",
    "availableAtReason": null,
    "collectedAt": "2026-11-01T17:43:33.014Z",
    "runStartedAt": "2026-11-01T17:43:31.000Z"
  },
  "metrics": {
    "position": { "value": 1, "sourceName": "platform_position", "unit": "rank", "kind": "rank" },
    "heat": { "value": 12111620, "sourceName": "hot_value", "unit": "douyin_hot_value", "kind": "index", "window": "snapshot" }
  },
  "data": { "board": "douyin_hot", "term": "...", "termIdStable": true, "position": 1, "isAd": null, "adEvidence": "not_provided_by_source", "isPinned": false, "stale": false, "labels": ["label_code_3"] },
  "sampling": { "boardFetchId": "f_edd2551d83e0", "topNRequested": 50, "visibleSize": 49, "rankedVisible": 49, "sourceAgeSeconds": 97 }
}
```

The `MANIFEST` record in the key-value store reports the status of every board (`ok`, `partial`, `blocked`, `failed`), visible size, rows selected and kept (`applied.selected`, `applied.kept`), truncation, cache age and errors. `applied.attempted: false` marks a board the run never fetched. Every requested board stands in every MANIFEST, also after an abort or a crash. `http` counts the requests of all workers of the run, and a request in flight when a worker was killed is not counted; `sources[].http` says what each board took. On `bilibili_popular`, reading stopped by `topN` while the source says more exists is `truncated: true` with `topN_requested`; a shorter list the source did not end is `partial` with `fewer_rows_than_topN_before_no_more`. With `keepRaw`, `raw` holds no account ids on any board.

### How to read the data

**Time.** These are different events, and an unknown one stays null:

| Field | Meaning |
|---|---|
| `runStartedAt` | the platform started the run; the same on every row, also after a restart |
| `observedAt` | the response of this board arrived; on a paged board, the response of this row's page |
| `collectedAt` | this collector built the row; use only this field to ask "what did we know at time T" |
| `sourceUpdatedAt`, `availableAt` | the board's own update time, Douyin `data.active_time` and Baidu `cards[0].updateTime`. Elsewhere null, and `sourceUpdatedAtBasis` and `availableAtReason` say why |

On every row `runStartedAt <= observedAt <= collectedAt`. A board's update time says when the platform refreshed the board, not when this collector knew the value.

**Staleness.** `data.stale` is `true` or `false` where the source publishes an update time and `null` where it does not: unknown is not fresh. A stamp more than 5 minutes ahead of the read says nothing about freshness either: `data.stale` and the age are `null`, and a note `SOURCE_STAMP_IN_FUTURE` names it.

**Ads.** `data.isAd` is `true` or `false` only on Weibo. `null` means the source gives no ad flag: unknown, not organic, so a paid row could sit in a ranked list unmarked. Weibo sets `is_ad` on ad rows only: 1, "1" and true mean an ad, 0, "0" and false mean none, and any other value gives `isAd: null` on that row with `SCHEMA_CHANGED`. A response with no ad slot is told by its rows: every row has a platform rank and none has `is_ad`, `ad_type`, `ad_channel` or `topic_ad`, so all are `isAd: false`. If no row carries a known flag and that does not hold, every listed row gets `isAd: null`, a row without a platform rank gets no position, and the board reports `SCHEMA_CHANGED` as `partial`.

**Changed sources.** A row of a board is defined by its term or title, its platform id and rank where the board has them, and its heat or counters. If one of these is absent or unreadable on every row, at any size, or on 10% or more of the rows from 10 rows up, the board reports `SCHEMA_CHANGED` and is `partial` while rows hold a term or an id, `failed` when none does. A value that cannot be read is `null` with `unparseable` and its raw text kept. A field holding null or an empty text is `source_null` or `source_empty`, and a field that is gone is `not_provided_by_source`. Zero on every ranked row is no measurement: `null` with `zero_not_confirmed_as_real` and `SCHEMA_CHANGED`; one zero among values stays 0. A missing pinned block (`data.hotgov`, `fixed_top_data`, Baidu `isTop` and `topContent`) or a missing `no_more` is reported the same way; null or an empty block says nothing is pinned. A row with neither a term nor an id is not delivered: it is listed in `sources[].excluded` with its place, with `ROWS_EXCLUDED`, and the board is `partial`. A row with a platform id and no term stays with `data.term: null` and a note `ROWS_WITHOUT_TERM`.

**Heat.** Heat is in each platform's native units and is not comparable between boards. The platforms do not define these numbers in their responses, and no normalization is applied or promised.

**Topic identity.** Ids are the platform's own where they exist (Douyin `sentence_id`, Zhihu question id, Toutiao `ClusterId`, Bilibili `bvid`). Elsewhere (Weibo, Baidu, Bilibili hot search, pinned rows) the id is a hash of the normalized text, marked `termIdStable: false`: it identifies a wording, not a topic, so a reworded topic gets a new id.

**Leaving the list.** Dropping out of the top N means leaving the visible part of the board, not zero interest.

**Restarts.** One board holds one fetch (`data.boardFetchId`). The rows of a fetch are saved in the run checkpoint before they are written, so a restart during the write delivers the rest of the same fetch under the same id and the board is not read again; the rows keep the times of the worker that received them. A board is read again only if the process died before that checkpoint. If the report of a fetch was lost with the process, the board is `partial` with `RESTART_REPORT_LOST`. If a run cannot read its own dataset back after a restart, a board whose rows were on their way is `partial` with `OWN_ROWS_UNREAD`. A cut made by `maxItems` is final: the run ends there, and a repeat with a larger limit under the same `snapshotId` reads the boards again.

### FAQ

**Can I compare heat between boards?** No. Each platform has its own units.

**Why is heat null on Douyin rising?** The source reports 0 for every rising topic, so the number says nothing. The position is real.

**Does `isAd: null` mean the row is organic?** No, it means unknown. Only Weibo marks ad rows.

**Why did a topic get a new id between two snapshots?** On boards without a platform id, the id follows the text. Check `data.termIdStable`.

### Cost

Runs in seconds. A measured 8-board snapshot at top 50 cost $0.0025 of platform usage at 512 MB; 6-board snapshots at top 50 cost $0.0010 to $0.0018. At $0.0025, four snapshots a day come to about $0.30 a month.

### Limits

Only openly served board endpoints are used. If a source blocks access, the board is reported as `blocked`, and no bypass is attempted.

# Actor input Schema

## `boards` (type: `array`):

Boards to snapshot. Default: six boards (Douyin hot, Douyin rising, Weibo, Baidu, Bilibili hot search, Zhihu).

## `topN` (type: `integer`):

Keep ranked rows with position <= N. Pinned and ad rows are always kept and flagged. The board size the platform showed is reported per board.

## `staleAfterMinutes` (type: `integer`):

Flag a board as stale when its own update time (Douyin active_time, Baidu updateTime) is older than this at fetch time: the manifest says so, and its rows carry data.stale=true. Boards with no such time have data.stale=null.

## `staleIsPartial` (type: `boolean`):

Mark a stale board as partial instead of ok (it is always flagged in the manifest).

## `snapshotId` (type: `string`):

Your id for this snapshot, one per task of one actor. Rows get recordKey = platform:type:id:snapshotId, so a repeat with the same snapshotId gives the same keys, and a new id gives a new observation. A repeat reads the source again: compare provenance.contentChecksum to see whether the values moved. Default: <actor>.<runId>.

## `queryId` (type: `string`):

Your search profile id, copied into provenance.queryId.

## `queryVersion` (type: `string`):

Search profile version, copied into provenance.queryVersion.

## `panelId` (type: `string`):

Fixed panel id for refresh runs, copied into provenance.panelId.

## `panelVersion` (type: `string`):

Panel composition version; a new composition must get a new version.

## `maxItems` (type: `integer`):

Hard cap on dataset rows for this run. Separate from the run money cap (maxTotalChargeUsd), which is set on the run itself.

## `maxRunSeconds` (type: `integer`):

Stop starting new units this many seconds after the run started and finish with a partial manifest. The budget belongs to the run: a restart does not renew it, and the time between workers counts.

## `keepRaw` (type: `boolean`):

Store the raw upstream item in `raw`. Off by default. provenance.rawChecksum is present on rows built from a raw item; it is null on a state row with no answer behind it and on derived rows.

## `failOnPartial` (type: `boolean`):

By default a partial result finishes as SUCCEEDED with status=partial in the MANIFEST record. Failed and blocked always finish as FAILED.

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

Proxy for upstream requests.

## Actor input object example

```json
{
  "boards": [
    "douyin_hot",
    "douyin_rising",
    "weibo_hot",
    "baidu_hot",
    "bilibili_hot_search",
    "zhihu_hot"
  ],
  "topN": 50,
  "staleAfterMinutes": 180,
  "staleIsPartial": true,
  "keepRaw": false,
  "failOnPartial": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

## `manifest` (type: `string`):

No description

# 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 = {
    "boards": [
        "douyin_hot",
        "douyin_rising",
        "weibo_hot",
        "baidu_hot",
        "bilibili_hot_search",
        "zhihu_hot"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("sergeyfaraday/china-hot-lists").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 = {
    "boards": [
        "douyin_hot",
        "douyin_rising",
        "weibo_hot",
        "baidu_hot",
        "bilibili_hot_search",
        "zhihu_hot",
    ],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("sergeyfaraday/china-hot-lists").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 '{
  "boards": [
    "douyin_hot",
    "douyin_rising",
    "weibo_hot",
    "baidu_hot",
    "bilibili_hot_search",
    "zhihu_hot"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call sergeyfaraday/china-hot-lists --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,sergeyfaraday/china-hot-lists"
        }
    }
}
```

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/ibAvXXGdXVUMUPeNr/builds/PEzUsBYeu4hftXSFf/openapi.json
