如何生成B站粉丝列表图片

2022年10月15日,由于wuziqian211的粉丝数回升到2000,让wuziqian211非常开心,因此wuziqian211发布了一条动态,这条动态里有一张含有所有粉丝的头像和昵称的图片。
那么,我们怎么生成这样子的图片呢?这篇文章就教您如何生成含所有粉丝的列表的图片。

这篇文章比较适合程序员、技术爱好者阅读,如果您没学过代码也可以按照本文的方法尝试。若您遇到任何问题,可以让wuziqian211教您一步步操作。

准备工作

本文中的代码都是JavaScript代码,所以您应该要预先安装Node.js建议您下载长期维护版,即LTS版)。您也可以使用其他编程语言,不过需要对本文中的代码进行一些小改动。

以Google Chrome为例:在登录了B站账号的浏览器中,打开B站任意页面,打开开发者工具(一般按F12键即可),在工具上方点击“应用”,在左侧点击“存储”部分中“Cookie”左边的箭头,点击下面的B站网址,在右侧表格的“名称”一栏中找到“SESSDATA”与“bili_jct”,分别双击它们右边的“值”,复制下来,这样您就获取到了Cookie。
获取Cookie
打开Node.js,您应该会看到一个命令行窗口。在这个窗口里输入代码const headers = { Cookie: 'SESSDATA=SESSDATA的值; bili_jct=bili_jct的值, Origin: 'https://www.bilibili.com', Referer: 'https://www.bilibili.com/', 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36' };,便于在后续操作中使用您账号的登录信息。
例:假如SESSDATA的值1a2b3c4d%2C1789012345%2C5e6f7*efbili_jct的值0123456789abcdef0123456789abcdef,那么就输入代码:

1
const headers = { Cookie: 'SESSDATA=1a2b3c4d%2C1789012345%2C5e6f7*ef; bili_jct=0123456789abcdef0123456789abcdef', Origin: 'https://www.bilibili.com', Referer: 'https://www.bilibili.com/', 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36' }; // 注意:此 Cookie 仅作为示例展示,请修改成自己的 Cookie

特别注意:请不要把您刚刚复制的“SESSDATA”“bili_jct”中任何一个Cookie的值告诉任何人!它们的值是您的账号的登录信息,与账号、密码的作用相似,别人可能会利用这些值来登录您的账号。

目前,B站的Cookie是定期更新的,所以建议您获取完Cookie后暂时不要访问B站的网页,防止原来的Cookie因更新而失效。待您完成所有步骤后,就可以访问B站的网页了。

第一步 获取所有粉丝的列表

B站官方给我们提供的获取指定用户的粉丝列表的API是https://api.bilibili.com/x/relation/followers,请求方式是GET。
这个API需要您提供有效的Cookie,返回的列表按照关注时间的先后顺序逆向排序(越晚关注,就在列表的越前面),最多只能获取到最近关注的1000名粉丝的信息。
主要URL参数包括:

参数名内容必要性备注
vmid目标用户UID必要
ps每页项数非必要默认为50,且最多为50
pn页码非必要默认为1

如果这个API被正确调用,那么会得到像下面这样的JSON回复(仅作为示例展示,一些项已经省略):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
"code": 0, // 返回值,0 表示成功,-400 表示请求错误,22007 表示访问超过 5 页
"message": "0", // 错误信息,默认为 “0”
// ...
"data": {
"list": [{ // 粉丝 1 的信息
"mid": 12345678, // 粉丝 UID
"attribute": 6, // 该粉丝对于自己的关系,0 表示您未关注 TA,1 表示您悄悄关注了 TA,2 表示您关注了 TA,6 表示您与 TA 互粉,128 表示您拉黑了 TA
"mtime": 1678901234, // 最近一次改变目标用户对于该粉丝的关系的秒级时间戳
"tag": [-10], // 该粉丝对于目标用户的关注分组,其中 -10 为特别关注分组;若没有关注或为默认分组,则为 null
"special": 1, // 目标用户是否特别关注了该粉丝
"contract_info": { // “老粉计划” 相关信息
"is_contract": true, // 目标用户是否为该粉丝的 “原始粉丝” 或 “老粉”,仅当为真时才显示此项
"is_contractor": true, // 粉丝是否为目标用户的 “原始粉丝” 或 “老粉”,仅当为真时才显示此项
"ts": 1678901234, // 粉丝成为目标用户的 “原始粉丝” 或 “老粉” 的秒级时间戳,仅当 “is_contractor” 项的值为真时才显示此项
"user_attr": 1 // 粉丝是否为目标用户的 “老粉”,仅当为真时才显示此项
},
"uname": "Example", // 粉丝昵称
"face": "https://i0.hdslb.com/bfs/face/xxx.jpg", // 粉丝头像地址
"sign": "个性签名", // 粉丝个性签名
"face_nft": 0, // 头像是否为数字藏品头像
"official_verify": { // 用户认证信息
"type": -1, // 用户认证状态,-1 表示未认证,0 表示 UP 主认证,1 表示机构认证
"desc": "" // 用户认证说明文字
},
"vip": { // 用户会员信息
// ...
"vipStatus": 1, // 用户会员状态,0 表示没有大会员,1 表示有大会员
// ...
},
// ...
}, { // 粉丝 2 的信息
// (数据结构同上)
},
// ...
],
// ...
"total": 2000 // 目标用户的粉丝数
}
}

我们就先来尝试获取一下自己粉丝列表的第1页吧(每页50个粉丝)。
下面是wuziqian211写的代码,记得要在顶层(top level)或者异步(async)函数中运行,在非异步函数中运行会报错,后面wuziqian211写的所有代码也需要在顶层或异步函数中运行。

1
console.log((await (await fetch('https://api.bilibili.com/x/relation/followers?vmid=425503913&ps=50&pn=1', { headers })).json()).data.list); // 注意:请将 “vmid=” 后面的数字修改成自己的 UID

运行上面的代码后,正常情况下控制台会显示一个带有很多元素的数组(array),而且数组的每个元素都是对象(object)。
我们可以在上面代码的基础上稍作修改,来获取多页粉丝列表。如果您设置的每页项数为50,那么您要获取的页数一般为自己的粉丝数除以50,再向上取整(取不小于该数值的最小整数,如2.98→3、3→3、3.02→4)。由于B站的限制,最多只能获取最后关注您的1000个粉丝的列表,所以如果您的粉丝数超过了1000,建议您只获取前20页粉丝列表,继续往后获取也是获取不到信息的。

1
2
3
4
let followers = []; // 存储粉丝列表
for (let i = 1; i <= 20; i++) { // 获取前 20 页粉丝的信息,每页 50 个;这里的页数是根据自己的粉丝数而定的
followers.push(...(await (await fetch(`https://api.bilibili.com/x/relation/followers?vmid=425503913&ps=50&pn=${i}`, { headers })).json()).data.list); // 注意:请将 “vmid=” 后面的数字修改成自己的 UID
}

这样,“followers”变量就存储了最多1000个粉丝的列表。

出于安全目的,B站采取了一些措施,使用户无法通过常规手段获取到超过1000个粉丝的列表。也就是说,如果您的粉丝数超过了1000,就没有办法直接获取到不在刚刚获取到的粉丝列表里的粉丝了。
当然,如果您在没有超过1000粉丝的时候就保存了自己所有粉丝的列表,那么您可以将之前的列表与现在的列表合并,记得去除重复项。

1
2
3
4
// 假设 “oldFollowers” 变量为之前存储的所有粉丝信息的数组
for (const f of oldFollowers) {
if (!followers.find(t => t.mid === f.mid)) followers.push(f);
}

但是,合并后的列表里的用户现在不一定仍在关注您,所以要移除没有关注您的用户。

获取用户与自己关系的API是https://api.bilibili.com/x/space/wbi/acc/relation,请求方式是GET。
这个API需要您提供有效的Cookie,也需要使用Wbi签名来鉴权,详见https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md。下面的代码已经实现了Wbi鉴权。
主要URL参数包括:

参数名内容必要性备注
mid目标用户的UID必要
wts当前时间戳必要https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md
w_ridWbi签名必要https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md

如果这个API被正确调用,那么会得到像下面这样的JSON回复(仅作为示例展示,一些项已经省略):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"code": 0, // 返回值,0 表示成功,-400 表示请求错误
"message": "0", // 错误信息,默认为 0
// ...
"data": {
{
"relation": { // 该用户对于自己的关系
"mid": 12345678, // 该用户的 UID
"attribute": 6, // 该用户对于自己的关系代码,0 表示您未关注 TA,1 表示您悄悄关注了 TA,2 表示您关注了 TA,6 表示您与 TA 互粉,128 表示您拉黑了 TA
"mtime": 1678901234, // 最近一次改变用户对于自己的关系的秒级时间戳;若自己没有关注用户,则为 0
"tag": [-10], // 用户对于自己的关注分组,其中 -10 为特别关注分组;若没有关注或为默认分组,则为 null
"special": 1 // 自己是否特别关注了用户
},
"be_relation": { // 自己对于该用户的关系
"mid": 425503913, // 自己的 UID
"attribute": 6, // 自己对于该用户的关系代码
"mtime": 1612345678, // 最近一次改变自己对于该用户的关系的秒级时间戳;若用户没有关注自己,则为 0
"tag": [123456], // 自己对于该用户的关注分组
"special": 0 // 用户是否特别关注了自己
}
}
}
}

