n8n+TikHub每周监控B站关键词视频和热门评论

在前面几篇里,我们已经搭过 YouTube、Reddit 等「选题情报」自动化流程(系列文章如下)。

【链接】

对应《一图说透「内容创作全流程」》里的「挖选题」:每多一条稳定渠道,就多一个可持续观察的信息入口。

本篇是系列第四弹,对象换成 B 站(bilibili):按关键词抓取近一周视频,拉取热门评论,再写入飞书多维表格。

配图

其实这条工作流的方式和之前几期工作流逻辑都是类似,用n8n流程,对接tikhub的接口,获取到对应的数据写到多维表格里,如果你之前手把手搭建过,这期对你来说就很简单了。没有的话也没关系,这期我们来讲讲这个工作流实现的逻辑。

在开始之前,我们先说说为什么把B站也接进我们的选题池。

①中文语境强:因为很多工具教程或内容、比如飞书 / n8n / AI 工作流类内容,在 B 站更新密度更高,而且内容的方向和匹配中文用户。

②评论即需求:热门评论里常有「怎么用」「踩坑」「求配置」——和 Reddit 一样,都是真实声音,并且更贴近中文用户。

③和前几弹互补:YouTube、Reddit 偏全球或英文语境;B 站补中文场景。数据都进多维表格后,仪表盘上可以多一块「B 站」视图;若你读过「仪表盘 + 龙虾」那篇,后续接龙虾做周报也顺。

一、这个工作流的整体架构

先说说工作流的整体的框架。

我们可以把它理解成一条按周运行的流水线:

配图

1.定时触发:例如每周固定时间跑一轮。

2.Init 配置:统一把 TikHub Key、飞书应用信息、多维表格 app_token / table_id、监控关键词列表、以及「最多处理多少条视频」上限的数据,都放在这个节点来进行配置。

3.时间计算:因为我们想获取最近一周发布的视频,所以需要做一个时间的计算。我们把本次流程运行的时间作为结束时间,然后往前推7天作为开始时间,交给搜索接口做「近一周」过滤。

4.飞书鉴权:拿 tenant_access_token,给后面写表用。

5.核心 Code 节点(TikHub Search + Comments):在一个节点里串完,用代码的方式完成多关键词搜索 → 按 视频BV号去重 → 逐条调评论接口 → 拼成飞书 batch_create 需要的 records。

6.IF:有记录才写表,无记录就不需要写

7.HTTP 节点写多维表格:一次性将获取到的数据写入到多维表格中。

二、多维表格如何配置

我们可以在之前的多维表格中,新建一张数据表,和之前的reddit、youtube表一样。 这次新建的数据表就只放我们从B站获取的视频数据。

配图

字段可以放以下几个:

字段 字段类型
视频标题 文本
作者 文本
bvid 文本
视频url 文本
视频时长 文本
视频描述 文本
视频标签 文本
视频发布时间 文本
视频播放次数 数字
视频收藏次数 数字
搜索关键词 文本(本条视频是由哪个关键词搜到的)
视频评论 文本:多条评论合并为 1.xxx、2.xxx;若没有评论则固定为 暂无评论

三、流程节点配置说明

这一章节详细说明每个节点的配置和作用

配图

1、Schedule Trigger

定时触发节点,每周触发一次流程,如果你希望按需调整触发的频率或时间,直接在这个节点修改就好

2、Init (Config + 7d range)(Code)

代码节点

节点作用:

①将所有的配置信息,如TikHub apiKey、feishu.appId / appSecret、多维表格 bitableAppToken、bitableTableId、keywords 数组、maxVideos等需要配置的信息都放在这个节点里,方便随时修改。

②时间计算,计算 range.pubtime_end_s(当前秒)和 range.pubtime_begin_s(减 7 天)。

详细配置:

