微信定时推送LeetCode每日一题,再也不怕没人喊你刷题了

前段时间发过一篇关于微信机器人开发的文章,讲述了如何快速开发一个微信机器人,本篇文章就来实现一个最近开发的一个功能案例,在这个案例中会遇到了各种问题,可以帮助大家减少自己去踩坑的时间。通过此案例也可以帮助你去扩想一些更符合你的使用场景。

如果你还不了解微信机器人的开发,建议先阅读下面的文章:

只需要6行代码,就可以开发一个微信机器人

通过微信定时推送功能,每日向用户发送LeetCode的每日题目。这项服务不仅方便了编程爱好者和求职者随时练习算法题,而且解决了缺乏动力或组织者来提醒刷题的问题,鼓励读者养成每日刷题的学习习惯。

功能描述

  • 每日通过微信消息的方式,把leetcode当日题目推送到指定联系人或群聊中。
  • 给机器人发送消息,随机推送一条leetcode的题目

推送的消息中需要包含题目的一些基本信息。比如:题目名称、难度等级、通过率、解题地址等。

为了可以更直观的了解详情,也需要将题目内容进行推送。由于内容一般都是比较大的,所以采用图片的方式推送是一个不错的选择。效果如下:

如果你对今天的题目比较感兴趣,就可以通过点击解题地址去打卡今日任务,即使电脑不在身边,也可通过题目内容图片了解到今日题目内容

实现

  • 接收消息:接收到指定的消息后,推送内容,例如:每日一题、随机出题
  • 定时推送:每日定时将题目题送到指定联系人或群聊
  • 获取数据:获取leetcode题目内容
  • 整合数据:生成需要推送的文本和图片消息
  • 发送消息:将内容推送给用户

1. 获取数据

打开leetcode官网首页,可以看到在题目列表中第一项就是当题的题目。

打开控制台,可以看到有很多相同的URL请求,使用的是graphQL查询。一个一个的点看查看返回内容,找到今日题目的请求。

复制请求参数,构建请求。

javascript 复制代码
// 获取今日题目
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("");
        });
    });
}

拿到返回的数据后,就可以提取想要发送的数据信息。

javascript 复制代码
// 获取今天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字段。

javascript 复制代码
// 查询题目详情
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 等,它的功能还远不止这些。

javascript 复制代码
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();
}

对图片中的内容可以灵活的自定义,比如添加额外信息,添加一些样式等,就跟我们开发一个页面一样简单。

javascript 复制代码
// ...
const detail = await fetchLeetCodeQuestionDetail(titleSlug);
// 截图
const { translatedTitle, translatedContent } = detail;
await htmlText2Image(
    `<h1>${translatedTitle}&nbsp;&nbsp;<span style="font-size:18px;font-weight:400">难度:${difficultyCn}&nbsp;&nbsp;通过率:${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>`,
    "这里是图片存储的路径"
);
​
// ...

封装推送的图片消息

javascript 复制代码
const { FileBox } = require("file-box");
​
const imagePath = '这里是图片的路径';
const imageFileBox = FileBox.fromFile(imagePath);

踩坑记录

到现在为止,上面一切在本地运行测试都没问题,但当我部署到服务器(centos)上时,一系列的问题就出现了。

1. 服务器上没有chrome浏览器 使用sudo yum install chromium 去安装,会报找不到chromium这个包,可能是源不对,并且这种方式安装的chromium会有很多问题,不推荐。

推荐直接下载安装包,然后手动安装:

powershell 复制代码
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不同,根据实际错误提示进行升级。

powershell 复制代码
#查询已安装的glibc版本
strings /lib64/libc.so.6 | grep GLIBC

3. chromium拥有很多依赖库,必须把所有的必须依赖库全部安装上才可以

powershell 复制代码
sudo yum install libatk1.0-0 libatk-bridge2.0-0 libcups2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libpango1.0-0 libasound2 

4. 需要禁用沙箱模式

在启动浏览器是,需要配置--no-sandbox 参数,其的作用是禁用沙箱模式,这可以解决在一些环境中由于权限问题无法正常启动浏览器的问题,并提高浏览器的启动速度。

powershell 复制代码
// 启动一个新的浏览器实例
const browser = await puppeteer.launch({
    args: ["--no-sandbox"],
});

5. 截图中无法显示中文

使用Puppeteer进行截图时,会出现部分中文显示方块字乱码的问题。这并不是Puppeteer的问题,实际上是Linux字体库对中文支持不好的原因。 只需要给服务器的Linux系统安装支持的中文字体库即可。

powershell 复制代码
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. 创建推送列表配置信息

javascript 复制代码
const = LEETCODE_PUSH_LIST: [
    {
        name: "前端筱园交流群",
        id: "@@6ab29397570b5672d1b53945432b5ce1326dc43682bf3de4dff4c3579afa4325",
        date: "0 30 9 * * *",
    }
]

2. 封装定时器

javascript 复制代码
const schedule = require("node-schedule");
function setSchedule(date, callback) {
    schedule.scheduleJob(date, callback);
}

3. 机器人登录后启动定时器

javascript 复制代码
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);
            }
         }
        });
    }
    }
}

收到指定消息回复中间的流程都一样,只是需要判断下收到的消息是否匹配,如果是个人发送就回复给个人,如果是群聊中被@,就推送到群聊中。

通过本案例不仅是希望你了解学习到更多的关于**微信机器人chromiumpuppeteer**等相关知识。更是为了让你开拓自己的想象空间,开发更多的适合自己的使用场景,给你的生活带来便利。

写在最后

最后,如果你觉得这个功能对你很有帮助,但是自己又不想去开发,回复"交流群"加入前端筱园交流群,就可以免费体验啦。 可以在群内添加前端筱园机器人为好友,独自享受更多功能服务。

欢迎到我的个人网站(www.dengzhanyong.com)

关注我的公众号【前端筱园】,不错过每一篇推送

加入【前端筱园交流群】,与大家一起交流,共同进步!

相关推荐
狗头大军之江苏分军16 分钟前
iPhone 17 vs iPhone 17 Pro:到底差在哪?买前别被忽悠了
前端
小林coding16 分钟前
再也不怕面试了!程序员 AI 面试练习神器终于上线了
前端·后端·面试
文心快码BaiduComate28 分钟前
WAVE SUMMIT深度学习开发者大会2025举行 文心大模型X1.1发布
前端·后端·程序员
babytiger29 分钟前
python 通过selenium调用chrome浏览器
前端·chrome
passer98135 分钟前
基于webpack的场景解决
前端·webpack
华科云商xiao徐42 分钟前
Java并发编程常见“坑”与填坑指南
javascript·数据库·爬虫
奶昔不会射手1 小时前
css3之grid布局
前端·css·css3
举个栗子dhy1 小时前
解决在父元素上同时使用 onMouseEnter和 onMouseLeave时导致下拉菜单无法正常展开或者提前收起问题
前端·javascript·react.js