1、什么是puppeteer?
puppeteer是google开源的一套利用nodejs实现的自动爬取网站,实现自动化操作的工具包,使用puppeteer可以实现网站数据爬取、UI自动化测试、学习puppeteer对于前端人员来说成本最低。puppeteer全都是使用的async/await语法,利用了大量的异步编程的思想。
2、本文大概需求概况
需求大概是这几天我那惹人喜欢的婆娘因为之前离职了导致某些原因必须要在国家社会保险公共服务平台上来提交关系转接,然后总是猴急猴撩的想知道转接进度,一天登录个好几次网站去看,我就突发奇想想着为什么不利用个puppeteer来做个小Demo练习一下呢,于是就有了今天的文章。说的不对的地方还请各位大佬多多指教!哈哈,代码中使用的包主要有如下几个:
puppeteer(google爬虫所需要的包)、nodemailer(发送邮件的包)、node-schedule(定时任务)、pm2(运行项目,自动重启项目)、tencentcloud-sdk-nodejs(腾讯云ocr识别所需要的包)
(1)首先我们必须安装puppeteer,执行命令
//1、创建一个文件夹 demo
npm init -y
cd demo
//2、安装puppeteer
cnpm i puppeteer --save //没有安装cnpm的使用npm
puppeteer会自动下载google的Chromium可以理解成chrome浏览器的阉割版,后面的自动化操作都是利用该软件自动执行的。puppeteer的语法API大家可以上puppeteer中文网查看,里面讲的很清晰,需要用什么东西就直接拿就是了。
(2)首先分析一下国家社会保险公共服务平台这个网站,点击进入后看到的界面是这样的
首先我们需要进入该页面,然后大家可以hover一下登录按钮会出现个人登录、企业登录两个选项,我们当然要得是个人登录。那么我们要做几个事情:
1)打开一个浏览器,创建一个page页面,进入网址。
2)hover登录按钮,点击个人登录。
就这两个事情,那我们的代码基本是下面这样。
创建getReport.js文件如下:
const puppeteer = require("puppeteer");
console.log("初始化执行");
//启动浏览器,接受一个对象设置browser对象的参数
const browser = await puppeteer.launch({
headless:false, //无头部 ,运行时会自动打开Chromium浏览器,设置为true不会打开浏览器。
slowMo:100, //降低执行速度,方便观察
devtools:true //打开开发者工具
})
const page = await browser.newPage(); //等待browser对象创建page
page.setViewport({width:1200,height:900}) //设置视窗的大小
await page.goto("http://si.12333.gov.cn"); //page对象跳转至指定页面
await page.waitForSelector("#login_button") //等待登录按钮出现
const closeDiv = await page.$(".closeDiv"); //获取右下角 烦人的弹窗对象
closeDiv && await closeDiv.click() //如果获取成功,则执行click方法关闭
await page.hover("#login_button"); //hover登录按钮
const userLoginBtn = await page.$("a.usercenter"); //找到个人登录按钮
await userLoginBtn.click(); //点击个人登录按钮
await page.waitForNavigation(); //等待页面跳转
上面代码中主要用到的是async/await来异步执行代码,其中page.$(“selector”)跟css选择器是一样一样的。
(3)很好,如果上面不出意外的话会自动跳转至登录页面
首先打开开发者工具找到账号、密码的id选择器,#userName,#pwd,编写代码,没有账号密码的可以自己注册一个。
await page.type("#userName","你的账号"); //输入账号
await page.type("#pwd","你的密码"); //输入密码
page.type方法接受两个参数,第一个是css选择器,第二个是输入的内容,同时page.type每个字符输入后都会触发 keydown, keypress/input 和 keyup 事件。这个算个小知识点。
输入账号密码之后,就到了验证码了,然后我发现wtf,这是个什么鬼,图形验证码,然后就一顿百度找各种图像识别ocr库,找到了一个比较热门的 tesseract,github上有如何使用tesseract,评星很高,但是我使用了一下发现非常的不友好,首先2.0版本的按照官方的指引文档根本不会执行,这个大家可以去了解一下,issues找了很久发现是需要指定几个文件路径,后续执行成功后发现wtf,这个库识别率低的可怕,图形验证码的准确率基本0%,这个库牛逼的地方在于可以自己使用训练模型,训练程序,提交准确率。该库训练后会自动生成eng.traineddata文件,至于如何训练大家可以自行百度一下,文章讲的很多,首先需要一些样本图片来让程序识别,然后再做后续操作。
一看到需要样本图片,算了,不用了,因为我用的别人的网站根本没有那些样本图片,后续想到了使用云能力解决这个问题,毕竟大佬还是大佬嘛,有白嫖的东西为啥不用,傻吗?
这里使用的腾讯云的OCR工具
首先登陆腾讯云控制台,找到左侧英文识别选项,点击右上角接口文档
点击调试按钮
其中左侧的个人秘钥必填项,如果没有秘钥,点击查看秘钥自己申请一个,然后再secretID及secretKey中输入。
下面的输入参数,其种图片ImageBase64,及图片路径ImageUrl二选一,如果传递了url则以url为准。
由于我的思想是将二维码截图保存在本地然后利用腾讯云识别所有的代码都在自己本地跑,没有放在服务器,所以这里选择的是ImageBase64模式,将图片转成base64之后传递。
腾讯云的代码复制过来依赖于tencentcloud-sdk-nodejs包,所以大家需要先安装一下
cnpm i tencentcloud-sdk-nodejs --save
然后创建ocr.js文件如下:
const tencentcloud = require("tencentcloud-sdk-nodejs");
class Ocr{
constructor(){
}
imgToBase64(imgPath){
const fs = require("fs");
const path = require("path")
const data = fs.readFileSync(imgPath); //不要再设置编码
return data;
}
async init(imgPath){
const OcrClient = tencentcloud.ocr.v20181119.Client;
const models = tencentcloud.ocr.v20181119.Models;
const Credential = tencentcloud.common.Credential;
const ClientProfile = tencentcloud.common.ClientProfile;
const HttpProfile = tencentcloud.common.HttpProfile;
//注意这里改成你自己的secretId及secretKey值
let cred = new Credential("你的secretId", "你的secret秘钥");
let httpProfile = new HttpProfile();
httpProfile.endpoint = "ocr.tencentcloudapi.com";
let clientProfile = new ClientProfile();
clientProfile.httpProfile = httpProfile;
let client = new OcrClient(cred, "ap-guangzhou", clientProfile);
let req = new models.EnglishOCRRequest();
let params = `{"ImageBase64":"${this.imgToBase64(imgPath)}"}`;
// console.log(params)
req.from_json_string(params);
// console.log(req)
return new Promise((resolve,reject)=>{
client.EnglishOCR(req, function(errMsg, response) {
if (errMsg) {
reject(errMsg);
return;
}
resolve(response.to_json_string())
// console.log(response.to_json_string());
});
})
}
}
module.exports = {Ocr}
这个ocr类主要是接受传进来的base64图片文件,然后将base64图片发给腾讯云识别,接受返回的数据。
我们修改getReport.js文件如下
const puppeteer = require("puppeteer");
const {TimeoutError} = puppeteer.errors;
// const {createWorker} = require("tesseract.js"); //引入图形识别
const path = require("path");
const {Ocr} = require("./ocr");
const {NodeMailer} = require("./nodemail");
const schedule = require("node-schedule"); //定时任务
class Report {
constructor(){}
schedule(){ //每天早上十点半自动发送
var j = schedule.scheduleJob('30 30 10 * * *', ()=>{
this.init();
});
}
async init(){
console.log("初始化执行");
const browser = await puppeteer.launch({
headless:false, //无头部
slowMo:100,
devtools:true
})
const page = await browser.newPage();
page.setViewport({width:1200,height:900})
await page.goto("http://si.12333.gov.cn");
await page.waitForSelector("#login_button")
const closeDiv = await page.$(".closeDiv"); //关闭弹窗
closeDiv && await closeDiv.click()
await page.hover("#login_button");
const userLoginBtn = await page.$("a.usercenter"); //个人登录按钮
await userLoginBtn.click();
await page.waitForNavigation();
// console.log("已跳转至登录页面")
await page.type("#userName","你的登录账号"); //输入账号
await page.type("#pwd","你的密码"); //输入密码
const yzmFunction = async ()=>{
try{ //处理ocr异常bug
const userNameValue = await page.$eval("#userName",el=>el.value)
!userNameValue && await page.type("#userName","你的账号")
const pwdVal = await page.$eval("#pwd",el=>el.value);
!pwdVal && await page.type("#pwd","你的密码"); //输入密码
//防止验证码无法出现的bug由于网站会经常出现无法出现验证码的情况,所以这里click两次,降低异常情况
const imgReload = await page.$("#img_captcha")
await imgReload.click()
await imgReload.click()
const yzm_el = await page.$(".yzm");
//调用screenShot将元素截图保存在本地,并且以base64的形式存储起来
await yzm_el.screenshot({ //将图片已base64的形式存储起来
path:"./img/1.png",
encoding:"base64"
})
//定义正则至匹配数字 + 英文。
let reg = /^[a-zA-Z0-9]{4}$/g;
//执行ocr识别 出现异常自动重新执行
let result = await new Ocr().init(path.join(__dirname,"./img/1.png")); //接受ocr识别返回的验证码
result = JSON.parse(result)
const yzm = result.TextDetections[0].DetectedText.trim() //获取识别的文本内容
//这里做一层处理,过滤掉不是4位数的验证码,及非数字及英文的。
if(yzm.length == 4 && reg.test(yzm)){ //正则匹配必须4位数为数字及英文
console.log("验证码为:" + yzm)
await page.type("#yz_text",yzm); //输入验证码
const login_btn = await page.$("#gr_login") //登录按钮
await login_btn.click() //点击登录按钮
await page.waitForNavigation({waitUntil:["networkidle0"]});//等待没有请求
page.on("response", async response =>{
// console.log(response.url())
let url = response.url();
if(url.indexOf("/cas/siLogin") > -1){
//由于登录时采用form表单提交的方式,在官网上并没有找到form表单提交返回的数据,还请各位大佬多指教,这里我只是将form返回的response捕获。
console.log( response.status())
}
})
await page.goto("http://si.12333.gov.cn/1989594.jhtml"); //直接跳转至详情页并执行截图
//等待list列表的dom 元素出现
await page.waitForSelector(".slick-viewport",{
timeout:20000
}) //waitfroSelector与goto一起使用
console.log("开始截图") //执行截图
const p = path.join(__dirname,"./img" + getDate() + ".png");
await page.screenshot({ //每日生成图片
path:p
})
console.log("截图已完成")
await browser.close(); //关闭浏览器
new NodeMailer(p).sendMail(); //发送邮件
}else{
console.log("识别的验证码为" + yzm)
//验证码验证不通过自动重新执行验证码过程
yzmFunction(); //如果没有则继续重试
}
}catch(e){
if(e instanceof TimeoutError){ //如果是waitfor方法超时则自动重试
console.log("连接超时,重试")
yzmFunction()
return;
// return;
}
console.log("发生异常错误!重新识别验证码")
yzmFunction()
}
}
yzmFunction()
function getDate(){ //获取日期
var d = new Date();
var year = d.getFullYear()
var month = d.getMonth() + 1;
var day = d.getDate() ;
year = year >= 10 ? year : '0' +year;
month = month >= 10 ? month : '0' +month;
day = day >= 10 ? day : '0' +day;
return year + month + day;
}
console.log("执行完毕")
}
}
new Report().schedule(); //立即执行
上面代码大概指的是,封装了验证码的函数,
- 由于当验证码不正确,点击登录按钮会自动清空账号密码,所以这里做了一层判断当用户账号密码为空时才进行输入。
- 验证码由于网络原因经常会显示不出来,这里使用imgReload.click()执行了两次点击,降低验证码显示异常的几率。
3)await yzm_el.screenshot({ //将图片已base64的形式存储起来
path:"./img/1.png",
encoding:“base64”
})
将验证码图片截图,并保存为base64形式,存放在本地。
4)正则过滤掉不是4位验证码及非数字或英文的验证码。
5)输入验证码,当验证码不通过时waitForNavigation会出现TimeoutError异常,我们在try/catch中重新执行yzmFunction方法,重新执行后续过程。当验证码通过时,跳转至详情页,并截图保存。
好了,这个大致思想就是这样,项目中我还用到了nodeMailer包将截图发送邮件通知。使用了node-schedule来创建了定时任务来定时执行登录,并发送邮件。
创建一个nodemail.js文件如下:
const nodemailer = require('nodemailer');
class NodeMailer {
constructor(imgPath){
this.imgPath = imgPath;
}
sendMail(){
let transporter = nodemailer.createTransport({
// host: 'smtp.ethereal.email',
service: 'qq', // 使用了内置传输发送邮件 查看支持列表:https://nodemailer.com/smtp/well-known/
port: 465, // SMTP 端口
secureConnection: true, // 使用了 SSL
auth: {
user: '你的邮箱',
// 这里密码不是qq密码,是你设置的smtp授权码
pass: '你的smtp授权码',
}
});
let mailOptions = {
from: '你的邮箱', // sender address
to:["接收人的邮箱'],
subject: '来自老公的邮件', // Subject line
// 发送text或者html格式
// text: 'Hello world?', // plain text body
html: '<b>社会关系转移邮件!</b>', // html body
attachments:[
{
filename:"审核状态图片.png",
path:this.imgPath
}
]
};
// send mail with defined transport object
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
return console.log(error);
}
console.log('Message sent: %s', info.messageId);
});
}
}
module.exports = {NodeMailer}
大家可以自己去百度查一下如何使用这些包。记得将发件人邮箱及接收人邮箱改过来。
最后代码写好了之后使用 pm2包将文件运行起来
cnpm i pm2 -g //全局安装
执行命令 pm2 start getReport.js
第一次写博客请大家多多指教。后续将公共的东西抽取出来方便修改,并发布到github上,大佬们多多评论指教。