手把手教你用node撸一个简易的handless爬虫cli工具

栏目: Node.js · 发布时间: 7年前

内容简介: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下的图片呢?这里我们还是用到了 nodefs 模块,首先利用 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();
}
复制代码

我们引入 commanderinquirerprogram.command 方法是为我们生成命令名的, alias 是该命令的缩写, description 是该命令的描述, action 是该命令要做的事情。

我们首先用 command 生成了两个命令, searchclean ,接着可以看到,我们在 action 中用了 inquirerinquirer 的提问是一个异步的过程,所以我们也一样用了 asyncawaitinquirer 接收一个问题数组,里面包含问题的type、name、message和验证方法等,具体的可以参考inquirer的文档。我们这里的问题如下,这里返回了两个数组,一个是用于输入关键字的时候的,一个是用于清空图片时确认的。提问数组中会验证是否有填写关键字,如果没有,则不会继续下一步并提示你该输入关键字,否则就正式开始爬虫流程。删除确认数组就是简单的一个确认,如果确认了,则开始删除图片。最后,用 program.parse 将命令注入到 nodeprocess.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,
    }
];
复制代码

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Discrete Mathematics and Its Applications

Discrete Mathematics and Its Applications

Kenneth H Rosen / McGraw-Hill Science/Engineering/Math / 2003-04-22 / USD 132.81

Discrete Mathematics and its Applications is a focused introduction to the primary themes in a discrete mathematics course, as introduced through extensive applications, expansive discussion, and deta......一起来看看 《Discrete Mathematics and Its Applications》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

html转js在线工具
html转js在线工具

html转js在线工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具