利用puppeteer实现每日自动健康打卡

2,483 阅读6分钟

github地址 Puppeteer, 中文文档地址 Puppeteer

介绍

Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.

Puppeteer是由Chrome团队开发的Node库, 可以用来控制Chrome或者Chromium. 

那么可以用来做什么呢? 

大部分你能手动在Chrome完成的事情都能用Puppeteer实现! 以下是一些入门指南:
  • 生成页面截图和PDF.
  • 抓取SPA(单页应用程序)并生成预渲染的内容(即“ SSR”(服务器端渲染)).
  • 自动表单提交, UI 测试, 键盘输入, 等等.
  • 创建最新的, 自动化测试环境. 使用最新的JavaScript和浏览器功能,直接在最新版本的Chrome中运行测试。
  • 捕获你的站点的 timeline trace 帮助诊断性能问题.
  • 测试Chrome扩展程序.

自动健康打卡涉及的操作就是提交表单, 点击按钮, 发送请求等.

用法

mkdir puppeteer-test
cd puppeteer-test
npm init
npm i puppeteer-core
# or "yarn add puppeteer-core"

这里选择安装puppeteer-core. puppeteer-core版本不会安装Chromium, 因为Chromium在下载可能会出现问题. 之后直接使用Chrome就可以. 当然也可以手动下载最新版的Chromium. 两者区别可以看 puppeteer vs puppeteer-core.

创建index.js

const main = async () => {
  const browser = await puppeteer.launch({
    //启动
    executablePath:
      "C:\\Program Files (x86)\\Google\\Chrome Dev\\Application\\chrome.exe", //Chrome路径, 可以右键Chrome->属性->目标
    headless: false // 是否以无头模式运行, 默认ture. 无头就是不打开Chrome图形界面, 更快.
  });
  const page = await browser.newPage(); // 打开一个页面, page就是后序将要操作的
  page.setDefaultNavigationTimeout(120000); // 设置页面的打开超时时间, 因为我要打卡的是学校的垃圾服务器, 超时时间设置了2分钟
  try {
    await page.goto("https://baidu.com", { waitUntil: "domcontentloaded" }); //页面跳转, 第二个参数为可选options, 这里表示等待页面结构加载完成, 无需等待img等资源
    console.log("登录页加载成功!"); //控制台输出一下进度
    // 登陆
    await page.evaluate(() => {
      //在当前页面执行js代码, 也就是在浏览器环境执行代码, 可以使用所有的浏览器API
      const usernameInput = document.querySelector("input[name=username]"); //用户名input
      const passwordInput = document.querySelector("input[name=password]"); //密码input
      const loginInput = document.querySelector("input[name=login_submit]"); //登陆按钮
      usernameInput.value = "zhangsan"; //输入用户名密码
      passwordInput.value = "123abc";
      loginInput.click(); //点击登陆
    });
    await page.waitForNavigation(); //因为要跳转页面, 所以这里等待页面导航

    // 查找打卡选项入口在哪, 我的打卡入口登陆后->推荐列表->本科生健康状况申报
    console.log("登陆成功!");
    const url = await page.evaluate(() => {
      const recommendList = [...document.querySelectorAll(".recommendList li")]; // 获取推荐列表所有的项目
      for (let i = 0; i < recommendList.length; i++) {
        const item = recommendList[i];
        if (
          item.querySelector(".app_name").innerText === "本科生健康状况申报"
        ) {
          // 文字说明在app_name下面
          return item.getAttribute("appurl"); //dom的appurl属性是我下一步要跳转的地址, 将这个地址返回, 这样node环境就能拿到了
        }
      }
    });

    await page.goto(url, { waitUntil: "networkidle0" }); //跳转, 这里表示等待所有网络请求完结, 默认请求不在出现后等待500ms
    console.log("表格加载成功!"); //等待结束后我这里会出现填写健康信息的表格
    // 填报信息, 我这里填写第一次后, 后序打卡默认都填好了, 只需点击承诺, 点击确定
    await page.evaluate(() => {
      // 点击承诺
      [...document.querySelectorAll(".infoplus_checkLabel")].pop().click(); //所有checkbox的最后一个是我要的承诺checkbox
      // 点击确认
      document.querySelector(".command_button_content").click(); //点击确定
    });
    // 这里我点击确定后会有一个请求, 等请求结束后在进行下一步
    const res = await page.waitForResponse(
      response =>
        response.url() ===
          "https://baidu.com/infoplus/interface/listNextStepsUsers" &&
        response.status() === 200
    );
    // res.json().then(data => console.log(data)) // 可以把请求内容打出来看看
    // 最后再点一下 好
    await page.evaluate(() => {
      document.querySelector(".dialog_footer button").click();
    });
    await browser.close(); //关闭浏览器结束
  } catch (err) {
    console.log("签到过程出错了!");
    await browser.close();
    throw err;
  }
};


