前段时间发过一篇关于微信机器人开发的文章,讲述了如何快速开发一个微信机器人,本篇文章就来实现一个最近开发的一个功能案例,在这个案例中会遇到了各种问题,可以帮助大家减少自己去踩坑的时间。通过此案例也可以帮助你去扩想一些更符合你的使用场景。
如果你还不了解微信机器人的开发,建议先阅读下面的文章:
只需要6行代码,就可以开发一个微信机器人
通过微信定时推送功能,每日向用户发送LeetCode的每日题目。这项服务不仅方便了编程爱好者和求职者随时练习算法题,而且解决了缺乏动力或组织者来提醒刷题的问题,鼓励读者养成每日刷题的学习习惯。
功能描述
- 每日通过微信消息的方式,把leetcode当日题目推送到指定联系人或群聊中。
- 给机器人发送消息,随机推送一条leetcode的题目
推送的消息中需要包含题目的一些基本信息。比如:题目名称、难度等级、通过率、解题地址等。
为了可以更直观的了解详情,也需要将题目内容进行推送。由于内容一般都是比较大的,所以采用图片的方式推送是一个不错的选择。效果如下:
如果你对今天的题目比较感兴趣,就可以通过点击解题地址去打卡今日任务,即使电脑不在身边,也可通过题目内容图片了解到今日题目内容
实现
- 接收消息:接收到指定的消息后,推送内容,例如:每日一题、随机出题
- 定时推送:每日定时将题目题送到指定联系人或群聊
- 获取数据:获取leetcode题目内容
- 整合数据:生成需要推送的文本和图片消息
- 发送消息:将内容推送给用户
1. 获取数据
打开leetcode官网首页,可以看到在题目列表中第一项就是当题的题目。
打开控制台,可以看到有很多相同的URL请求,使用的是graphQL查询。一个一个的点看查看返回内容,找到今日题目的请求。
复制请求参数,构建请求。
// 获取今日题目
async function fetchTodayLeetCode() {const url = "https://leetcode.cn/graphql/";return new Promise((resolve) => {axios.post(url, {query: "\n query questionOfToday {\n todayRecord {\n date\n userStatus\n question {\n questionId\n frontendQuestionId: questionFrontendId\n difficulty\n title\n titleCn: translatedTitle\n titleSlug\n paidOnly: isPaidOnly\n freqBar\n isFavor\n acRate\n status\n solutionNum\n hasVideoSolution\n topicTags {\n name\n nameTranslated: translatedName\n id\n }\n extra {\n topCompanyTags {\n imgUrl\n slug\n numSubscribed\n }\n }\n }\n lastSubmission {\n id\n }\n }\n}\n ",variables: {},operationName: "questionOfToday",}).then((res) => {try {const data = res.data.data.todayRecord[0];resolve(data);} catch (error) {resolve("");}}).catch(() => {resolve("");});});
}
拿到返回的数据后,就可以提取想要发送的数据信息。
// 获取今天LeetCode题目
async function getTodayLeetCode() {// 获取题目数据const data = await fetchTodayLeetCode();if (!data) return "";const question = data.question;const { title, titleCn, titleSlug, difficulty, acRate } = question;let difficultyCn = "未知";switch (difficulty) {case "Hard":difficultyCn = "困难";break;case "MEDIUM":difficultyCn = "中等";break;case "EASY":ifficultyCn = "简单";break;default:break;}
const url = `https://leetcode.cn/problems/${titleSlug}/description`;const text = `----每日一题----\n题目:${titleCn}\n难度:${difficultyCn}\n通过率:${Number(acRate * 100).toFixed(2)}%\n解题地址:${url}`;return text;
}
在首页的请求只会返回题目的基本信息,没有内容信息,此时需要点击进入题目详情页面,按照同样的方法,找到查询题目详情的接口来获取详情内容。
这里需要注意修改查询参数titleSlug的值,改为题目中返回的titleSlug字段。
// 查询题目详情
async function fetchLeetCodeQuestionDetail(titleSlug) {const url = "https://leetcode.cn/graphql/";return new Promise((resolve) => {axios.post(url, {query: "\n query questionTranslations($titleSlug: String!) {\n question(titleSlug: $titleSlug) {\n translatedTitle\n translatedContent\n }\n}\n ",variables: {titleSlug: titleSlug,},operationName: "questionTranslations",}).then((res) => {try {const data = res.data.data.question;resolve(data);} catch (error) {resolve("");}}).catch(() => {resolve("");});});
}
2. 内容截图
题目详情接口中返回的详情内容格式为html字符串的格式,很容易就联想到将其渲染后再进行截图。
如果是在前段环境下,这很容易做到,可以直接创建一个元素,设置innerHtml为题目内容,变成真正的dom元素。然后使用html2canvas
转成图片即可。
但是在node环境下是没有window对象的,相关的API也不可用。这里介绍一个强大的node.js库puppeteer来实现这个过程。
Puppeteer 是一个 Node.js 库,提供了一套高级 API 通过 DevTools 协议控制 Chrome 或 Chromium 浏览器,用于自动化测试、网页抓取、生成页面截图和 PDF 等,它的功能还远不止这些。
async function htmlText2Image(content, savePath) {// 启动一个新的浏览器实例const browser = await puppeteer.launch();// 创建一个新的页面const page = await browser.newPage();// 设置要渲染的HTML内容;const htmlContent = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>leetcode题目</title><style>pre {white-space: pre-wrap;}
</style></head><body>${content}</body></html>`;// 将HTML内容设置为页面的内容await page.setContent(htmlContent);// 将页面渲染为图片并保存到文件await page.screenshot({path: savePath,fullPage: true,});// 关闭浏览器await browser.close();
}
对图片中的内容可以灵活的自定义,比如添加额外信息,添加一些样式等,就跟我们开发一个页面一样简单。
// ...
const detail = await fetchLeetCodeQuestionDetail(titleSlug);
// 截图
const { translatedTitle, translatedContent } = detail;
await htmlText2Image(`<h1>${translatedTitle} <span style="font-size:18px;font-weight:400">难度:${difficultyCn} 通过率:${Number(acRate * 100).toFixed(2)}%</span></h1>${translatedContent}<div style="text-align:center;"><img src="https://resource.dengzhanyong.com/images/9ce7efc7-d880-470d-8864-9c29e242c4f5.png" style="height:120px;"/></div>`,"这里是图片存储的路径"
);
// ...
封装推送的图片消息
const { FileBox } = require("file-box");
const imagePath = '这里是图片的路径';
const imageFileBox = FileBox.fromFile(imagePath);
踩坑记录
到现在为止,上面一切在本地运行测试都没问题,但当我部署到服务器(centos
)上时,一系列的问题就出现了。
1. 服务器上没有chrome浏览器
使用sudo yum install chromium
去安装,会报找不到chromium这个包,可能是源不对,并且这种方式安装的chromium会有很多问题,不推荐。
推荐直接下载安装包,然后手动安装:
wget https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm
mkdir ./google_chrome
mv google-chrome-stable_current_x86_64.rpm ./google_chrome/
cd ./google_chrome # 进入目录
yum -y install google-chrome-stable_current_x86_64.rpm
2. glibc版本过低
centos7默认的glibc函数库的版本为2.17,chromium要求glibc-2.25
版本,因此需要升级glibc版本。可能不同版本的chromium要求glibc不同,根据实际错误提示进行升级。
#查询已安装的glibc版本
strings /lib64/libc.so.6 | grep GLIBC
3. chromium拥有很多依赖库,必须把所有的必须依赖库全部安装上才可以
sudo yum install libatk1.0-0 libatk-bridge2.0-0 libcups2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libpango1.0-0 libasound2
4. 需要禁用沙箱模式
在启动浏览器是,需要配置--no-sandbox
参数,其的作用是禁用沙箱模式,这可以解决在一些环境中由于权限问题无法正常启动浏览器的问题,并提高浏览器的启动速度。
// 启动一个新的浏览器实例
const browser = await puppeteer.launch({args: ["--no-sandbox"],
});
5. 截图中无法显示中文
使用Puppeteer
进行截图时,会出现部分中文显示方块字乱码的问题。这并不是Puppeteer的问题,实际上是Linux
字体库对中文支持不好的原因。
只需要给服务器的Linux系统安装支持的中文字体库即可。
sudo yum install wqy-microhei-fonts.noarch -y
sudo yum install wqy-unibit-fonts.noarch -y
sudo yum install wqy-zenhei-fonts.noarch -y
消息推送
最后只需要创建定时器,或接收到指定消息后执行上面的流程即可。
1. 创建推送列表配置信息
const = LEETCODE_PUSH_LIST: [{name: "前端筱园交流群",id: "@@6ab29397570b5672d1b53945432b5ce1326dc43682bf3de4dff4c3579afa4325",date: "0 30 9 * * *",}
]
2. 封装定时器
const schedule = require("node-schedule");
function setSchedule(date, callback) {schedule.scheduleJob(date, callback);
}
3. 机器人登录后启动定时器
bot.on("login", onLogin);
async function onLogin(user) {console.log(`${user} 登录成功`);setTimeout(() => {leetCodePush(this);}, 5000);
}
/*** 每日一题推荐* @param {*} that*/
async function leetCodePush(that) {console.log("开启每日一题推荐");const list = config.LEETCODE_PUSH_LIST;if (list.length) {for (const item of list) {const { title, name, date, count } = item;setSchedule(date, async () => {const message = await getTodayLeetCode();// 找到目标群聊const room = await findRoom(that, name);if (room && message) {// 如果是多条消息,则逐条推送if (isArray(message)) {for (const item of message) {await room.say(item);}} else {room.say(message);}}});}}
}
收到指定消息回复中间的流程都一样,只是需要判断下收到的消息是否匹配,如果是个人发送就回复给个人,如果是群聊中被@,就推送到群聊中。
通过本案例不仅是希望你了解学习到更多的关于**微信机器人
、chromium
、puppeteer
**等相关知识。更是为了让你开拓自己的想象空间,开发更多的适合自己的使用场景,给你的生活带来便利。
写在最后
最后,如果你觉得这个功能对你很有帮助,但是自己又不想去开发,回复“交流群”加入前端筱园交流群,就可以免费体验啦。
可以在群内添加前端筱园机器人为好友,独自享受更多功能服务。
欢迎访问我的个人网站:www.dengzhanyong.com
欢迎加入前端筱园交流群:
关注我的公众号【前端筱园】,不错过每一篇推送