B站自动监控系统怎么搭?n8n完整配置教程

有一段时间,我每天花将近一小时刷 B 站做选题。

划啊划,看到不错的视频就截图,评论区里有意思的讨论就复制粘贴,扔进备忘录。第二天打开,一堆乱截图,完全没法用——想找某个视频的数据,翻半天找不到,好不容易记起有个爆款,忘了是哪个关键词搜到的。

后来我搭了一套自动化,这件事就变成:每天早上打开飞书多维表格,昨日发布的相关视频按播放量排好,评论也拉下来,10 分钟搞定选题调研。说说这套系统怎么搭的。

这个流程,我用到了n8n、多维表格和tikhub,先说三个工具各是做什么的,毕竟不是每个人都接触过 n8n 和 TikHub。

1.n8n 是一个开源的流程自动化工具,可以理解成”到点自动做一串事”——定时触发、调 API、写数据,不需要人守着。

2.TikHub 是一个三方接口平台,专门给开发者用的第三方数据服务,提供 B 站、抖音等平台的公开数据接口,当然这个是要付费的,因为要去爬虫的话,封号的风险很高,所以用这第三方平台的接口,就安全可靠一些。

我用它按关键词搜视频,获取数据、评论和字幕。

3.飞书多维表格就不多说了,n8n 把筛好的视频推进去,打开表格就能看,不用手动操作。

n8n(调度)+ TikHub(B 站数据)+ 飞书多维表格(存数据)。

每天 22:00 → n8n 触发 → 按关键词搜 B 站 → 只留昨天发布的 → 去掉已记录的 → 新视频写进飞书表格。

跑一遍大概 2-3 分钟,不需要人在场。

配图

二、飞书多维表格怎么建

先把表格建好,n8n 才知道往哪里写数据。我们建一张选题监控表,可以用如下字段:

字段名字段类型说明
标题文本
作者文本
bvid文本视频唯一 ID,去重用
视频url文本
视频时长文本
视频描述文本
关键词文本哪个关键词搜到的
视频播放次数数字
视频收藏次数数字
视频发布时间文本
视频字幕文本
视频评论文本热门评论,自动采集

也就是我们需要把想获取到的B站视频的信息的字段维护到多维表格里,后面把数据同步这里面就好

配图

建好之后,记下多维表格链接里的两个参数,后面 n8n 配置要用到:

  • App Token:URL 里 /base/ 后面那段
  • Table ID:URL 里 table= 后面那段

三、n8n 工作流:节点配置

一共 7 个节点。节点 1-2 负责触发和配置,节点 3 单独向飞书换 Token(避免把认证逻辑塞进主代码节点),节点 4 把 Token 和配置打包传下去,节点 5 是核心——搜索、去重、评论、字幕通过tikhub的接口一气都拿到,节点 6 是条件判断(有新视频才写,没有直接结束,不报错),节点 7 写入飞书。

配图

下面我们详细说说这几个节点的配置。

节点 1:定时触发

类型:Schedule Trigger

类似设个闹钟,到时间自动开跑,不用手动点。

配图

节点 2:配置初始化

类型:Code(JavaScript)

我们直接用代码节点,把关键词、认证信息、日期范围一次准备好,交给后面的节点用。

代码如下:

const config = {
  timezone: 'Asia/Shanghai',

  tikhub: {
    baseUrl: 'https://api.tikhub.io',
    apiKey: 'XXXX',
  },

  feishu: {
    appId: 'XXXX',
    appSecret: 'XXXX',
    bitableAppToken: 'XXXX',
    bitableTableId: 'XXXX',
  },

  keywords: [
    '飞书多维表格',
    'n8n',
    'AI工作流',
    '自动化工作流',
    '多维表格教程',
    'AI Agent',
    '企业数字化',
    '低代码工具',
  ],

  // 每个关键词最多写入播放量 TOP N 的视频
  // 已入库的不占此配额(例如今天 TOP10 里有 3 个已入库,则额外再取后面 3 个新视频)
  topPerKeyword: 10,
};

