内容简介:handless爬虫主要靠它。它可以模拟用户打开网页的过程,但是并没有打开网页。写过自动化测试的同学应该对这个会比较熟悉,因为用它爬虫的过程跟自动化测试的过程几乎是一样的。基于node的cli命令行工具。利用它,我们可以很方便的写出各种各样的cli命令。交互式命令行工具。什么叫做交互式命令行呢?其实就是类似npm init的时候,问一个问题,我们答一个问题,最后根据答案生成package.json的过程。
handless爬虫主要靠它。它可以模拟用户打开网页的过程,但是并没有打开网页。写过自动化测试的同学应该对这个会比较熟悉,因为用它爬虫的过程跟自动化测试的过程几乎是一样的。
commander
基于node的cli命令行工具。利用它,我们可以很方便的写出各种各样的cli命令。
inquirer
交互式命令行工具。什么叫做交互式命令行呢?其实就是类似npm init的时候,问一个问题,我们答一个问题,最后根据答案生成package.json的过程。
chalk
这个其实就是一个让我们在命令行中输出的文字更加优美的工具。
好了,介绍完了 工具 以后,让我们正式开始我们的项目。
项目介绍
首先,要搞清楚我们想要实现的功能。我们想要实现的功能就是,在 命令行 中输入我们想要下载的图片,然后node去网上爬取我们想要的图片(这里就先去百度图片爬吧),直接下载到本地。以及输入一个命令,可以清空我们输出目录中的图片。
文件目录
|-- 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 复制代码
以上是项目用到的一个简单的目录结构
- output 用以存放下载的图片
- bin cli工具会用到的文件
-
src
代码主要存放于此
- index.js 项目入口文件
- app.js 主要功能文件
- clean.js 用于清空图片操作的文件
- config 用于存放一些配置
- helper 用于存放一些辅助方法的文件
开始项目
首先我们看一下app.js。
我们用一个类包裹核心方法,是为了命令行工具可以更方便的调用我们的方法。
这个类很简单, constructor
接收参数, start
开启主要流程。 start
方法是一个async函数,因为 puppeteer
操作浏览器的过程几乎都是异步的。
接着我们用 puppeteer
生成page的实例,利用 goto
方法模拟进入百度图片页面。这时其实就是跟我们真实打开浏览器进入百度图片是一样的,只不过因为我们是handless的,所以我们无法感知打开浏览器的过程。
然后我们需要设置一下浏览器的宽度(想象一下),不能太大,也不能太小。太大会触发百度反爬虫机制,导致我们爬下来的图片是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, } ]; 复制代码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。