《高阶前端指北》之Node爬虫脚手架(第六弹)

1,367 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第15天,点击查看活动详情

上次我们封装了配置,请求等方法,这次我们做一下机器人事件通知和数据库连接。

事件通知

我们在src/utils/下创建message文件夹,并新建index.js文件(上节已创建)。 然后在创建ding-botfeishu-botwx-bot文件。

如果感觉枯燥,可以直接跳过,重点关注加签即可。

钉钉机器人

const axios = require("axios");
const crypto = require("crypto");
const dayjs = require("dayjs");

const defaultOptions = {
  msgtype: "text",
  text: {
    content: "hello~",
  },
};

class DingBot {
  constructor(options = {}) {
    this.text = "";

    this.webhook = options.webhook;
    this.secret = options.secret;
    const timestamp = new Date().getTime();
    const sign = this.signFn(this.secret, `${timestamp}\n${this.secret}`);
    this.allWebhookUrl = `${this.webhook}&timestamp=${timestamp}&sign=${sign}`;
  }

  signFn(secret, content) {
    // 加签
    const str = crypto
      .createHmac("sha256", secret)
      .update(content)
      .digest()
      .toString("base64");
    return encodeURIComponent(str);
  }

  send(data = defaultOptions) {
    let p;
    // 没有这两个参数则静默失败
    if (!this.webhook || !this.secret) {
      p = Promise.resolve({
        errcode: -1,
        errmsg: "webhook和secret不能为空",
      });
    } else {
      p = axios({
        url: this.allWebhookUrl,
        method: "POST",
        data,
        headers: {
          "Content-Type": "application/json;charset=utf-8",
        },
      }).then((res) => {
        return res.data;
      });
    }
    return p;
  }

  sendMessage(msg) {
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
    }
    this.text += `- ${dayjs().format("HH:mm:ss")} ${msg}\n`;
    this.timer = setTimeout(() => {
      this.send({
        msgtype: "markdown",
        markdown: {
          title: "Push标题",
          text: this.text,
        },
      }).then(() => {
        this.text = "";
      });
    }, 1000);
  }
}

module.exports = DingBot;

飞书机器人

const axios = require("axios");
const crypto = require("crypto");
const dayjs = require("dayjs");

class FeishuBot {
  constructor(options = {}) {
    this.text = "";
    this.webhook = options.webhook;
    this.secret = options.secret;
    // 飞书官方文档描述不清楚,这里的 timestamp 应该精确到秒
    this.timestamp = ~~(Date.now() / 1000);
    this.sign = this.signFn(`${this.timestamp}\n${this.secret}`);
  }

  signFn(content) {
    // 加签
    return (
      crypto
        // 加密一次即可
        .createHmac("sha256", content)
        .digest()
        .toString("base64")
    );
  }

  send(data) {
    let p;
    // 没有这两个参数则静默失败
    if (!this.webhook || !this.secret) {
      p = Promise.resolve({
        errcode: -1,
        errmsg: "webhook和secret不能为空",
      });
    } else {
      p = axios({
        url: this.webhook,
        method: "POST",
        data,
        headers: {
          "Content-Type": "application/json;charset=utf-8",
        },
      }).then((res) => {
        return res.data;
      });
    }
    return p;
  }

  sendMessage(msg) {
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
    }
    this.text += `${dayjs().format("HH:mm:ss")} ${msg}\n`;
    this.timer = setTimeout(() => {
      this.send({
        timestamp: this.timestamp,
        sign: this.sign,
        msg_type: "text",
        content: {
          text: this.text,
        },
      }).then(() => {
        this.text = "";
      });
    }, 1000);
  }
}

module.exports = FeishuBot;

企业微信机器人

const https = require("https");
const config = require("../../config");

function getToken({ id, secret }) {
  return new Promise((resolve, reject) => {
    const option = {
      hostname: "qyapi.weixin.qq.com",
      path: `/cgi-bin/gettoken?corpid=${id}&corpsecret=${secret}`,
      method: "GET",
      headers: {
        "Content-Type": "application/json",
      },
    };
    const req = https.request(option, (res) => {
      const datas = [];
      let size = 0;
      res.on("data", (d) => {
        datas.push(d);
        size += d.length;
      });
      res.on("end", function () {
        const buff = Buffer.concat(datas, size);
        let result = buff.toString();
        if (result) {
          result = JSON.parse(result);
          if (result.errcode == 0) {
            console.log(`获取 accessToken 成功`);
            resolve(result.access_token);
          } else {
            reject(result.errmsg || "获取 accessToken 失败");
          }
        } else {
          reject("获取 accessToken 失败");
        }
      });
    });
    req.on("error", (error) => {
      reject(error);
    });
    req.end();
  });
}