/**
 * Bilibili 关键词(近 7 天)+ 热门评论 → 飞书多维表格
 *
 * 请务必填写:config.tikhub.apiKey、config.feishu.appId / appSecret
 * 已在对话中暴露的密钥请尽快在 TikHub 轮换。
 *
 * 多维表格字段名需与本流程「fields」一致,或在下方 mergeFields 中改名:
 * search_keyword, author, bvid, arcurl, duration, title, description, tag, pubdate, play, favorites, hot_comments
 */

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

  tikhub: {
    baseUrl: 'https://api.tikhub.io',
    apiKey: 'j6ohNv49g3eUw3KhEZzMkEyILSeOd8RAvxbltW6Rdf/jjEy8AH2IKjnELw==',
  },

  feishu: {
    appId: 'cli_a6e243e0f9d9100c',
    appSecret: '3GwtYgK5jOGmVA6yGyeR7gFoeBVCu3bg',
    bitableAppToken: 'RTnbbVoapaEDOEsaseCcVWxDnyg',
    bitableTableId: 'tbl49sp2cdLxpiN5',
  },

  // 与你「内容创作 / 自动化 / 飞书 / n8n 选题」定位相近的检索词,可按需增删
  keywords: [
    'n8n工作流',
    '飞书 多维表格',
    'AI工作流',
    '内容运营自动化',
    '内容运营 选题',
    'AI agent',
  ],

  // 综合搜索:order=click,每词 1 页 20 条;去重后最多拉取评论的视频数(防超时)
  maxVideos: 40,
};

const nowSec = Math.floor(Date.now() / 1000);
const beginSec = nowSec - 7 * 24 * 60 * 60;

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

3、Feishu – tenant_access_token(HTTP Request)

获取飞书的tenant_access_token,给后面写表用。

详细配置:

①Method:POST

②URL:https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/

③Headers:name:Content-Type,value:application/json; charset=utf-8

④Body(JSON):app_id、app_secret 。用表达式从 \$json.config.feishu 读取。

{{ JSON.stringify({ app_id: $json.config.feishu.appId, app_secret: $json.config.feishu.appSecret }) }}
配图

4、Pack (config + token)(Code)

code节点,这个节点的主要目的是把 Init 里的 config + range 和 上一步拿到的 token 合并成一条下游数据,供下一个核心的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.TikHub Search + Comments(code)

这个节点是流程中最核心的节点,目的主要是调用tikhub里B站的搜索接口和获取评论接口,来获取到我们所需要的数据。

之前的流程我们会用http节点以及循环的逻辑来处理,节点看起来就比较多。这次我们可以换一种思路,直接用代码节点一次性搞定(不懂代码没关系,用AI啊哈哈)

我先讲讲这个节点的逻辑,最后把代码贴出来,直接复制就行。我们不用懂详细的代码,只需要大概清楚它的逻辑和在做什么事情就好,下面的介绍我也按照这个思路来。

(1)节点逻辑

主要处理三个事情

①搜:对每个关键词,调 TikHub「B 站综合搜索」接口,限定近 7 天、按播放量排序、只要第一页(例如 20 条),减轻费用和超时风险。

②收:把所有关键词的结果汇总,按 bvid 去重(同一个视频被多个词搜到只保留第一次),并限制总视频数不超过 maxVideos。

③评 + 写:对每个保留下的视频再调「视频评论」接口(热门排序、第一页),把评论正文抽出来,编成编号列表,和标题、链接等一起塞进 records[].fields,交给飞书写入表格。

(2)节点里的几个处理

有几个函数简单说说

①getRepliesFromCommentResp:TikHub 返回体是「套娃」结构:评论列表一般在 data.data.replies 这一层。这个函数专门从复杂 JSON 里捞出数组,拿不到就返回空数组,后面逻辑不用写一堆 if。

②commentMessage:单条评论里,正文可能在 content.message,也可能个别场景是字符串。这里做兼容,统一成一段字符串,避免某条评论结构略变就整段崩掉。

③buildNumberedComments:把多条评论合成一段多行文本,比如1.第一条评论……2.第二条评论……

(3)代码逻辑里的第一段:关键词循环 + 搜索 + 去重

①从上游读出 config、range、tenant_access_token。

②用 Map(键为 bvid)做去重:同一个视频只保留第一次出现时绑定的「搜索关键词」,方便我们反查「是哪个词带来的」。

③每个关键词拼好 fetch_general_search 的 query:

  • order = click(最多播放)
  • page = 1、page_size = 20、duration = 0(不限时长)
  • pubtime_begin_s / pubtime_end_s 用 Init 算好的一周窗口

④用 this.helpers.httpRequest 发 GET(带 Authorization: Bearer)。某个词请求失败就 continue,不拖死整次执行。

⑤从返回里取 data.data.result,只保留 type === ‘video’ 且有 bvid 的项,写入 Map;达到 maxVideos 就提前结束外层的词循环。

(4)代码逻辑里的第二段:逐视频拉评论 + 组装 fields

