2022年10月15日,梦春酱的粉丝数回升到2000,于是梦春酱非常开心地发布了一条动态,这条动态的其中一张图片里就有TA的所有粉丝的头像与昵称。
那么,我们怎么生成这样子的图片呢?这篇文章就教您如何生成含所有粉丝的列表的图片。
这篇文章比较适合程序员、技术爱好者阅读,如果您没学过编程也可以按照本文的方法尝试。若您遇到任何问题,可以让梦春酱教您一步步操作。
准备工作
本文中的代码都是JavaScript代码,所以您应该要预先安装Node.js(建议您下载长期维护版,即LTS版)。您也可以使用其他编程语言,不过需要对本文中的代码进行一些小改动。
首先,我们应该获取B站页面的Cookie。
以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站的网页了。
第一步 获取所有粉丝的列表
B站官方给我们提供的获取指定用户的粉丝列表的API是https://api.bilibili.com/x/relation/fans,请求方式是GET。
这个API需要您提供有效的Cookie,返回的列表按照关注时间的先后顺序逆向排序(越晚关注,就在列表的越前面)。
主要的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 41
| { "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": { "vipType": 1, "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)。
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”变量就存储了您的粉丝列表。
第二步 获取所有粉丝的详细信息、粉丝数(可选)
目前“followers”变量虽然存储了所有粉丝的信息,但是这个信息不够详细,比如不包括等级、头像框信息等,我们要想办法获取更详细的粉丝信息。
获取多个用户的详细信息的API是https://api.vc.bilibili.com/x/im/user_infos,请求方式是GET。
主要的URL参数为:
| 参数名 | 内容 | 必要性 | 备注 |
|---|
| uids | 目标用户的UID列表 | 必要 | 每个成员间用英文逗号,分割 |
如果这个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 47
| { "code": 0, "msg": "0", "message": "0", "data": [{ "mid": 12345678, "name": "Example", "sex": "保密", "face": "https://i0.hdslb.com/bfs/face/xxx.jpg", "sign": "个性签名", "level": 3, "silence": 0, "vip": { "type": 1, "status": 1, }, "pendant": { "pid": 0, "name": "", "image": "", "image_enhance": "", }, "official": { "role": 0, "title": "", "desc": "", "type": -1 }, "is_deleted": 0, "face_nft": 0, "is_senior_member": 0, }, { }, ] }
|
获取多个用户的关系状态数的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
| const cjson = await (await fetch(`https://api.vc.bilibili.com/x/im/user_infos?uids=${followers.map(f => f.mid)}`, { headers })).json(); if (cjson.code === 0 && cjson.data?.length) { for (const info of cjson.data) { Object.assign(followers.find(f => f.mid === info.mid), info); } }
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 && sjson.data) { for (const [mid, stat] of Object.entries(sjson.data)) { Object.assign(followers.find(f => f.mid === +mid), stat); } } }
|
这样,“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
| 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 cjson = await (await fetch(`https://api.vc.bilibili.com/x/im/user_infos?uids=${followers.map(f => f.mid)}`, { headers })).json(); if (cjson.code === 0 && cjson.data?.length) { for (const info of cjson.data) { Object.assign(followers.find(f => f.mid === info.mid), info); } }
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 && sjson.data) { for (const [mid, stat] of Object.entries(sjson.data)) { Object.assign(followers.find(f => f.mid === +mid), stat); } } }
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日生成的所有粉丝列表的图片]()