// 计算「昨天」的北京时间范围(UTC+8)
// 例:6月15日22:00 BJT 运行 → 取 6月14日 00:00:00 ~ 23:59:59 BJT
const BJT_OFFSET_MS = 8 * 60 * 60 * 1000;
const nowBJT_ms = Date.now() + BJT_OFFSET_MS;
const yesterdayBJT = new Date(nowBJT_ms - 24 * 60 * 60 * 1000);
const y = yesterdayBJT.getUTCFullYear();
const mo = yesterdayBJT.getUTCMonth();
const d = yesterdayBJT.getUTCDate();

// 昨天 00:00:00 BJT → UTC Unix 秒
const beginSec = Math.floor((Date.UTC(y, mo, d, 0, 0, 0) - BJT_OFFSET_MS) / 1000);
// 昨天 23:59:59 BJT → UTC Unix 秒
const endSec   = Math.floor((Date.UTC(y, mo, d, 23, 59, 59) - BJT_OFFSET_MS) / 1000);

return [{ json: { config, range: { pubtime_begin_s: beginSec, pubtime_end_s: endSec } } }];

这里面有几个逻辑重点说明一下:

① 关键词

就是要你要让B站去搜的关键词,根据你自己的需求,来维护这里面的关键词

const keywords = [
  '飞书多维表格', 'n8n', 'AI工作流',
  '自动化工作流', '多维表格教程',
  'AI Agent', '企业数字化', '低代码工具'
];

② 认证信息

TikHub API Key(注册后在个人中心拿),飞书 Access Token(用 app_id + app_secret 换取,参考飞书开放平台文档)。

把XXXX替换成自己的key和飞书内容。

  tikhub: {
    baseUrl: 'https://api.tikhub.io',
    apiKey: 'XXXX',
  },

  feishu: {
    appId: 'XXXX',
    appSecret: 'XXXX',
    bitableAppToken: 'XXXX',
    bitableTableId: 'XXXX',
  },

③ 昨日北京时间换算

n8n 服务器通常跑在 UTC,需要手动换算,不做这步数据范围会乱:

const BJT_OFFSET_MS = 8 * 60 * 60 * 1000;
const nowBJT_ms = Date.now() + BJT_OFFSET_MS;
const yesterdayBJT = new Date(nowBJT_ms - 24 * 60 * 60 * 1000);
const y = yesterdayBJT.getUTCFullYear();
const mo = yesterdayBJT.getUTCMonth();
const d = yesterdayBJT.getUTCDate();
// 昨日 00:00:00 北京时间对应的 Unix 时间戳(秒)
const beginSec = Math.floor((Date.UTC(y, mo, d, 0, 0, 0) - BJT_OFFSET_MS) / 1000);
const endSec   = Math.floor((Date.UTC(y, mo, d, 23, 59, 59) - BJT_OFFSET_MS) / 1000);

节点 3/4:获取飞书token和打包配置信息

分别用http节点来获取飞书的token,然后用code节点来把 Token 和配置信息打包

1.获取token,我们用http节点来获取

配图

2.配置信息,用code节点来完成

const token = $json.tenant_access_token;
const init = $('Init (Config + 7d range)').first().json;
if (!init?.config?.tikhub?.apiKey) {
  throw new Error('请在 Init 节点填写 config.tikhub.apiKey(TikHub Bearer Token)');
}
if (!token) {
  throw new Error('飞书 tenant_access_token 为空,请检查 appId/appSecret');
}
return [{ json: { ...init, tenant_access_token: token } }];

节点 5:搜索 + 评论 + 字幕(核心节点)

类型:Code(JavaScript)

这个节点就是核心了,我们用code来去从tikhub接口获取到B站的数据

这段代码做了这几个事:先从飞书拿全部历史 BV 号来去重(之前已经同步过的相同内容的数据就不需要再次同步了),再按关键词搜 B 站,过滤保留新视频,最后逐条拉热门评论和字幕。这是整个流程最重的部分,跑完大概 2-3 分钟。

完整代码如下:

/**
 * 执行逻辑(三步):
 * Step1 — 从飞书多维表格加载全部已有 bvid(分页翻完),建立去重集合
 * Step2 — 对每个关键词搜索视频,每词取前 topPerKeyword 条新视频
 *          · 跨关键词 bvid 去重(本轮内)
 *          · 已入库 bvid 去重(对比 Step1 集合)
 *          · 已跳过的不占 topPerKeyword 配额,会继续往后取
 * Step3 — 拉取热门评论 + 字幕(全部视频均拉,内容截取 8000 字符)
 *          字幕流程:先调 B站公开 pagelist API 拿 cid,再调 TikHub 字幕接口
 */

function stripHtml(s) {
  return String(s || '').replace(/<[^>]+>/g, '');
}

function getRepliesFromCommentResp(resp) {
  const r = resp?.data?.data?.replies;
  return Array.isArray(r) ? r : [];
}

function commentMessage(reply) {
  const c = reply?.content;
  if (c == null) return '';
  if (typeof c === 'string') return c;
  return c.message || c.text || '';
}

function buildNumberedComments(replies) {
  const lines = [];
  for (const reply of replies) {
    let t = commentMessage(reply).trim().replace(/\r?\n/g, ' ').replace(/\s+/g, ' ');
    if (!t) continue;
    lines.push(`${lines.length + 1}.${t}`);
  }
  return lines.join('\n');
}

function buildQuery(params) {
  const parts = [];
  for (const key in params) {
    if (!Object.prototype.hasOwnProperty.call(params, key)) continue;
    const value = params[key];
    if (value === undefined || value === null || value === '') continue;
    parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(String(value)));
  }
  return parts.join('&');
}

// 飞书多维表格文本字段可能是字符串,也可能是 [{type,text}] 数组,统一处理
function extractText(fieldValue) {
  if (!fieldValue) return '';
  if (typeof fieldValue === 'string') return fieldValue;
  if (Array.isArray(fieldValue)) return fieldValue.map(seg => seg.text || '').join('');
  return String(fieldValue);
}

// 从 TikHub 字幕接口返回中提取纯文本
// 尝试常见格式:body 数组、嵌套 data、字符串 JSON
function extractSubtitleText(respData) {
  if (!respData) return '';
  // 常见格式1:直接是 body 数组({ body: [{content, from, to}, ...] })
  if (respData.body && Array.isArray(respData.body)) {
    return respData.body
      .map(item => (item.content || item.text || '').trim())
      .filter(Boolean)
      .join(' ')
      .slice(0, 8000);
  }
  // 常见格式2:嵌套 data
  if (respData.data) return extractSubtitleText(respData.data);
  // 常见格式3:顶层就是数组
  if (Array.isArray(respData)) {
    return respData
      .map(item => (item.content || item.text || '').trim())
      .filter(Boolean)
      .join(' ')
      .slice(0, 8000);
  }
  // 常见格式4:字符串(可能是 JSON 字符串)
  if (typeof respData === 'string') {
    try { return extractSubtitleText(JSON.parse(respData)); } catch { return respData.slice(0, 8000); }
  }
  return '';
}

const input = $input.first().json;
const cfg = input.config;
const range = input.range;
const token = input.tenant_access_token;
const apiKey = cfg.tikhub.apiKey;
const base = (cfg.tikhub.baseUrl || 'https://api.tikhub.io').replace(/\/$/, '');
const tikhubHeaders = { Authorization: `Bearer ${apiKey}` };
const feishuHeaders = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
const topPerKeyword = Number(cfg.topPerKeyword) || 10;

// ── Step 1: 加载飞书多维表格中全部已有 bvid ──────────────────────────
const existingBvids = new Set();
let pageToken = undefined;
do {
  const qs = { page_size: '100' };
  if (pageToken) qs.page_token = pageToken;
  let resp;
  try {
    resp = await this.helpers.httpRequest({
      method: 'GET',
      url: `https://open.feishu.cn/open-apis/bitable/v1/apps/${cfg.feishu.bitableAppToken}/tables/${cfg.feishu.bitableTableId}/records`,
      headers: feishuHeaders,
      qs,
      json: true,
      timeout: 30000,
    });
  } catch (e) {
    // 获取失败时中止分页,已收集的继续用;宁可少量重复也不丢新数据
    break;
  }
  for (const item of (resp?.data?.items || [])) {
    const bvid = extractText(item?.fields?.bvid).trim();
    if (bvid) existingBvids.add(bvid);
  }
  pageToken = resp?.data?.page_token || undefined;
} while (pageToken);