①把 Map 转成数组,逐个视频请求 fetch_video_comments:mode=3 热门,next_offset=1 先取第一页(可按需以后扩展翻页)。

②评论列表走 buildNumberedComments;若结果为空或只有空白,把「视频评论」字段设为「暂无评论」——这样表里不会出现空单元格含义不清的情况。

③fields 里的键名用中文,与多维表格列名一字不差,包括 视频url、视频评论 等,避免飞书报「字段不存在」。

④播放、收藏若在接口里缺失,用 0 填入数字列,减少「数字字段为空」的写入错误。

(5)代码逻辑里的最后: return

返回一个 item,json 里包含:

①feishu:bitableAppToken、bitableTableId、tenant_access_token(给下一个 HTTP 节点用)

②records:飞书 batch_create 要的数组

③stats.videoCount:方便你在 n8n 里一眼看到本次写入条数

整段代码如下:

/**
 * 对每个关键词调用综合搜索(近 7 天、最多播放排序)→ 去重 BV → 拉取热门评论 → 组装飞书记录
 * 多维表格字段名与表头一致(中文)
 */

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('&');
}

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 headers = { Authorization: `Bearer ${apiKey}` };
const maxVideos = Number(cfg.maxVideos) || 40;

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,
      json: true,
      timeout: 120000,
    });
  } catch (e) {
    continue;
  }

  const list = searchResp?.data?.data?.result || [];
  for (const it of list) {
    if (!it || it.type !== 'video' || !it.bvid) continue;
    if (byBvid.has(it.bvid)) continue;
    byBvid.set(it.bvid, {
      search_keyword: kw,
      author: it.author || '',
      bvid: it.bvid,
      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,
      raw: it,
    });
    if (byBvid.size >= maxVideos) break;
  }
  if (byBvid.size >= maxVideos) break;
}

const videos = Array.from(byBvid.values());
const records = [];

for (const v of videos) {
  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,
      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 = '暂无评论';
  }

  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,
    },
  });
}

return [
  {
    json: {
      feishu: {
        bitableAppToken: cfg.feishu.bitableAppToken,
        bitableTableId: cfg.feishu.bitableTableId,
        tenant_access_token: token,
      },
      records,
      stats: { videoCount: records.length },
    },
  },
];

执行后,每条视频记录就按标准的数据结构输出出来了,我们可以直接把这些内容插入到多维表格里即可

配图

6、IF records > 0

IF节点,主要作用是如果接口可以获取到数据,那么才会调用多维表格的接口进行写入,如果没有,就走 No Operation,减少无意义请求。

配图

7、Feishu – batch_create records(HTTP Request)

http节点,这个节点的作用主要是调用飞书多维表格的写入节点,把我们获取到的B站数据一次性写入到数据表格里

详细配置:

①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 }}

④Body:{{ JSON.stringify({ records: \$json.records }) }} ,这里的body值和飞书接口文档中要求的一致即可

配图

四、最后

这一篇内容,我们是把 B 站接进了同一套「选题情报 → 多维表格」的拼图里:和 YouTube、Reddit、频道监控放在一起,我们就有了多源、可对比、可复盘的数据层。这样数据来源更广,我们就不容易把某一渠道的「偶然爆款」当成全局结论。

AI 洞察我们可以像第二弹那样单独做,也可以交给飞书龙虾按表再总结。

【飞书龙虾的那一章节】

配图

*(让龙虾帮我们分析表格数据)*

为什么我一直坚持把数据写进多维表格,而不是只把 Agent 当成唯一入口,因为我认为这种方式对小白和团队都更友好,表里的每一行对应一条可追溯的记录——标题、链接、时间、播放量、评论摘录都在,任何人打开表就能「看见产出了什么」,理解和纠错的成本都低。相比之下,如果只依赖对话式 Agent,中间经历了哪些检索、筛掉了什么、有没有幻觉,往往像黑盒:用起来快,出了问题却难定位,也不利于和同事对齐口径。

多维表格在这里更像人机之间的「可观测层」:自动化负责搬运与结构化,人负责筛选与判断;需要再上 AI 时,也可以让模型基于同一份表内事实去做归纳(例如飞书龙虾、多维表格插件里的 AI),而不是从零开始「口述需求—等一个不可复查的答案」。这样既保留自动化的效率,又保留可迭代的空间。


📋 这套B站关键词相关模板,我整理成模板了

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

🤔 卡在某一步了?

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

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

扫码加我,备注「B站关键词」领模板

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

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部