手把手教你用node擼一個簡易的headless爬蟲cli工具

https://user-gold-cdn.xitu.io/2018/10/17/16681b5b59f749e0?w=730&h=410&f=jpeg&s=162158

衆所周知,node功能很強大,爲前端提供了更多的可能。今天,就跟你們分享一下我是如何用node寫一個headless爬蟲的。原文連接leeing.site/2018/10/17/…前端

用到的工具

  • puppeteer
  • commander
  • inquirer
  • chalk

下面就給你們講一下這些工具都有什麼做用node

puppeteer

headless爬蟲主要靠它。它能夠模擬用戶打開網頁的過程,可是並無打開網頁。寫過自動化測試的同窗應該對這個會比較熟悉,由於用它爬蟲的過程跟自動化測試的過程幾乎是同樣的。git

commander

基於node的cli命令行工具。利用它,咱們能夠很方便的寫出各類各樣的cli命令。github

inquirer

交互式命令行工具。什麼叫作交互式命令行呢?其實就是相似npm init的時候,問一個問題,咱們答一個問題,最後根據答案生成package.json的過程。npm

chalk

這個其實就是一個讓咱們在命令行中輸出的文字更加優美的工具。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
複製代碼

以上是項目用到的一個簡單的目錄結構瀏覽器

  • output 用以存放下載的圖片
  • bin cli工具會用到的文件
  • src 代碼主要存放於此
    • index.js 項目入口文件
    • app.js 主要功能文件
    • clean.js 用於清空圖片操做的文件
    • config 用於存放一些配置
    • helper 用於存放一些輔助方法的文件

開始項目

首先咱們看一下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下的圖片呢? 這裏咱們仍是用到了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,
    }
];
複製代碼

項目下載

npm i get_picture -g

參考連接

相關文章
相關標籤/搜索