众所周知,node功能很强大,为前端提供了更多的可能。今天,就跟你们分享一下我是如何用node写一个headless爬虫的。原文连接leeing.site/2018/10/17/…前端
下面就给你们讲一下这些工具都有什么做用node
headless爬虫主要靠它。它能够模拟用户打开网页的过程,可是并无打开网页。写过自动化测试的同窗应该对这个会比较熟悉,由于用它爬虫的过程跟自动化测试的过程几乎是同样的。git
基于node的cli命令行工具。利用它,咱们能够很方便的写出各类各样的cli命令。github
交互式命令行工具。什么叫作交互式命令行呢?其实就是相似npm init的时候,问一个问题,咱们答一个问题,最后根据答案生成package.json的过程。npm
这个其实就是一个让咱们在命令行中输出的文字更加优美的工具。json
好了,介绍完了工具之后,让咱们正式开始咱们的项目。数组
首先,要搞清楚咱们想要实现的功能。咱们想要实现的功能就是,在命令行中输入咱们想要下载的图片,而后node去网上爬取咱们想要的图片(这里就先去百度图片爬吧),直接下载到本地。以及输入一个命令,能够清空咱们输出目录中的图片。promise
|-- Documents
|-- .gitignore
|-- README.md
|-- package.json
|-- bin
| |-- gp
|-- output
| |-- .gitkeeper
|-- src
|-- app.js
|-- clean.js
|-- index.js
|-- config
| |-- default.js
|-- helper
|-- questions.js
|-- regMap.js
|-- srcToImg.js
复制代码
以上是项目用到的一个简单的目录结构浏览器
首先咱们看一下app.js。bash
咱们用一个类包裹核心方法,是为了命令行工具能够更方便的调用咱们的方法。
这个类很简单,constructor
接收参数,start
开启主要流程。 start
方法是一个async函数,由于puppeteer
操做浏览器的过程几乎都是异步的。
接着咱们用puppeteer
生成page的实例,利用goto
方法模拟进入百度图片页面。这时其实就是跟咱们真实打开浏览器进入百度图片是同样的,只不过由于咱们是headless的,因此咱们没法感知打开浏览器的过程。
而后咱们须要设置一下浏览器的宽度(想象一下),不能太大,也不能过小。太大会触发百度反爬虫机制,致使咱们爬下来的图片是403或者别的错误。过小会致使爬到的图片很是少。
接下去咱们聚焦搜索框,输入咱们想要搜索的关键字(这个关键字呢就是咱们在命令行输入的关键字),而后点击搜索。
等页面加载之后,咱们用page.$$eval
获取页面上全部class
为.main_img
的图片(具体规律须要本身去观察),再获取上面的src
属性后,将src
转为咱们本地的图片。
到这里,app.js的任务就完成了。 很简单吧。
下面是代码。
const puppeteer = require('puppeteer');
const chalk = require('chalk');
const config = require('./config/default');
const srcToImg = require('./helper/srcToImg');
class App {
constructor(conf) {
//有传入的参数既用传入的参数,没有既用默认的参数
this.conf = Object.assign({}, config, conf);
}
async start () {
//用puppeteer生成一个browser的实例
//用browser再生成一个page的实例
const browser = await puppeteer.launch();
const page = await browser.newPage();
//打开搜索引擎,先写死百度
await page.goto(this.conf.searchPath);
console.log(chalk.green(`go to ${this.conf.searchPath}`));
//设置窗口大小,过大会引发反爬虫
await page.setViewport({
width: 1920,
height: 700
});
//搜索文字输入框聚焦
await page.focus('#kw');
//输入要搜索的关键字
await page.keyboard.sendCharacter(this.conf.keyword);
//点击搜索
await page.click('.s_search');
console.log(chalk.green(`get start searching pictures`));
//页面加载后要作的事
page.on('load', async () => {
console.log(chalk.green(`searching pictures done, start fetch...`));
//获取全部指定图片的src
const srcs = await page.$$eval('img.main_img', pictures => {
return pictures.map(img => img.src);
});
console.log(chalk.green(`get ${srcs.length} pictures, start download`));
srcs.forEach(async (src) => {
await page.waitFor(200);
await srcToImg(src, this.conf.outputPath);
});
});
}
};
module.exports = App;
复制代码
接下来咱们看一下,如何把图片的src属性转化为咱们本地的图片呢?咱们看下helper下的srcToImg.js
首先,这个模块主要引入了node的http
模块、https
模块、path
模块和fs
模块及一些辅助工具,好比正则、将回调函数转化为promise的promisify
和将输出更好看的chalk
。
为何咱们要同时引入http和https模块呢?仔细观察百度图片搜索结果中的图片,咱们能够发现,既有http的也有https的,因此咱们引入两个模块,区分出具体的图片属于哪一个就用哪一个模块去请求图片。请求了图片之后,咱们就用fs
模块的createWriteStream
方法,将图片存入咱们的output
目录中。
若是咱们仔细观察了百度搜索结果中的图片的src,咱们会发现,除了http和https开头的图片,还有base64的图片,因此咱们要对base64的图片也作一下处理。
跟普通图片同样的处理,先根据src
分割出扩展名,再计算出存储的路径和文件名,最后写入调用fs
模块的writeFile
方法写入文件(这里就简单的用writeFile了)。
以上,图片就存入本地了。
代码以下。
const http = require('http');
const https = require('https');
const path = require('path');
const fs = require('fs');
const { promisify } = require('util');
const chalk = require('chalk');
const writeFile = promisify(fs.writeFile);
const regMap = require('./regMap');
const urlToImg = promisify((url, dir) => {
let mod;
if(regMap.isHttp.test(url)){
mod = http;
}else if(regMap.isHttps.test(url)){
mod = https;
}
//获取图片的扩展名
const ext = path.extname(url);
//拼接图片存储的路径和扩展名
const file = path.join(dir, `${parseInt(Math.random() * 1000000)}${ext}`);
mod.get(url, res => {
//采用stream的形式,比直接写入更快捷
res.pipe(fs.createWriteStream(file)).on('finish', () => {
console.log(file);
});
});
});
const base64ToImg = async (base64Str, dir) => {
const matchs = base64Str.match(regMap.isBase64);
try {
const ext = matchs[1].split('/')[1].replace('jpeg', 'jpg');
const file = path.join(dir, `${parseInt(Math.random() * 1000000)}.${ext}`);
await writeFile(file, matchs[2], 'base64');
console.log(file);
} catch (error) {
console.log(chalk.red('没法识别的图片'));
}
};
module.exports = (src, dir) => {
if(regMap.isPic.test(src)){
urlToImg(src, dir);
}else{
base64ToImg(src, dir);
}
};
复制代码
咱们再看一下如何清空output下的图片呢? 这里咱们仍是用到了node
的fs
模块,首先利用fs.readdir
方法读取output
文件夹,而后遍历其下的文件,若是是图片,则调用fs.unlink
方法删除它。也很简单,对吧。
代码以下
const fs = require('fs');
const regMap = require('./helper/regMap');
const config = require('./config/default');
const cleanPath = config.outputPath;
class Clean {
constructor() {}
clean() {
fs.readdir(cleanPath, (err, files) => {
if(err){
throw err;
}
files.forEach(file => {
if(regMap.isPic.test(file)){
const img = `${cleanPath}/${file}`;
fs.unlink(img, (e) => {
if(e) {
throw e;
}
});
}
});
console.log('clean finished');
});
}
};
module.exports = Clean;
复制代码
最后咱们看一下如何写cli工具呢? 首先咱们须要在bin
目录下新建一个脚本文件gp
,以下
#! /usr/bin/env node
module.exports = require('../src/index');
复制代码
意思是找到/usr/bin/env
下的node
来启动第二行的代码
其次咱们须要在package.json里加入一个bin
对象,对象下属性名是咱们命令的名字,属性是bin
下的脚本文件的路径,以下
"bin": {
"gp": "bin/gp"
}
复制代码
接着咱们来看下index.js
const program = require('commander');
const inquirer = require('inquirer');
const pkg = require('../package.json');
const qs = require('./helper/questions');
const App = require('./app');
const Clean = require('./clean');
program
.version(pkg.version, '-v, --version');
program
.command('search')
.alias('s')
.description('get search pictures what you want.')
.action(async () => {
const answers = await inquirer.prompt(qs.startQuestions);
const app = new App(answers);
await app.start();
});
program
.command('clean')
.alias('c')
.description('clean all pictures in directory "output".')
.action(async () => {
const answers = await inquirer.prompt(qs.confirmClean);
const clean = new Clean();
answers.isRemove && await clean.clean();
});
program.parse(process.argv);
if(process.argv.length < 3){
program.help();
}
复制代码
咱们引入commander
和inquirer
,program.command
方法是为咱们生成命令名的,alias
是该命令的缩写,description
是该命令的描述,action
是该命令要作的事情。
咱们首先用command
生成了两个命令,search
和clean
,接着能够看到,咱们在action
中用了inquirer
,inquirer
的提问是一个异步的过程,因此咱们也同样用了async
和await
,inquirer
接收一个问题数组,里面包含问题的type、name、message和验证方法等,具体的能够参考inquirer的文档。咱们这里的问题以下,这里返回了两个数组,一个是用于输入关键字的时候的,一个是用于清空图片时确认的。提问数组中会验证是否有填写关键字,若是没有,则不会继续下一步并提示你该输入关键字,不然就正式开始爬虫流程。删除确认数组就是简单的一个确认,若是确认了,则开始删除图片。最后,用program.parse
将命令注入到node
的process.argv
中,根据命令行有没有输入参数提示help信息。
至此,咱们的程序大功告成。接下去咱们只要将咱们的程序发布到npm里,就可让其余人下载来使用了~npm的发布咱们这里就再也不赘述啦,不清楚的同窗网上随便搜一下就ok啦。
src/helper/questions.js
以下
const config = require('../config/default');
exports.startQuestions = [
{
type: 'input',
name: 'keyword',
message: 'What pictures do yo want to get ?',
validate: function(keyword) {
const done = this.async();
if(keyword === ''){
done('Please enter the keyword to get pictures');
return;
}
done(null, true);
}
}
];
exports.confirmClean = [
{
type: 'confirm',
name: 'isRemove',
message: `Do you want to remove all pictures in ${config.outputPath} ?`,
default: true,
}
];
复制代码
npm i get_picture -g