下面的代码会分别查询自己与每个用户的关系,可能会执行很长时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const crypto = require('node:crypto');
const md5 = data => { // 对数据进行 MD5 加密
const md5Hash = crypto.createHash('md5');
md5Hash.update(data, 'utf-8');
return md5Hash.digest('hex');
};
const encodeWbi = async originalQuery => { // 对请求参数进行 Wbi 签名,改编自 https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md
const ujson = await (await fetch('https://api.bilibili.com/x/web-interface/nav', { headers })).json();
const imgKey = ujson.data.wbi_img.img_url.replace(/^(?:.*\/)?([^\.]+)(?:\..*)?$/, '$1'),
subKey = ujson.data.wbi_img.sub_url.replace(/^(?:.*\/)?([^\.]+)(?:\..*)?$/, '$1');
const mixinKey = [46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52].reduce((accumulator, n) => accumulator + (imgKey + subKey)[n], '').slice(0, 32), // 对 imgKey 和 subKey 进行字符顺序打乱编码
query = { ...originalQuery, wts: Math.floor(Date.now() / 1000) }; // 添加 wts 字段
const params = new URLSearchParams(Object.keys(query).sort().map(name => [name, String(query[name]).replace(/[!'()*]/g, '')])); // 按照 key 重排参数,过滤 value 中的 “!”“'”“(”“)”“*” 字符
params.append('w_rid', md5(params + mixinKey)); // 计算 w_rid
return params;
};

const realFollowers = [];
for (const f of followers) { // 获取所有在粉丝列表里的用户与自己的关系
const rjson = await (await fetch(`https://api.bilibili.com/x/space/wbi/acc/relation?mid=${await encodeWbi({ mid: f.mid })}`, { headers })).json();
if ([1, 2, 6].includes(rjson.data.be_relation.attribute)) realFollowers.push(f); // 如果用户现在正在关注您,可以加入到 “realFollowers” 数组
}

followers = realFollowers;

第二步 获取所有粉丝的详细信息、粉丝数

目前“followers”变量虽然存储了所有粉丝的信息,但是这个信息不够详细,我们要想办法获取更详细的粉丝信息。
获取用户的详细信息的API是https://api.bilibili.com/x/space/wbi/acc/info,请求方式是GET。
这个API需要您提供有效的Cookie,也需要使用Wbi签名来鉴权。
主要URL参数包括:

参数名内容必要性备注
mid目标用户的UID必要
wts当前时间戳必要https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md
w_ridWbi签名必要https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md

如果这个API被正确调用,那么会得到像下面这样的JSON回复(仅作为示例展示,一些项已经省略):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
{
"code": 0, // 返回值,0 表示成功,-400 表示请求错误
"message": "0", // 错误信息,默认为 0
// ...
"data": { // 用户信息
"mid": 12345678, // 用户 UID
"name": "Example", // 用户昵称
"sex": "保密", // 用户性别
"face": "https://i0.hdslb.com/bfs/face/xxx.jpg", // 用户头像地址
"face_nft": 0, // 头像是否为数字藏品头像
// ...
"sign": "个性签名", // 用户个性签名
// ...
"level": 6, // 用户等级
// ...
"silence": 0, // 用户是否被封禁
// ...
"official": { // 用户认证信息
"role": 0, // 用户认证类型
"title": "", // 用户认证说明文字
"desc": "", // 用户认证备注
"type": -1, // 用户认证状态,-1 表示未认证,0 表示 UP 主认证,1 表示机构认证
},
"vip": { // 用户会员信息
"type": 1, // 用户会员类型
"status": 1, // 用户会员状态,0 表示没有大会员,1 表示有大会员
// ...
},
"pendant": { // 用户头像框信息
"pid": 0, // 头像框 ID
"name": "", // 头像框名称
"image": "", // 头像框图片地址
// ...
"image_enhance": "", // 头像框动态图片地址
// ...
},
// ...
"is_followed": true, // 是否已关注该用户
"top_photo": "https://i0.hdslb.com/bfs/space/xxx.png", // 个人空间头图地址
// ...
"birthday": "01-01", // 用户生日的月、日,若未公开则为空文本
// ...
"is_senior_member": 0, // 用户是否为硬核会员
// ...
}
}

获取用户关系状态数的API是https://api.bilibili.com/x/relation/stat,请求方式是GET。
主要URL参数包括:

参数名内容必要性
vmid目标用户UID必要

如果这个API被正确调用,那么会得到像下面这样的JSON回复(仅作为示例展示,一些项已经省略):

1
2
3
4
5
6
7
8
9
10
11
{
"code": 0, // 返回值,0 表示成功
"message": "0", // 错误信息
// ...
"data": {
"mid": 12345678, // 用户 UID
"following": 234, // 用户关注数
// ...
"follower": 345 // 用户粉丝数
}
}

于是我们就可以写出下面的代码:

1
2
3
4
5
6
7
for (const f of followers) { // 获取所有粉丝的详细信息、粉丝数
const info = await (await fetch(`https://api.bilibili.com/x/space/wbi/acc/info?${await encodeWbi({ mid: f.mid })}`, { headers })).json();
if (info.code === 0) Object.assign(f, info.data);

const relationStat = await (await fetch(`https://api.bilibili.com/x/relation/stat?vmid=${f.mid}`, { headers })).json();
if (relationStat.code === 0) f.follower = relationStat.data.follower;
}

这样,“followers”变量就存储了所有粉丝的信息与粉丝数。

第三步 生成图片

我们既然已经获取到了所需要的信息,就应该要生成粉丝列表的图片了。您可以用自己喜欢的方式生成图片。
wuziqian211提供了一种生成图片的方法:先生成HTML文件,界面类似于wuziqian211的动态里的图片,再在浏览器中截图。
先在Node.js中生成HTML文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
const encodeHTML = str => typeof str === 'string' ? str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/ (?= )|(?<= ) |^ | $/gm, '&nbsp;').replace(/\n/g, '<br />') : '';

// 此处提供了 2 种样式,您可以任选一个样式
// 样式 1:每个粉丝之间换行
const html = followers.map(u => `<div class="info"><div class="image-wrap${u.pendant?.image ? ' has-frame' : ''}"><img class="face" src="${u.face}" referrerpolicy="no-referrer" />${u.pendant?.pid ? `<img class="face-frame" src="${u.pendant.image_enhance || u.pendant.image}" referrerpolicy="no-referrer" />` : ''}${u.face_nft ? `<img class="face-icon icon-face-nft${[0, 1].includes((u.official || u.official_verify)?.type) || u.vip?.status ? ' second' : ''}" src="https://wuziqian211.top/images/default-faces%26face-icons/nft.gif" />` : ''}${(u.official || u.official_verify)?.type === 0 ? '<img class="face-icon" src="https://wuziqian211.top/images/default-faces%26face-icons/personal.svg" />' : (u.official || u.official_verify)?.type === 1 ? '<img class="face-icon" src="https://wuziqian211.top/images/default-faces%26face-icons/business.svg" />' : u.vip?.status ? '<img class="face-icon" src="https://wuziqian211.top/images/default-faces%26face-icons/big-vip.svg" />' : ''}</div> <div><strong>${encodeHTML(u.name || u.uname)}</strong></div></div>`).join('');

// 样式 2:一个粉丝紧跟着另一个粉丝
const html = followers.map(u => `<div class="inline-block"><div class="info"><div class="image-wrap${u.pendant?.image ? ' has-frame' : ''}"><img class="face" src="${u.face}" referrerpolicy="no-referrer" />${u.pendant?.pid ? `<img class="face-frame" src="${u.pendant.image_enhance || u.pendant.image}" referrerpolicy="no-referrer" />` : ''}${u.face_nft ? `<img class="face-icon icon-face-nft${[0, 1].includes((u.official || u.official_verify)?.type) || u.vip?.status ? ' second' : ''}" src="https://wuziqian211.top/images/default-faces%26face-icons/nft.gif" />` : ''}${(u.official || u.official_verify)?.type === 0 ? '<img class="face-icon" src="https://wuziqian211.top/images/default-faces%26face-icons/personal.svg" />' : (u.official || u.official_verify)?.type === 1 ? '<img class="face-icon" src="https://wuziqian211.top/images/default-faces%26face-icons/business.svg" />' : u.vip?.status ? '<img class="face-icon" src="https://wuziqian211.top/images/default-faces%26face-icons/big-vip.svg" />' : ''}</div> <div><strong>${encodeHTML(u.name || u.uname)}</strong></div></div></div>`).join('');

const content = `
<style>
* {
font-family: Lato, 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 20px;
overflow-wrap: anywhere;
text-align: justify;
}
div.inline-block {
display: inline-block;
margin-right: 5px;
}
div.info {
align-items: center;
display: flex;
}
div.image-wrap {
margin-right: 5px;
position: relative;
}
img {
vertical-align: middle;
}
img.face {
border-radius: 50%;
height: 60px;
}
img.icon-face-nft {
border: 2px solid var(--background-color);
box-sizing: border-box;
}
div.image-wrap.has-frame img.face {
height: 51px;
padding: 19.5px;
}
div.image-wrap.has-frame img.face-frame {
height: 90px;
left: calc(50% - 45px);
position: absolute;
top: 0;
}
div.image-wrap img.face-icon {
border-radius: 50%;
height: 18px;
left: calc(50% + 13.25px);
position: absolute;
top: calc(50% + 13.25px);
}
div.image-wrap img.face-icon.second {
left: calc(50% - 3.75px);
}
div.image-wrap.has-frame img.face-icon {
left: calc(50% + 9px);
top: calc(50% + 9px);
}
div.image-wrap.has-frame img.face-icon.second {
left: calc(50% - 8px);
}
</style>
${html}`;

fs.writeFileSync('followers.html', content); // 注意:请将 “followers.html” 修改成生成的 HTML 文件的名称

再将网页转换成图片:我们可以在浏览器中打开生成的文件,然后打开开发者工具(一般按F12键即可),点击右上角的三个点展开菜单,选择“运行命令”(也可直接按下Ctrl+Shift+P),输入“屏幕截图”,再选择“截取完整尺寸的屏幕截图”,并选择保存图片的位置,就可以保存一张包括所有粉丝的图片了。
生成图片

总结

生成自己的所有粉丝列表的图片看似很难,实际上只有三个步骤,每个步骤不需要您进行太多操作。
下面被折叠的代码就是实现上述功能的完整代码,您可以复制代码并适当修改一下代码,运行脚本,就可以生成您自己的粉丝列表的图片了。

点击查看完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// 初始化
const headers = { Cookie: 'SESSDATA=1a2b3c4d%2C1789012345%2C5e6f7*ef; bili_jct=0123456789abcdef0123456789abcdef', Origin: 'https://www.bilibili.com', Referer: 'https://www.bilibili.com/', 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36' }; // 注意:此 Cookie 仅作为示例展示,请修改成自己的 Cookie

const crypto = require('node:crypto');
const md5 = data => { // 对数据进行 MD5 加密
const md5Hash = crypto.createHash('md5');
md5Hash.update(data, 'utf-8');
return md5Hash.digest('hex');
};
const encodeWbi = async originalQuery => { // 对请求参数进行 Wbi 签名,改编自 https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md
const ujson = await (await fetch('https://api.bilibili.com/x/web-interface/nav', { headers })).json();
const imgKey = ujson.data.wbi_img.img_url.replace(/^(?:.*\/)?([^\.]+)(?:\..*)?$/, '$1'),
subKey = ujson.data.wbi_img.sub_url.replace(/^(?:.*\/)?([^\.]+)(?:\..*)?$/, '$1');
const mixinKey = [46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52].reduce((accumulator, n) => accumulator + (imgKey + subKey)[n], '').slice(0, 32), // 对 imgKey 和 subKey 进行字符顺序打乱编码
query = { ...originalQuery, wts: Math.floor(Date.now() / 1000) }; // 添加 wts 字段
const params = new URLSearchParams(Object.keys(query).sort().map(name => [name, String(query[name]).replace(/[!'()*]/g, '')])); // 按照 key 重排参数,过滤 value 中的 “!”“'”“(”“)”“*” 字符
params.append('w_rid', md5(params + mixinKey)); // 计算 w_rid
return params;
};
const encodeHTML = str => typeof str === 'string' ? str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/ (?= )|(?<= ) |^ | $/gm, '&nbsp;').replace(/\n/g, '<br />') : '';

// 获取可以获取到的粉丝的信息
let followers = []; // 存储粉丝列表
for (let i = 1; i <= 20; i++) { // 获取前 20 页粉丝的信息,每页 50 个;这里的页数是根据自己的粉丝数而定的
followers.push(...(await (await fetch(`https://api.bilibili.com/x/relation/followers?vmid=425503913&ps=50&pn=${i}`, { headers })).json()).data.list); // 注意:请将 “vmid=” 后面的数字修改成自己的 UID
}

/* 如果您之前保存过自己所有粉丝的列表,可以执行以下代码:
// 假设 “oldFollowers” 变量为之前存储的所有粉丝信息的数组
for (const f of oldFollowers) {
if (!followers.find(t => t.mid === f.mid)) followers.push(f);
}

// 移除没有关注自己的用户(耗时较长)
const realFollowers = [];
for (const f of followers) { // 获取所有在粉丝列表里的用户与自己的关系
const rjson = await (await fetch(`https://api.bilibili.com/x/space/wbi/acc/relation?mid=${await encodeWbi({ mid: f.mid })}`, { headers })).json();
if ([1, 2, 6].includes(rjson.data.be_relation.attribute)) realFollowers.push(f); // 如果用户现在正在关注您,可以加入到 “realFollowers” 数组
}

followers = realFollowers;
*/

// 获取所有粉丝的详细信息、粉丝数
for (const f of followers) {
const info = await (await fetch(`https://api.bilibili.com/x/space/wbi/acc/info?${await encodeWbi({ mid: f.mid })}`, { headers })).json();
if (info.code === 0) Object.assign(f, info.data);

const relationStat = await (await fetch(`https://api.bilibili.com/x/relation/stat?vmid=${f.mid}`, { headers })).json();
if (relationStat.code === 0) f.follower = relationStat.data.follower;
}

// 生成文件,此处提供了 2 种样式,您可以任选一个样式
// 样式 1:每个粉丝之间换行
const html = followers.map(u => `<div class="info"><div class="image-wrap${u.pendant?.image ? ' has-frame' : ''}"><img class="face" src="${u.face}" referrerpolicy="no-referrer" />${u.pendant?.pid ? `<img class="face-frame" src="${u.pendant.image_enhance || u.pendant.image}" referrerpolicy="no-referrer" />` : ''}${u.face_nft ? `<img class="face-icon icon-face-nft${[0, 1].includes((u.official || u.official_verify)?.type) || u.vip?.status ? ' second' : ''}" src="https://wuziqian211.top/images/default-faces%26face-icons/nft.gif" />` : ''}${(u.official || u.official_verify)?.type === 0 ? '<img class="face-icon" src="https://wuziqian211.top/images/default-faces%26face-icons/personal.svg" />' : (u.official || u.official_verify)?.type === 1 ? '<img class="face-icon" src="https://wuziqian211.top/images/default-faces%26face-icons/business.svg" />' : u.vip?.status ? '<img class="face-icon" src="https://wuziqian211.top/images/default-faces%26face-icons/big-vip.svg" />' : ''}</div> <div><strong>${encodeHTML(u.name || u.uname)}</strong></div></div>`).join('');

// 样式 2:一个粉丝紧跟着另一个粉丝
const html = followers.map(u => `<div class="inline-block"><div class="info"><div class="image-wrap${u.pendant?.image ? ' has-frame' : ''}"><img class="face" src="${u.face}" referrerpolicy="no-referrer" />${u.pendant?.pid ? `<img class="face-frame" src="${u.pendant.image_enhance || u.pendant.image}" referrerpolicy="no-referrer" />` : ''}${u.face_nft ? `<img class="face-icon icon-face-nft${[0, 1].includes((u.official || u.official_verify)?.type) || u.vip?.status ? ' second' : ''}" src="https://wuziqian211.top/images/default-faces%26face-icons/nft.gif" />` : ''}${(u.official || u.official_verify)?.type === 0 ? '<img class="face-icon" src="https://wuziqian211.top/images/default-faces%26face-icons/personal.svg" />' : (u.official || u.official_verify)?.type === 1 ? '<img class="face-icon" src="https://wuziqian211.top/images/default-faces%26face-icons/business.svg" />' : u.vip?.status ? '<img class="face-icon" src="https://wuziqian211.top/images/default-faces%26face-icons/big-vip.svg" />' : ''}</div> <div><strong>${encodeHTML(u.name || u.uname)}</strong></div></div></div>`).join('');

const content = `
<style>
* {
font-family: Lato, 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 20px;
overflow-wrap: anywhere;
text-align: justify;
}
div.inline-block {
display: inline-block;
margin-right: 5px;
}
div.info {
align-items: center;
display: flex;
}
div.image-wrap {
margin-right: 5px;
position: relative;
}
img {
vertical-align: middle;
}
img.face {
border-radius: 50%;
height: 60px;
}
img.icon-face-nft {
border: 2px solid var(--background-color);
box-sizing: border-box;
}
div.image-wrap.has-frame img.face {
height: 51px;
padding: 19.5px;
}
div.image-wrap.has-frame img.face-frame {
height: 90px;
left: calc(50% - 45px);
position: absolute;
top: 0;
}
div.image-wrap img.face-icon {
border-radius: 50%;
height: 18px;
left: calc(50% + 13.25px);
position: absolute;
top: calc(50% + 13.25px);
}
div.image-wrap img.face-icon.second {
left: calc(50% - 3.75px);
}
div.image-wrap.has-frame img.face-icon {
left: calc(50% + 9px);
top: calc(50% + 9px);
}
div.image-wrap.has-frame img.face-icon.second {
left: calc(50% - 8px);
}
</style>
${html}`;

fs.writeFileSync('followers.html', content); // 注意:请将 “followers.html” 修改成生成的 HTML 文件的名称

下面的图片就是wuziqian211在2022年10月15日生成的粉丝列表图片。
wuziqian211在2022年10月15日生成的所有粉丝列表的图片