function send({ agentId, touser = "@all", msgData, accessToken }) {
  return new Promise((resolve, reject) => {
    console.log("发送企业微信通知...");
    data = new TextEncoder().encode(
      JSON.stringify({
        touser,
        agentid: agentId,
        duplicate_check_interval: 600,
        ...msgData,
      })
    );
    const option = {
      hostname: "qyapi.weixin.qq.com",
      path: `/cgi-bin/message/send?access_token=${accessToken}`,
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Content-Length": data.length,
      },
    };
    const req = https.request(option, (res) => {
      const datas = [];
      let size = 0;
      res.on("data", (d) => {
        datas.push(d);
        size += d.length;
      });
      res.on("end", function () {
        const buff = Buffer.concat(datas, size);
        let result = buff.toString();
        if (result) {
          result = JSON.parse(result);
          if (result.errcode === 0) {
            console.log("发送通知成功");
            resolve();
          } else {
            reject(result.errmsg || "发送失败");
          }
        }
      });
    });
    req.on("error", (error) => {
      reject(error);
    });
    req.write(data);
    req.end();
  });
}

async function WXWorkNotify({ id, secret, agentId, touser, msgData }) {
  try {
    const accessToken = await getToken({ id, secret });
    await send({
      agentId,
      touser,
      msgData,
      accessToken,
    });
  } catch (error) {
    console.log(`发送失败 => ${error}`);
  }
}
let msg = "\n";
let timer = "";
const wxWorkBot = (message) => {
  if (config.WX_COMPANY_ID && config.WX_APP_ID && config.WX_APP_SECRET) {
    msg += message + "\n";

    timer && clearTimeout(timer);
    timer = setTimeout(function () {
      WXWorkNotify({
        id: config.WX_COMPANY_ID, // 企业 ID
        agentId: config.WX_APP_ID, // 应用 ID
        secret: config.WX_APP_SECRET, // 应用 secret
        msgData: {
          msgtype: "text",
          text: {
            content: msg,
          },
        },
      });
    }, 500);
  }
};
module.exports = wxWorkBot;

连接数据库

通常情况下插入数据库的动作较多,我们暂时先封装一个insert

const mysql = require("mysql");
const config = require("../config");
const connection = mysql.createConnection(config.mysql);

connection.connect();

function insert(table, data) {
  return new Promise((resolve, reject) => {
    connection.query(
      "INSERT INTO " + table + " SET ?",
      data,
      function (error, results, fields) {
        if (error) reject(error);
        resolve(results);
      }
    );
  });
}

module.exports = {
  insert,
};

fs操作相关

通常情况下,我们会保存文件到本地,为了防止重复创建文件或路径,我们需要一些路径判断方法。

const fs = require("fs");
const path = require("path");

/**
 * @description: 读取路径信息,判断路径是否存在
 * @param { string } path
 * @return {Promise().then(false|stats)}
 */
function isHasPath(path) {
  return new Promise((resolve, reject) => {
    fs.stat(path, (err, stats) => {
      if (err) {
        resolve(false);
      } else {
        resolve(stats);
      }
    });
  });
}

/**
 * @description: 创建路径
 * @param { string } dir 路径
 * @return {Promise().then(true|false)}
 */
function mkdir(dir) {
  return new Promise((resolve, reject) => {
    fs.mkdir(dir, (err) => {
      if (err) {
        resolve(false);
      } else {
        resolve(true);
      }
    });
  });
}

/**
 * @description: 判断路径是否存在,如果不存在则创建
 * @param { string } dir
 * @return {boolean}
 */
async function dirExists(dir) {
  let isExists = await getStat(dir);
  if (isExists && isExists.isDirectory()) {
    return true;
  }

  //如果该路径不存在
  let tempDir = path.parse(dir).dir; //拿到上级路径
  //递归判断,如果上级目录也不存在,则会代码会在此处继续循环执行,直到目录存在
  let status = await dirExists(tempDir);

  let mkdirStatus;
  if (status) {
    // 如果路径存在则创建文件夹
    mkdirStatus = await mkdir(dir);
  }
  return mkdirStatus;
}
module.exports = {
  isHasPath,
  mkdir,
  dirExists,
};

总结

本节课,我们封装了Push常用事件,数据库连接事件,文件操作事件等,爬虫基础框架完成了5%了,是否跟我一样很开心。一行又一行,10分钟搞定。

如果喜欢我的文章,麻烦点个赞评个论收个藏关个注

手绘图,手打字,纯原创,摘自未发布的书籍:《高阶前端指北》,转载请获得本人同意。
如果喜欢我的文章,麻烦点个赞评个论收个藏关个注

手绘图,手打字,纯原创,摘自未发布的书籍:《高阶前端指北》,转载请获得本人同意。