// ── Step 2: 按关键词搜索,每词取 topPerKeyword 条真正新视频 ──────────
const byBvid = new Map(); // 本轮跨关键词去重

for (const keyword of cfg.keywords || []) {
  const kw = String(keyword || '').trim();
  if (!kw) continue;

  const qs = buildQuery({
    keyword: kw,
    order: 'click',
    page: '1',
    page_size: '20', // 搜多一些,过滤已入库后剩下的才计入配额
    duration: '0',
    pubtime_begin_s: String(range.pubtime_begin_s),
    pubtime_end_s: String(range.pubtime_end_s),
  });

  const url = `${base}/api/v1/bilibili/web/fetch_general_search?${qs}`;
  let searchResp;
  try {
    searchResp = await this.helpers.httpRequest({
      method: 'GET', url, headers: tikhubHeaders, json: true, timeout: 120000,
    });
  } catch (e) {
    continue;
  }

  const list = searchResp?.data?.data?.result || [];
  let addedThisKeyword = 0;

  for (const it of list) {
    if (!it || it.type !== 'video' || !it.bvid) continue;
    if (byBvid.has(it.bvid)) continue;        // 本轮跨关键词去重
    if (existingBvids.has(it.bvid)) continue; // 已入库去重(不占配额)
    if (addedThisKeyword >= topPerKeyword) break;

    byBvid.set(it.bvid, {
      search_keyword: kw,
      author: it.author || '',
      bvid: it.bvid,
      aid: it.aid || it.id || '',  // 字幕接口 a_id 参数需要
      arcurl: it.arcurl || `https://www.bilibili.com/video/${it.bvid}`,
      duration: it.duration != null ? String(it.duration) : '',
      title: stripHtml(it.title || ''),
      description: String(it.description || ''),
      tag: String(it.tag || ''),
      pubdate: typeof it.pubdate === 'number' ? new Date(it.pubdate * 1000).toISOString() : '',
      play: it.play != null ? Number(it.play) : 0,
      favorites: it.favorites != null ? Number(it.favorites) : 0,
    });
    addedThisKeyword++;
  }
}

// ── Step 3: 拉热门评论 + 字幕,组装 records ──
const videos = Array.from(byBvid.values());
const records = [];

for (const v of videos) {

  // --- 3a: 热门评论 ---
  const qsC = buildQuery({ bv_id: v.bvid, mode: '3', next_offset: '1' });
  const cUrl = `${base}/api/v1/bilibili/app/fetch_video_comments?${qsC}`;
  let cResp;
  try {
    cResp = await this.helpers.httpRequest({
      method: 'GET', url: cUrl, headers: tikhubHeaders, json: true, timeout: 120000,
    });
  } catch (e) {
    cResp = null;
  }
  const replies = cResp ? getRepliesFromCommentResp(cResp) : [];
  let hot_comments = buildNumberedComments(replies);
  if (!hot_comments || !hot_comments.trim()) hot_comments = '暂无评论';

  // --- 3b: 字幕(所有视频均获取,内容截取 8000 字符)---
  let subtitle_text = '';
  if (v.aid) {
    try {
      // 第一步:调 B站公开 pagelist API 取 cid(不消耗 TikHub 额度)
      const pagelistResp = await this.helpers.httpRequest({
        method: 'GET',
        url: 'https://api.bilibili.com/x/player/pagelist',
        qs: { bvid: v.bvid },
        json: true,
        timeout: 10000,
      });
      const cid = pagelistResp?.data?.[0]?.cid;

      if (cid) {
        // 第二步:调 TikHub 字幕接口
        const subtitleResp = await this.helpers.httpRequest({
          method: 'GET',
          url: `${base}/api/v1/bilibili/web/fetch_video_subtitle`,
          headers: tikhubHeaders,
          qs: { a_id: String(v.aid), c_id: String(cid) },
          json: true,
          timeout: 30000,
        });
        subtitle_text = extractSubtitleText(subtitleResp?.data) || '';
      }
    } catch (e) {
      // 字幕获取失败不影响主流程,留空即可
      subtitle_text = '';
    }
  }

  records.push({
    fields: {
      视频标题: v.title,
      作者: v.author,
      bvid: v.bvid,
      视频url: v.arcurl,
      视频时长: v.duration,
      视频描述: v.description,
      视频标签: v.tag,
      视频发布时间: v.pubdate,
      视频播放次数: v.play,
      视频收藏次数: v.favorites,
      搜索关键词: v.search_keyword,
      视频评论: hot_comments,
      视频字幕: subtitle_text,
    },
  });
}

