# Bilibili (哔哩哔哩) Scraper - Videos, Comments & Replies (`lanternlab318/bilibili-scraper`) Actor

Search Bilibili videos by keyword or by link and collect engagement counts, tags and optionally comments with replies.

- **URL**: https://apify.com/lanternlab318/bilibili-scraper.md
- **Developed by:** [Lantern Lab](https://apify.com/lanternlab318) (community)
- **Categories:** Social media
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$4.99 / 1,000 results

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

## Bilibili Video & Comment Scraper

Search Bilibili (哔哩哔哩) videos by keyword, or hand it video links, and get one row per video with the full engagement counts Bilibili shows on the page: views, likes, coins, favourites, shares, danmaku and comment count, plus the description, tags, duration, publish time, cover and author. Turn comments on and it collects the comments and the replies under them as well. No Bilibili login, no cookies, no account of your own. **$4.99 per 1,000 results (a video or a comment row), no start fee.** See [Pricing](#pricing).

Input:

```json
{
  "keywords": ["美食"],
  "maxVideosPerKeyword": 20,
  "includeComments": false
}
```

Output - one row per video (`type: "video"`); keyword- and video-summary rows are free. Sample from a real run; channel name partially masked, cover link shortened, tags trimmed:

```json
{
  "type": "video",
  "bvid": "BV1q6nb6cE4P",
  "aid": "117488947892299",
  "title": "与众09沿海而行，邂逅悠然青岛",
  "description": "追风逐浪，与众09载着海风向前 \n纯电静谧，恰与大海的沉静相融…",
  "url": "https://www.bilibili.com/video/BV1q6nb6cE4P",
  "coverUrl": "http://i1.hdslb.com/bfs/archive/…",
  "durationSec": 60,
  "publishedAt": "2026-10-23T07:06:35.000Z",
  "author": { "mid": "3546679701670263", "name": "大众***" },
  "stats": {
    "views": 22, "likes": 1, "coins": 0, "favorites": 1,
    "shares": 1, "danmaku": 0, "comments": 0
  },
  "tags": ["#大众安徽", "#大众09大众最美智能纯电轿跑", "…"],
  "keyword": "与众09",
  "collectedAt": "2026-10-23T08:04:48.468Z",
  "partial": false,
  "partialReason": null,
  "commentsRequested": false,
  "commentStatus": "not_requested"
}
```

Limits:

- **Public data only.** Reads what Bilibili shows a signed-out visitor; the Actor never logs in. No danmaku, no IP region on comments (Bilibili does not return it signed out), no downloads.
- **Every keyword gets a summary row saying whether its videos were collected in full, and every video says whether its comments were.** A video or keyword that could not be read is marked `failed` with the reason - never "0 results". Bilibili's own search stops at 1,000 videos per keyword.
- **Your limits stop collection and appear in the row**: `maxVideosPerKeyword`, `maxCommentsPerVideo`, the `maxUpstreamRequests` request ceiling and the maximum charge per run (see [Pricing](#pricing)).

### What it does

- Searches each keyword and reads the result pages until your limit or the end of what Bilibili returns. Bilibili's own search stops at 1,000 videos per keyword.
- Reads each video's own detail page, so `coins` and `shares` are real numbers rather than blanks - the search results alone do not carry them.
- Collects comments in the order the video page opens with (hot first), together with the replies under each comment. A comment page already carries up to three replies per comment; the reply endpoint is only called when a comment shows more than came with it.
- Takes video links, short links (`b23.tv`), share text copied from the app, BV IDs and av numbers. A line that is none of these stops the run at once - nothing is collected or charged.
- Compares what it collected with what Bilibili claims: per keyword (results reported vs. collected) and per video (comment count vs. comments collected).

### What it does not do

- No danmaku (the on-screen comments), no channel follower lists, no video or audio downloads, no anything that needs a logged-in account.
- No IP region on comments. Bilibili does not return it on signed-out reads, so `author.ipLocation` is `null` rather than filled in from elsewhere.
- No multi-part handling: `durationSec` is the whole video's length, as Bilibili reports it.
- No scheduling or monitoring built in; use Apify schedules.

### Input

| Field | Default | Meaning |
| --- | --- | --- |
| `keywords` | - | One search term per line. |
| `videoUrls` | - | One video per line: `https://www.bilibili.com/video/BV...`, `https://b23.tv/...`, share text from the app, a BV ID such as `BV1xx411c7mD`, or an av number such as `av2`. Collected before the keywords. |
| `maxVideosPerKeyword` | 20 | Videos to collect per keyword (1-1000). |
| `sort` | `relevance` | Search order: `relevance`, `newest`, `mostPlayed` or `mostDanmaku`. Does not affect `videoUrls`. |
| `includeComments` | `false` | Also collect each video's comments and their replies. |
| `maxCommentsPerVideo` | 50 | Comments **and replies together** per video (1-2000). |
| `maxUpstreamRequests` | derived | Hard ceiling on requests to Bilibili for the whole run. Leave empty and the Actor works one out from the rest of your input. |

At least one of `keywords` and `videoUrls` is required. Example:

```json
{
  "keywords": ["美食", "旅行"],
  "videoUrls": ["https://www.bilibili.com/video/BV1xx411c7mD"],
  "maxVideosPerKeyword": 20,
  "sort": "relevance",
  "includeComments": true,
  "maxCommentsPerVideo": 50
}
```

The same video entered twice, or found under two keywords, is collected and charged once; the repeat gets a `video_summary` row with `stopReason: "duplicate"`.

`maxUpstreamRequests` gives you a known upper bound before the run starts. It counts every request the run sends to Bilibili - search pages, video details, comment pages, reply pages, the signing-key read and every retry. Leave it empty and the Actor derives a ceiling from your keywords, video links, `maxVideosPerKeyword` and comment settings, so a run is never unbounded. When the ceiling is reached, what was being collected is marked `partial` with `stopReason: "upstream_request_cap"`, anything after it is reported as not collected, and the run ends normally. This is a limit of yours, not a problem at Bilibili, which may have more.

### Output

One dataset; `type` tells the rows apart. Four table views are predefined: Videos, Comments and replies, Keyword summaries, and Videos not collected. A `RUN_SUMMARY` record in the key-value store carries the run totals, the status of each keyword, the request counts and why collection stopped.

#### `type: "video"`

| Field | Meaning |
| --- | --- |
| `bvid`, `aid` | Bilibili's two IDs for the video. |
| `title`, `description` | As Bilibili shows them. |
| `url` | `https://www.bilibili.com/video/<bvid>`. |
| `coverUrl` | Cover image. |
| `durationSec` | Length in seconds. |
| `publishedAt` | When it was published on Bilibili (UTC). |
| `author` | `{ mid, name }` - the channel that published it. |
| `stats` | `{ views, likes, coins, favorites, shares, danmaku, comments }`. A number Bilibili did not return is `null`, never `0`. |
| `tags` | The tags on the video page. |
| `keyword` | The search term it was found under; `null` for a video given by link. |
| `collectedAt` | When we collected the row (UTC). |
| `commentsRequested` | Whether this run asked for comments. |
| `commentsCollected`, `repliesCollected` | How many were written; `null` when comments were not requested. |
| `commentStatus` | `complete`, `partial`, `failed`, `none`, `platform_gap` or `not_requested`. |
| `commentStopReason` | Machine-readable reason the comments ended, for example `limit_reached`, `risk_control`, `end_of_comments`. |
| `partial`, `partialReason` | `true` with a sentence when this video's collection was cut short. |

#### `type: "comment"`

| Field | Meaning |
| --- | --- |
| `commentId` | Bilibili comment ID (`rpid`), as a string: these are 64-bit. |
| `bvid` | The video it belongs to. |
| `level` | `1` for a top-level comment, `2` for a reply. |
| `parentCommentId` | The comment this one answers - a top-level comment or another reply. `null` on a top-level comment. |
| `rootCommentId` | The top-level comment the whole thread hangs off. `null` on a top-level comment. |
| `text` | The comment text. Emoji appear as Bilibili codes such as `[笑哭]`. |
| `likes` | Likes at collection time. |
| `replyCount` | Replies Bilibili counts under it. |
| `publishedAt` | When it was posted (UTC). |
| `author` | `{ mid, name, ipLocation }`. Only what the page prints next to the comment; `ipLocation` is `null` on signed-out reads. |
| `collectedAt` | When we collected the row (UTC). |
| `partial`, `partialReason` | `true` on a comment whose replies were cut short, with the reason. |

#### `type: "keyword_summary"` and `type: "video_summary"`

One `keyword_summary` per keyword: `videosRequested`, `videosCollected`, `resultsSeen`, `duplicatesSkipped`, `searchPagesRead`, `resultsReportedByBilibili`, `collectionStatus` (`complete`, `partial`, `empty` or `failed`), `stopReason`, `statusReason` and `partial` / `partialReason`.

A `video_summary` row appears for every video the run was asked for but wrote no video row for: a duplicate (`skipped`), a video Bilibili no longer serves (`not_found`), or one the request limit or an error cut off (`failed`). A keyword that returns nothing is `empty` with `partial: false`; a keyword Bilibili refused to serve is `failed` with `partial: true` and says so. The two are never mixed up.

#### Blocking is reported, not hidden

Bilibili sometimes answers a comment request with a three-comment preview and "the list is finished", while its own counter says there are tens of thousands. That is Bilibili withholding the list. This Actor recognises it, retries once from a different egress, and if the list is still withheld marks the video `partial` with `commentStopReason: "risk_control"`. It never writes a withheld preview out as "this video has 3 comments". `RUN_SUMMARY` carries `riskControlHits`, the egress the run ended on and every switch it made.

### Pricing

Pay per result: every `video` row and every `comment` row is one charged result. Summary rows are free. Comments are the expensive part of a run - one video with 50 comments is 51 results, not 1 - which is why `includeComments` is off by default.

# Actor input Schema

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

One search term per line. Each keyword is searched on Bilibili and the videos it returns are collected. Leave empty if you only want the videos listed in "Video links".

## `videoUrls` (type: `array`):

One video per line: a video link (bilibili.com/video/BV...), a short link (b23.tv/...), share text copied from the app, a BV ID such as BV1xx411c7mD, or an av number such as av2. These videos are collected before the keywords. A line that is none of these stops the run before anything is collected or charged.

## `maxVideosPerKeyword` (type: `integer`):

Stop each keyword after this many videos. Bilibili's search itself returns at most 1,000 videos per keyword.

## `sort` (type: `string`):

The order Bilibili returns the search results in. Videos given in "Video links" are not affected.

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

Also collect the comments of every video, and the replies under them. Each comment and each reply is one row and is charged like a video row. Off by default, because a video with comments costs many times more than the video on its own.

## `maxCommentsPerVideo` (type: `integer`):

Stop each video after this many comments. Replies count towards the same number, so 50 means 50 comments and replies together. A video stopped here is marked partial.

## `maxUpstreamRequests` (type: `integer`):

Hard ceiling on the HTTP requests this run sends to Bilibili: search pages, video details, comment pages, reply pages and retries together. At the ceiling collection stops - what is being collected is marked partial with stopReason "upstream_request_cap", anything after it is reported as not collected, and the run then ends normally. Leave empty and the Actor works a ceiling out from the rest of your input, so a run is never unbounded.

## Actor input object example

```json
{
  "keywords": [
    "美食"
  ],
  "maxVideosPerKeyword": 20,
  "sort": "relevance",
  "includeComments": false,
  "maxCommentsPerVideo": 50
}
```

# Actor output Schema

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

No description

## `runSummary` (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 = {
    "keywords": [
        "美食"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lanternlab318/bilibili-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 = { "keywords": ["美食"] }

# Run the Actor and wait for it to finish
run = client.actor("lanternlab318/bilibili-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 '{
  "keywords": [
    "美食"
  ]
}' |
apify call lanternlab318/bilibili-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lanternlab318/bilibili-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/sBM8lNbulG48lW79n/builds/XhU7gbcGbFf6J3Xfu/openapi.json