说明

  • headless模式下运行可以直观地看到浏览器操作过程, 有助于调试
  • try catch不必须, 不过学校服务器垃圾, 失败率太高, 需要错误信息做其他操作
  • 具体怎么登陆, 怎么填写信息, 具体情况具体分析.
  • 每次浏览器跳转都需要等待一下才能进行下一步操作, 不知道等多久等到什么时候可以直接page.waitFor(60000), 单位ms
  • 提交表单不一定非得点击按钮, 可以直接向服务器发送请求, 可以在浏览器环境执行fetch

更多功能

现在, 可以在index写下main(), 然后执行node index, 完成一次打卡. 但是, 这样和手动打卡也没多大区别. 所以, 我们需要设置一个定时任务. 

npm的包cron是非常好的选择.

同时打卡是否成功需要有一个提示, 用到node发送邮件包nodemailer

npm i cron nodemailer

index.js中

module.exports = main

mail.js

const nodemailer = require("nodemailer");

const sendMail = msg => {
  nodemailer.createTestAccount((err, account) => {
    let transporter = nodemailer.createTransport({
      service: "qq", // 使用了内置传输发送邮件 查看支持列表:https://nodemailer.com/smtp/well-known/
      port: 465, // SMTP 端口
      secureConnection: true, // 使用了 SSL
      auth: {
        user: "10001.qq.com",
        // 这里密码不是qq密码,是你设置的smtp授权码
        pass: "uiogamsiwefadfs"
      }
    });
    let mailOptions = {
      from: "Tony <10001@qq.com>", // 发送者
      to: "10002.qq.com", // 接收者
      subject: "打卡提醒", // 主题
      html: msg // html body
    };
    transporter.sendMail(mailOptions, (error, info) => {
      if (error) {
        return console.log(error);
      }
      console.log("Message sent: %s", info.messageId);
    });
  });
};

module.exports = sendMail;

cron.js

const CronJob = require("cron").CronJob; // 引入
const tick = require("./main"); // 定时每次执行的任务
const sendMail = require("./mail");

const createJob = (success, fail) => {
  // success成功回调, fail失败回调
  let succeed = false; // 今天是否打卡成功
  let today = new Date().getDate(); // 今天的日期
  const job = new CronJob(
    "0 20 0-11 * * *",
    async onComplete => {
      // 主任务, 参数分别代表任务时间, 任务, 任务完成回调
      const _today = new Date().getDate(); //任务执行时的日期
      if (today !== _today) {
        // 如果任务执行的日期和之前的日期不一样说明已经是第二天了
        succeed = false; // 第二天, 还没打卡, succeed  = false
        today = _today;
      }
      if (!succeed) {
        try {
          await tick();
        } catch (err) {
          fail && fail();
          onComplete("失败!");
          succeed = false;
          return;
        }
        succeed = true;
        success && success();
        onComplete("成功!");
      }
    },
    s => {
      // onComplete
      console.log(s, new Date().toLocaleString());
      console.log("----------");
    }
  );
  job.start(); // 任务开始
};

const success = () => {
  sendMail(`打卡成功 ${new Date().toLocaleString()}`);
};

const fail = () => {
  sendMail(`打卡失败! ${new Date().toLocaleString()}`);
};

createJob(success, fail);


CronJob第一个参数定时时间, 共6个分别表示:

  • Seconds: 0-59
  • Minutes: 0-59
  • Hours: 0-23
  • Day of Month: 1-31
  • Months: 0-11 (Jan-Dec)
  • Day of Week: 0-6 (Sun-Sat)

例如0 20 0-11 * * *表示每天0点到11点的0:20执行定时任务, 详细参考 Cron 

最后, 执行 node cron, 开始定时.

代码丢到服务器上, 一直跑就行了.

没有服务器可以让自己电脑不休眠. 想要休眠或者经常关机可以创建.bat文件(Windows), 写入

cd /d E:\puppeteer-test\src
node index

不用定时了, index中直接执行main, 每次打卡点一下.bat文件一键打卡.