2022年10月15日,由于梦春酱的粉丝数回升到2000,让梦春酱非常开心,因此梦春酱发布了一条动态,这条动态里有一张含有所有粉丝的头像和昵称的图片。
那么,我们怎么生成这样子的图片呢?这篇文章就教您如何生成含所有粉丝的列表的图片。
这篇文章比较适合程序员、技术爱好者阅读,如果您没学过编程也可以按照本文的方法尝试。若您遇到任何问题,可以让梦春酱教您一步步操作。
准备工作
本文中的代码都是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*ef
,bili_jct的值为0123456789abcdef0123456789abcdef
,那么就输入代码:
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' };
|
特别注意:请不要把您刚刚复制的“SESSDATA”“bili_jct”中任何一个Cookie的值告诉任何人!它们的值是您的账号的登录信息,与账号、密码的作用相似,别人可能会利用这些值来登录您的账号。
目前,B站的Cookie是定期更新的,所以建议您获取完Cookie后暂时不要访问B站的网页,防止原来的Cookie因更新而失效。待您完成所有步骤后,就可以访问B站的网页了。
第一步 获取所有粉丝的列表
在这个部分中,有一些内容来自https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/user/relation.md。
B站官方给我们提供的获取指定用户的粉丝列表的API是https://api.bilibili.com/x/relation/fans,请求方式是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, "message": "0", "data": { "list": [{ "mid": 12345678, "attribute": 6, "mtime": 1678901234, "tag": [-10], "special": 1, "contract_info": { "is_contract": true, "is_contractor": true, "ts": 1678901234, "user_attr": 1 }, "uname": "Example", "face": "https://i0.hdslb.com/bfs/face/xxx.jpg", "sign": "个性签名", "face_nft": 0, "official_verify": { "type": -1, "desc": "" }, "vip": { "vipStatus": 1, }, }, { }, ], "total": 2000 } }
|
我们就先来尝试获取一下自己粉丝列表的第1页吧(每页50个粉丝)。
下面是梦春酱写的代码,记得要在顶层(top level)或者异步(async)函数中运行,在非异步函数中运行会报错,后面梦春酱写的所有代码也需要在顶层或异步函数中运行。
console.log((await (await fetch('https://api.bilibili.com/x/relation/fans?vmid=425503913&ps=50&pn=1', { headers })).json()).data.list);
|
运行上面的代码后,正常情况下控制台会显示一个带有很多元素的数组(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++) { followers.push(...(await (await fetch(`https://api.bilibili.com/x/relation/fans?vmid=425503913&ps=50&pn=${i}`, { headers })).json()).data.list); }
|
这样,“followers”变量就存储了最多1000个粉丝的列表。
出于安全目的,B站采取了一些措施,使用户无法通过常规手段获取到超过1000个粉丝的列表。也就是说,如果您的粉丝数超过了1000,就没有办法直接获取到不在刚刚获取到的粉丝列表里的粉丝了。
当然,如果您在没有超过1000粉丝的时候就保存了自己所有粉丝的列表,那么您可以将之前的列表与现在的列表合并,记得去除重复项。
1 2 3 4
| for (const f of oldFollowers) { if (!followers.find(t => t.mid === f.mid)) followers.push(f); }
|
但是,合并后的列表里的用户现在不一定仍在关注您,所以要移除没有关注您的用户。
获取用户与自己关系的API是https://api.bilibili.com/x/web-interface/relation,请求方式是GET。这个API需要您提供有效的Cookie。
主要的URL参数为:
如果这个API被正确调用,那么会得到像下面这样的JSON回复(仅作示例,省略了部分项目):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| { "code": 0, "message": "0", "data": { "relation": { "mid": 12345678, "attribute": 6, "mtime": 1678901234, "tag": [-10], "special": 1 }, "be_relation": { "mid": 425503913, "attribute": 6, "mtime": 1612345678, "tag": [123456], "special": 0 } } }
|
下面的代码会分别查询自己与每个用户的关系,可能会执行很长时间。
1 2 3 4 5 6 7
| const realFollowers = []; for (const f of followers) { const rjson = await (await fetch(`https://api.bilibili.com/x/web-interface/relation?mid=${f.mid}`, { headers })).json(); if ([1, 2, 6].includes(rjson.data.be_relation.attribute)) realFollowers.push(f); }
followers = realFollowers;
|
第二步 获取所有粉丝的详细信息、粉丝数(可选)
在这个部分中,有一些内容来自https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/user/info.md与https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/user/status_number.md。
目前“followers”变量虽然存储了所有粉丝的信息,但是这个信息不够详细,比如不包括等级、头像框信息等,我们要想办法获取更详细的粉丝信息。
获取多个用户的详细信息的API是https://api.bilibili.com/x/polymer/pc-electron/v1/user/cards,请求方式是GET,这个API调用一次可以获取最多50个用户的信息。
主要的URL参数为:
参数名 | 内容 | 必要性 | 备注 |
---|
uids | 目标用户的UID列表 | 必要 | 每个成员间用英文逗号, 分割,最多50个成员 |
如果这个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
| { "code": 0, "message": "0", "data": { "12345678": { "mid": "12345678", "face": "https://i0.hdslb.com/bfs/face/xxx.jpg", "name": "Example", "official": { "desc": "", "role": 0, "title": "", "type": -1 }, "vip": { "status": 1, "type": 1, }, }, "23456789": { }, } }
|
获取多个用户的关系状态数的API是https://api.bilibili.com/x/relation/stats,请求方式是GET。
主要的URL参数为:
参数名 | 内容 | 必要性 | 备注 |
---|
mids | 目标用户UID列表 | 必要 | 每个成员间用英文逗号, 分割,最多20个成员 |
如果这个API被正确调用,那么会得到像下面这样的JSON回复(仅作示例,省略了部分项目):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| { "code": 0, "message": "0", "data": { "12345678": { "mid": 12345678, "following": 234, "follower": 345 }, "23456789": { }, } }
|
于是我们就可以写出下面的代码:
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
| const followersWithoutInfo = followers.map(f => f.mid), cjsonList = [];
while (followersWithoutInfo.length) { cjsonList.push(fetch(`https://api.bilibili.com/x/polymer/pc-electron/v1/user/cards?uids=${followersWithoutInfo.splice(0, 50).join(',')}`, { headers }).then(resp => resp.json())); }
for await (const cjson of cjsonList) { if (cjson.code === 0) { if (cjson.data) { for (const [mid, info] of Object.entries(cjson.data)) { Object.assign(followers.find(f => f.mid === +mid), info, { mid: +mid }); } } } }
const followersWithoutStat = followers.map(f => f.mid), sjsonList = [];
while (followersWithoutStat.length) { sjsonList.push(fetch(`https://api.bilibili.com/x/relation/stats?mids=${followersWithoutStat.splice(0, 20).join(',')}`, { headers }).then(resp => resp.json())); }
for await (const sjson of sjsonList) { if (sjson.code === 0) { if (sjson.data) { for (const [mid, stat] of Object.entries(sjson.data)) { Object.assign(followers.find(f => f.mid === +mid), { follower: stat.follower }); } } } }
|
这样,“followers”变量就存储了所有粉丝的信息与粉丝数。
第三步 生成图片
我们既然已经获取到了所需要的信息,就应该要生成粉丝列表的图片了。您可以用自己喜欢的方式生成图片。
梦春酱提供了一种生成图片的方法:先生成HTML文件,界面类似于梦春酱的动态里的图片,再在浏览器中截图。
先在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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/ (?= )|(?<= ) |^ | $/gm, ' ').replace(/\r\n|\r|\n/g, '<br />') : '';
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://www.yumeharu.top/images/default-faces%26face-icons/nft.gif" />` : ''}${(u.official || u.official_verify)?.type === 0 ? '<img class="face-icon" src="https://www.yumeharu.top/images/default-faces%26face-icons/personal.svg" />' : (u.official || u.official_verify)?.type === 1 ? '<img class="face-icon" src="https://www.yumeharu.top/images/default-faces%26face-icons/business.svg" />' : u.vip?.status ? '<img class="face-icon" src="https://www.yumeharu.top/images/default-faces%26face-icons/big-vip.svg" />' : ''}</div> <div><strong>${encodeHTML(u.name || u.uname)}</strong></div></div>`).join('');
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://www.yumeharu.top/images/default-faces%26face-icons/nft.gif" />` : ''}${(u.official || u.official_verify)?.type === 0 ? '<img class="face-icon" src="https://www.yumeharu.top/images/default-faces%26face-icons/personal.svg" />' : (u.official || u.official_verify)?.type === 1 ? '<img class="face-icon" src="https://www.yumeharu.top/images/default-faces%26face-icons/business.svg" />' : u.vip?.status ? '<img class="face-icon" src="https://www.yumeharu.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);
|
再将网页转换成图片:
我们可以在浏览器中打开生成的文件,然后打开开发者工具(一般按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 122 123 124 125 126 127 128 129 130 131 132 133 134
| 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' };
const encodeHTML = str => typeof str === 'string' ? str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/ (?= )|(?<= ) |^ | $/gm, ' ').replace(/\r\n|\r|\n/g, '<br />') : '';
const ujson = await (await fetch('https://api.bilibili.com/x/web-interface/nav', { headers })).json(); const UID = ujson.data.mid;
let followers = []; for (let i = 1; i <= 20; i++) { followers.push(...(await (await fetch(`https://api.bilibili.com/x/relation/fans?vmid=${UID}&ps=50&pn=${i}`, { headers })).json()).data.list); }
const followersWithoutInfo = followers.map(f => f.mid), jsonList = [];
while (followersWithoutInfo.length) { jsonList.push(fetch(`https://api.bilibili.com/x/polymer/pc-electron/v1/user/cards?uids=${followersWithoutInfo.splice(0, 50).join(',')}`, { headers }).then(resp => resp.json())); }
for await (const cjson of jsonList) { if (cjson.code === 0) { if (cjson.data) { for (const [mid, info] of Object.entries(cjson.data)) { Object.assign(followers.find(f => f.mid === +mid), info, { mid: +mid }); } } } }
const followersWithoutStat = followers.map(f => f.mid), sjsonList = [];
while (followersWithoutStat.length) { sjsonList.push(fetch(`https://api.bilibili.com/x/relation/stats?mids=${followersWithoutStat.splice(0, 20).join(',')}`, { headers }).then(resp => resp.json())); }
for await (const sjson of sjsonList) { if (sjson.code === 0) { if (sjson.data) { for (const [mid, stat] of Object.entries(sjson.data)) { Object.assign(followers.find(f => f.mid === +mid), { follower: stat.follower }); } } } }
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://www.yumeharu.top/images/default-faces%26face-icons/nft.gif" />` : ''}${(u.official || u.official_verify)?.type === 0 ? '<img class="face-icon" src="https://www.yumeharu.top/images/default-faces%26face-icons/personal.svg" />' : (u.official || u.official_verify)?.type === 1 ? '<img class="face-icon" src="https://www.yumeharu.top/images/default-faces%26face-icons/business.svg" />' : u.vip?.status ? '<img class="face-icon" src="https://www.yumeharu.top/images/default-faces%26face-icons/big-vip.svg" />' : ''}</div> <div><strong>${encodeHTML(u.name || u.uname)}</strong></div></div>`).join('');
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://www.yumeharu.top/images/default-faces%26face-icons/nft.gif" />` : ''}${(u.official || u.official_verify)?.type === 0 ? '<img class="face-icon" src="https://www.yumeharu.top/images/default-faces%26face-icons/personal.svg" />' : (u.official || u.official_verify)?.type === 1 ? '<img class="face-icon" src="https://www.yumeharu.top/images/default-faces%26face-icons/business.svg" />' : u.vip?.status ? '<img class="face-icon" src="https://www.yumeharu.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);
|
下面的图片就是梦春酱在2022年10月15日生成的粉丝列表图片。
![梦春酱在2022年10月15日生成的所有粉丝列表的图片]()