生活的不易体现在各个方面,想买个车还需要车牌指标,因此我一直挂外地牌照在天津跑。2014年就申请车牌指标,竟然到现在也没有摇上号。每月都在查询摇号结果,但是总忘记密码,为了方便,想写个爬虫点两下就得到摇号结果还是很爽的。
先来看看效果:
获取cookie
由于登录中有验证码,因此获取验证码的时候cookie就通过set-cookie
属性返回到浏览器。因此我就通过调用该接口获取cookie。使用的请求工具为superagent
,了解详细的接口可以查看文档,获取response
中的set-cookie
属性。
保存验证码
由于验证码识别时需要使用实际的图片文件,因此需要将验证码数据流保存为文件。获取cookie时调用的是获取验证码的接口,返回的数据中包含验证码数据信息。首先我先在跟目录下建立临时文件夹temp
,将文件写入该目录下,以uuid作为文件名。
let tempUrl = `${__dirname}\\temp`; //验证码临时路径
/**
* 将文件流生成图片文件
* @param {stream} stream 图片文件流
* @returns {string} 图片ID
*/
function saveImg(stream) {
let id = uuidv4();
fs.writeFileSync(`${tempUrl}\\${id}.png`, stream, "binary");
return id;
}
解析验证码
保存完验证码后,下边要做的就是对图片进行解析了。这块用到了google开发的OCR,关于OCR如何识别验证码有很多文章,这里大概讲一下流程,首先对图片进行去灰度变为单色,然后对图片进行阈值化去噪点,大概的意思就是根据一个设置的阈值,大于阈值的变白,小于阈值的变黑,这个值需要根据图片手动调。最后用OCR工具对生成的最后图片进行图像识别。这块的功能实现不是太好,图片识别成功率很低,也就在10%左右,因此需要配合人工来进行验证码识别。
该功能需要安装三个程序,tesseract-ocr
、ImageMagick
、GraphicsMagick
并修改环境变量,将这三个项目安装路径添加到PATH
环境变量中。安装mageMagick7
的时候需要注意。还需要以来与两个npm库,node-tesr
、gm
。
解析图片核心代码如下:
const tesseract = require("node-tesr");
const gm = require('gm');
/**
* 根据图片地址获取图片中的验证码
* @param {string} imgUrl 图片地址
* @returns {Promise} 获取图片验证码结果
*/
function getOcrReault(imgUrl) {
return new Promise((resolve, reject) => {
let imageMagick = gm.subClass({imageMagick: true})
let tempPath=imgUrl.replace(/(\.png|\.jpg)/g,'_temp$1');
imageMagick(imgUrl)
.colorspace('GRAY')
.monochrome()
.threshold(28, '%')
.write(tempPath, err => {
if (err) {
reject(err);
} else {
tesseract(tempPath, { l: "eng", oem: 3, psm: 7 }, function (err, data) {
if (err) {
reject(err)
console.log(err);
}
data=data.replace(/\s*/g,'');
resolve(data);
});
}
});
});
}
由于验证码解析正确率很低,需要配合手动录入人工识别的结果,后边在集成控制台输入
模块详细介绍。这里边还有一个功能点,获取完图片后需要直接打开,因为我们需要人工干预识别验证码。
const { exec } = require("child_process");
//打开图片
exec(`${tempUrl}\\${imgId}.png`, (err, stdout, stderr) => {
if (err) {
console.log(err);
return;
}
});
登录
得到验证码结果和cookie后再结合已知的用户名密码就能实现登录功能了,通过network
知道登录的地址和参数,下边我们来实现模拟登录功能。
这里我们使用的模块是superagent
/**
* 登录APP
* @param {string} userName 用户名
* @param {string} password 密码
* @param {string} validCode 验证码
* @param {string} cookieStr cookie字符串
* @returns {Promise} 登录结果
*/
function loginApp(userName,password,validCode,cookieStr) {
return new Promise(function (resolve, reject) {
superagent
.post(urls.loginUrl+'?r='+Math.random())
.set('Cookie', cookieStr)
.type('form')
.send({mobile:userName})
.send({password:password})
.send({validCode:validCode})
...
.then(function (response) {
resolve(response);
});
});
}
解析HTML节点
登录成功后在response
中能够找到返回的HTML
,我通过cheerio
工具加载获取到的HTML,然后打开页面找到要获取节点的JS Path
,然后找到该节点中的文本记录下来用于展示。
主要代码如下:
let $ = cheerio.load(loginResult.text); //解析返回的html字符串
let resultText=$("body > ... td:nth-child(7) > span").text();
集成控制台输入
由于验证码识别率不高,并且需要根据选择查询不同的账户,因此考虑通过控制台选择判断如何执行判断下一步要执行的逻辑。通过readline
模块的question
方法来接收用户的输入。由于该方法时通过回调方式来返回结果,考虑到可能会出现回调地狱的现场,想通过同步的方式进行获取输入,从网上找到的方案是使用readline-sync
模块,但是会有中文乱码问题,因此选择其他方案,通过工厂模式创建一个同步的question
方法。
主要代码如下:
//readline实例
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
/**
* 封装为同步工厂方法
* @param {Function} fn
*/
function rlPromisify(fn) {
return async (...args) => {
return new Promise(resolve => fn(...args, resolve));
};
}
//同步question方法
const question = rlPromisify(rl.question.bind(rl));
let isUse = await question("确认是否使用?(y/n) 如果想重新获取验证码请按(r)键:");
bat批处理控制node
以上的一系列操作基本就可以实现识别验证码,登录,获取结果一系列的功能。该功能做完后我希望在桌面上创建一个bat文件来执行查询结果,无需找到项目目录再执行命令。
主要代码如下:
@echo off
start cmd /k "D:&&cd D:\Project\yaohaoSpider&&node index.js"