return [{
  json: {
    feishu: {
      bitableAppToken: cfg.feishu.bitableAppToken,
      bitableTableId: cfg.feishu.bitableTableId,
      tenant_access_token: token,
    },
    records,
    stats: {
      newVideos: records.length,
      existingInTable: existingBvids.size,
    },
  },
}];

说几个关键点:

Step 1:读取飞书历史 BV 号

飞书记录接口每页最多 100 条,超过就翻页(page_token):

const existingBvids = new Set();
let pageToken = undefined;
do {
  const qs = { page_size: '100' };
  if (pageToken) qs.page_token = pageToken;
  const resp = await this.helpers.httpRequest({
    method: 'GET',
    url: `https://open.feishu.cn/open-apis/bitable/v1/apps/${APP_TOKEN}/tables/${TABLE_ID}/records`,
    headers: { Authorization: `Bearer ${ACCESS_TOKEN}` },
    qs, json: true
  });
  for (const item of (resp?.data?.items || [])) {
    const bvid = item?.fields?.bvid;
    if (bvid) existingBvids.add(typeof bvid === 'string' ? bvid : bvid[0]?.text);
  }
  pageToken = resp?.data?.page_token;
} while (pageToken);

Step 2:按关键词搜 B 站视频

const resp = await this.helpers.httpRequest({
  method: 'GET',
  url: 'https://api.tikhub.io/api/v1/bilibili/web/search_by_keyword',
  headers: { Authorization: `Bearer ${TIKHUB_KEY}` },
  qs: { keyword, page: 1, page_size: 20 },
  json: true
});
// 过滤 pubdate 在 [beginSec, endSec] 内的,按 play 降序

Step 3:两层去重,每关键词取前 10

const byBvid = new Map();
let addedThisKeyword = 0;
for (const video of sortedVideos) {
  if (byBvid.has(video.bvid)) continue;        // 当次运行内去重
  if (existingBvids.has(video.bvid)) continue;  // 历史去重
  if (addedThisKeyword >= 10) break;
  byBvid.set(video.bvid, video);
  addedThisKeyword++;
}
配图

Step 4:拉评论和字幕

新视频挑完,对每条视频做两件事:调 TikHub 评论接口拉热门评论,再调字幕接口拿字幕。

字幕要分两步走——搜索结果里带 aid(视频 ID),但 TikHub 字幕接口同时要 cid(视频分 P 号)。cid 用 B 站自己的公开接口拿,不消耗 TikHub 额度:

// Step 1:B站公开 pagelist 接口拿 cid(免费)
const pagelistResp = await this.helpers.httpRequest({
  method: 'GET',
  url: 'https://api.bilibili.com/x/player/pagelist',
  qs: { bvid: v.bvid },
  json: true,
  timeout: 10000,
});
const cid = pagelistResp?.data?.[0]?.cid;

// Step 2:TikHub 字幕接口
if (cid) {
  const subtitleResp = await this.helpers.httpRequest({
    method: 'GET',
    url: `${base}/api/v1/bilibili/web/fetch_video_subtitle`,
    headers: tikhubHeaders,
    qs: { a_id: String(v.aid), c_id: String(cid) },
    json: true,
    timeout: 30000,
  });
  subtitle_text = extractSubtitleText(subtitleResp?.data) || '';
}

