# Bilibili Scraper (video search, stats, tags, monitor) (`datahamster/bilibili-videos`) Actor

Bilibili scraper for video search: one flat row per video with views, danmaku, likes, favorites, replies, duration, tags, uploader and publish date. Sort by newest, views or danmaku, or look up a bvid. Monitor mode alerts on new uploads and view changes. No login, no cookies.

- **URL**: https://apify.com/datahamster/bilibili-videos.md
- **Developed by:** [Viktor Dubnytskiy](https://apify.com/datahamster) (community)
- **Categories:** Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.60 / 1,000 result items

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Scraper (video search, stats, tags, monitor)

Search bilibili (B站) by keyword — Chinese or Latin — and get one flat row per video: views, danmaku, likes, favorites, replies, duration, tags, uploader, publish date and thumbnail. No login, no cookies, no session to keep alive. Monitor mode turns the same input into an alert on new uploads and moving view counts.

### What you get

One row per video (`id` = the bvid, so the same video found by two keywords is a single row):

`id`, `bvid`, `aid`, `url`, `title`, `description`, `uploaderName`, `uploaderMid`, `publishedAt`, `durationSec`, `views`, `danmaku`, `likes`, `favorites`, `replies`, `coins`, `shares`, `thumbnail`, `tags`, `category`, `cid`, `partCount`, `width`, `height`, `detailsFetched`, `query`, `page`, `rank`, `source`, `scrapedAt`.

### Example output

Real rows from the example dataset.

Search row (`searches: ["python"]`, rank 1):

| Field | Value |
|---|---|
| `bvid` / `aid` | `BV1rpWjevEip` / `113006243481679` |
| `title` | `【全748集】目前B站最全最细的Python零基础全套教程，2026最新版…` |
| `uploaderName` / `uploaderMid` | `Python官方课程` / `3546597933714079` |
| `views` / `danmaku` | `19785247` / `129144` |
| `likes` / `favorites` / `replies` | `537284` / `828141` / `346364` |
| `durationSec` / `publishedAt` | `143894` / `2024-08-22T14:59:18Z` |
| `category` | `计算机技术` |
| `tags` | `["程序员", "Python教程", "Python入门零基础", "计算机", …]` |
| `url` | `https://www.bilibili.com/video/BV1rpWjevEip` |

A row fetched with `fetchDetails: true` replaces `tags` with bilibili's official tag list and `durationSec` with the exact second count from the player, and adds `cid`, `partCount`, `width` and `height`.

### Use cases

- **Trend and topic research.** Pull 60 videos for a keyword with views, danmaku and likes and see what a topic actually looks like on bilibili, sorted by newest or by play count.
- **Competitive tracking.** Save the actor as a task in `monitor` mode on your keywords; rows come back only when a new video appears or the counters move, with a webhook or Telegram message.
- **Content sourcing.** `sort: pubdate` gives the freshest uploads for a keyword — a feed of what was published in the last hours.

### Input

| Field | Meaning | Default |
|---|---|---|
| `searches` | Keywords to search, Chinese or Latin, one per line | `["python"]` |
| `bvids` | Known bvids or video URLs to look up directly | empty |
| `sort` | `default` (relevance), `pubdate` (newest), `views`, `danmaku` | `default` |
| `maxPagesPerSearch` | Result pages per keyword, 20 videos each (1-50) | `3` |
| `fetchDetails` | Official tag list, exact duration, parts and resolution (2 extra requests per video) | `false` |
| `strictQueryMatch` | Drop the unrelated videos bilibili pads an unmatchable keyword with | `true` |
| `maxItems` | Stop after this many rows | `100` |
| `mode` | `scrape`, or `monitor` for only new/changed rows | `scrape` |
| `monitorStateId`, `webhookUrl`, `telegramBotToken`, `telegramChatId` | Monitor-mode state key and alert targets | empty |

### Pricing

| Event | Price |
|---|---|
| result | $0.002 per video ($2 per 1,000) |
| monitor-check | $0.005 per monitor run |
| change | $0.002 per new/changed video |

Charged only for rows actually pushed. A run that finds nothing charges no result events. No proxy is needed for the search path, so there is no proxy traffic to pay for on top.

### Why this actor

- **An order of magnitude cheaper** than the other bilibili scrapers on the Store, at $0.002 per video.
- Search rows are already complete: views, danmaku, likes, favorites, replies, duration, tags and uploader all come from the search response itself, so 60 videos is 3 requests, not 63.
- Server-side sorting by newest, views or danmaku — not a re-sort of whatever the first page happened to contain.
- Built-in monitor mode with webhook and Telegram alerts; the change fingerprint is `views`, `likes` and `replies`.
- A refused run is reported as a block, never as an empty dataset (see the run summary's `emptyReason`).
- A rate limit or a network failure that hits before any row is produced ends the run under its own reason (`rate_limited`, `upstream_error`) instead of handing you a silent empty dataset.

### Limits (read these)

- **`coins` and `shares` are always null.** The only bilibili endpoint that carries them (`x/web-interface/view`) answers with a 412 risk-control page on the tiers this actor was surveyed from — no proxy and datacenter. The residential pool was unreachable at the time, so residential is untested rather than cleared; if it ever answers, these two fields become reachable. Until then the actor does not pretend to have them. Every other counter — views, danmaku, likes, favorites, replies — comes from the search response and is present on every row.
- **No uploader listing.** Every "videos of this uploader" endpoint bilibili has (`x/space/arc/search` and its signed variant, the space HTML page, the dynamic feed) is behind the same risk-control wall on those two tiers; a correctly signed request gets through the wall only to be refused with `-352`. `uploaderName` and `uploaderMid` are on every row, so you can group by uploader after the fact, but you cannot ask for one uploader's catalogue.
- **bilibili never returns an empty search page.** A keyword it cannot match still answers with 20 unrelated videos and `numResults: 1000`, because it matches fragments of your keyword. With `strictQueryMatch` on (the default) rows in which no whole word of the keyword appears are dropped, the keyword stops paging, and you are charged nothing. Set it to `false` to see exactly what bilibili returned.
- bilibili mixes one or two promo cards into every result page. They carry no bvid and are never pushed or charged, so `rank` skips their positions and a 3-page sweep usually yields a little under 60 rows.
- `bvids` rows recover their counters by feeding the video's own title back into the search index, because the direct lookup endpoint is walled. When a video does not surface for its own title the row is still returned, with the stats null.
- Deep paging is capped by bilibili at 50 pages per keyword, and a keyword stops early once bilibili serves a short page or reaches the page count it reported, so a large `maxPagesPerSearch` costs nothing extra on a thin keyword.
- Risk control refuses in two shapes: a 412 captcha page, and a 200 response whose JSON body carries a refusal code (`-352`, `-412`, `-403`, `-401`). Both are treated as the same wall; `code: 0` with no results stays an honest empty answer.
- No comments, no danmaku text, no viewer or commenter identities — only the video's own counters and its uploader's public channel name and id.

### FAQ

**Do I need a bilibili account or cookies?** No. There is no account or cookie field; every request is made as a logged-out visitor.

**Why is `coins` null?** The endpoint that serves coins and shares is behind bilibili's risk-control wall on both tiers the actor was surveyed from (no proxy and datacenter); residential could not be reached to test. The field is kept in the schema so nothing downstream breaks, but it is never populated. Views, danmaku, likes, favorites and replies are always there.

**My keyword returned nothing — is the actor broken?** Probably not. bilibili pads an unmatchable keyword with unrelated videos instead of returning an empty page, and the actor drops them rather than billing you for 20 junk rows. Check the spelling, or set `strictQueryMatch: false` to see the raw answer. The `RUN_SUMMARY` record in the run's key-value store carries `emptyReason`, which separates "nothing matched" from "the source refused us".

**How do I track a topic?** Save the actor as a task with `mode: monitor` and your keywords, schedule it, and set `webhookUrl` or the Telegram fields. Rows come back only when a new video appears or `views`, `likes` or `replies` changed.

**What does `fetchDetails` add?** bilibili's official tag list for the video, the exact duration in seconds from the player (the search string is rounded to `mm:ss`), the part count, the `cid` and the video resolution. It costs two extra requests per video.

### Changelog

- 0.1: initial release — keyword search with server-side sort and paging, bvid lookup, official tags and exact duration, unrelated-result filtering, monitor mode.

***

If this actor saved you time, a short review on its Store page genuinely helps other people find it. Found a bug or need a field that is missing? Open a ticket on the **Issues** tab.

# Actor input Schema

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

Stop after this many results (you are charged only for pushed items)

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

scrape = full results; monitor = only new/changed items since the previous run of this task

## `monitorStateId` (type: `string`):

Optional state id when not running as a saved task (monitor mode)

## `webhookUrl` (type: `string`):

POST a change summary here in monitor mode

## `telegramBotToken` (type: `string`):

Optional: bot token for monitor-mode change summaries

## `telegramChatId` (type: `string`):

Optional: chat id that receives monitor-mode summaries

## `searches` (type: `array`):

Keywords to search bilibili videos for, one per line. Chinese and Latin both work, e.g. "python", "原神", "机器学习". Each keyword is paged separately.

## `bvids` (type: `array`):

Known videos to look up directly, as bvids or full URLs, e.g. "BV1GJ411x7h7" or "https://www.bilibili.com/video/BV1GJ411x7h7". Can be combined with Search keywords or used alone. Old "av" ids are not supported.

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

Ordering bilibili applies to each keyword before paging. "Newest first" is the one to use for a feed of fresh uploads. Example: pubdate.

## `maxPagesPerSearch` (type: `integer`):

How many result pages to read per keyword, 20 videos each, e.g. 3 for about 60 videos. bilibili caps deep paging at 50. Range 1-50.

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

true = open two extra endpoints per video for bilibili's official tag list, the exact duration in seconds, the part count and the resolution. false = use the counters and tag string the search already returned. Example: false.

## `strictQueryMatch` (type: `boolean`):

true = drop videos in which no whole word of your keyword appears. bilibili answers an unmatchable keyword with 20 unrelated videos rather than an empty page, and this keeps you from paying for them. Example: true.

## Actor input object example

```json
{
  "maxItems": 100,
  "mode": "scrape",
  "searches": [
    "python"
  ],
  "sort": "default",
  "maxPagesPerSearch": 3,
  "fetchDetails": false,
  "strictQueryMatch": true
}
```

# Actor output Schema

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

All pushed rows (dataset, JSON)

## `resultsTable` (type: `string`):

Dataset in the Console viewer

## `runSummary` (type: `string`):

RUN\_SUMMARY record

# 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 = {
    "searches": [
        "python"
    ],
    "maxPagesPerSearch": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("datahamster/bilibili-videos").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 = {
    "searches": ["python"],
    "maxPagesPerSearch": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("datahamster/bilibili-videos").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 '{
  "searches": [
    "python"
  ],
  "maxPagesPerSearch": 3
}' |
apify call datahamster/bilibili-videos --silent --output-dataset

```

## MCP server setup

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

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/Op46QaJXwgUgPNcm2/builds/RQ1QE7xQIcZcbvDBG/openapi.json