我们这里加了一个限制,写入多维表格的字幕文本不能超过8000字符,因为有的视频太长了,文字太多传到多维表格也会有问题。

所以拿到的字幕文本截到 8000 字符写进飞书,字幕拉取失败(比如视频没有字幕)不影响整条流程,该条置空就行。

节点 7:写入飞书多维表格

类型:HTTP Request

把上一步筛好的视频推进飞书表格。

配图

①Method:POST

②URL

https://open.feishu.cn/open-apis/bitable/v1/apps/{{ $json.feishu.bitableAppToken }}/tables/{{ $json.feishu.bitableTableId }}/records/batch_create

③Header

Authorization:Bearer {{ \$json.feishu.tenant_access_token }}

Content-Type:application/json; charset=utf-8

④Body 里写好字段映射,把标题、播放量、评论等对应表格字段名。

{{ JSON.stringify({ records: $json.records }) }}

以上就是这条流程的详细配置步骤了

四、关键词怎么选

关键词决定你能监控到什么,选得太宽引来大量无关内容,选得太窄会漏掉机会。我根据我自己的内容定位选了 8 个:

  • 工具类:飞书多维表格、多维表格教程、n8n、低代码工具
  • 方法论类:AI 工作流、自动化工作流、AI Agent
  • 受众触点类:企业数字化

每个关键词最多取播放量前 10 名,加起来最多 80 条候选视频。经过日期过滤和去重,每天实际新增 20-40 条。绝对是够用了。

你可以根据自己的实际业务需求来修改这些关键词,这些关键词也可以随时在配置节点里调整。

五、为什么是 n8n,不直接用 Claude code或 Codex?

有朋友会问,为什么我不能直接用claude或codex来做这个事情?

如果只做一次,让 Claude code直接来完全可以,他可以把这些事情都给你做了。但n8n的好处在于它每天可以自动跑、并且结果沉淀在固定地方。

而且n8n比较直观,整个过程运行到哪个节点,历史的运行日志,这些都很清晰,配置一次,后面就自动跑就行。

Claude code或 Codex也很强大,但是它有一定的学习成本,并且每次都会消耗token。

当然最后我们同步到多维表格的数据,是可以直接给到Claude code或 Codex来做分析的。

Claude 适合分析数据,n8n 适合采集和搬运。两者是分工,不是替代。

根据自己的需求来选择即可

六、我的日常是这么用的

现在每天晚上 22:00 自动触发这个n8n流程,第二天早上打开飞书,昨天发的相关视频已经按播放量排好了。扫一遍,10 分钟内找到 2-3 个值得深入的方向,选题调研就算结束。之前靠的是”今天碰到了什么”,现在所有相关内容都在表格里,只负责挑。

如果想更深入分析的话,我就直接把这个表格丢给Claude code,让它根据表格的内容、我账号的定位、我历史的文章,来给我生成可以参考的选题内容。

当然现在这套只是数据采集,如果想做的更深入的话,可以继续:

1.每日 AI 洞察:每天让 AI 看完监控数据,直接输出 3 个值得写的方向,不用自己逐条判断。

2.一键生成初稿:选好题后,结合历史文章风格,AI 出稿,自己来润色。

…..


📋 这套B站配置相关模板,我整理成模板了

跟着上面步骤搭要花点时间。想直接拿现成的改,加我微信备注「B站配置」,我发你。

🤔 卡在某一步了?

搭建过程里如果哪一步卡住了,如果你照着做没跑通,或者你们公司情况不太一样,可以找我聊聊,我免费帮你看看怎么调。不合适我也会直说。

加吨师傅微信咨询飞书多维表格搭建

扫码加我,备注「B站配置」领模板

想看更多这类方案,我都整理在 吨师傅工具箱 里了:客户管理、进销存、工单报修……按场景分好类,可以直接抄。

相关阅读

滚动至顶部