JustYeh的前端博客

使用puppeteer做爬虫的一次实践

2019-07-09

爬虫puppeteer

这段时间迷上了一本漫画ヽ(✿゚▽゚)ノ,百度了一番,一直没有找到下载的资源,只找到几个可以在线观看的网站,不过网站的观看体验是在太差,小广告飘啊飘的(ノへ ̄、),于是决定把它给爬下来放在本地观看。

nodegrass + cheerio

在这之前我有过写爬虫的体验(使用 nodejs 做爬虫来爬取一本小说),按照以前的经验,选定一个网站就开始操作了,经过漫长下载(5000+张图片)终于完成,结果看了几话后发现这网站里面的数据是不完整的,存在许多图片缺失的情况,CTMD!

puppeteer

吸取上面的教训后开始仔细甄别数据来源,总算找到一个数据完整的网站,不过也遇到一个难题,这个网站的图片地址是通过一段 js 动态生成的,比较恶心的是这 js 混淆的比较厉害,自己尝试了去解析一下,不过这活是在难搞,也就放弃了。

于是我开始另外寻找思路:作为前端码畜,想一想 SPA 是怎么被爬取的呢?Headless 无头浏览器啊。

一开始我找到的是 PhantomJS,试了一下发现并不好用:文档不好用、而且好像也不维护了。问了一下群里的老哥,发现了 puppeteer 这个神器(只要浏览器能操作的,puppeteer都能帮你模拟,还有截屏、设备模拟等使用功能),下面我就结合代码,说明一下使用经验。

app.js

const getChapterImageSrc = require("./src/getChapterImageSrc");
const download = require("./src/download");
async function init() {
    let list = await getChapterImageSrc("xxx");
    list.forEach(async (item,itemIndex) => {
        await download({
            image: item,
            index: itemIndex+1
            dir: "xxxx"
        });
    });
}
init();

上面的代码中 download 我并没有做并行处理,主要原因是单个任务下载完成后我会手动检查一下数据是否完整,必要的时候可以切断任务,但保证任务顺序是可控的!

getChapterImageSrc.js

const axios = require("axios");
const cheerio = require("cheerio");
const pb = require("./progress-bar")("获取图片地址");
const puppeteer = require("puppeteer");
const path = require("path");

module.exports = async baseUrl => {
    try {
        let res = await axios.get(baseUrl);
        if (res) {
            let brower = await puppeteer.launch({
                headless: false,
                devtools: true,
                args: [
                    //"--proxy-server='direct://'",
                    //"--proxy-bypass-list=*",
                    "--no-sandbox",
                    "--disable-setuid-sandbox",
                    "--enable-features=NetworkService"
                ],
                // args: [`--proxy-server="socks5://127.0.0.1:1080"` '--no-sandbox', '--disable-setuid-sandbox', '--enable-features=NetworkService'],
                executablePath: path.resolve(
                    "C:/Users/54657645/AppData/Local/Google/Chrome/Application/chrome.exe"
                )
            });

            let $ = cheerio.load(res.data);
            let pageTotal = $(".manga-page").text();
            pageTotal = parseInt(pageTotal.slice(1, pageTotal.length));
            let srcList = [];
            for (let i = 1; i <= pageTotal; i++) {
                let page = await brower.newPage();
                /* await page.setRequestInterception(true);
                page.on("request", async request => {
                    let resourceType = request.resourceType();
                    if (["image", "stylesheet"].includes(resourceType)) {
                        request.abort();
                    } else {
                        request.continue();
                    }
                }); */
                await page.goto(`` || `${baseUrl}?p=${i}`);
                html = await page.content();
                let src =
                    cheerio
                        .load(html)("img#manga")
                        .attr("src") || "";
                pb.render({ completed: i, total: pageTotal });
                srcList.push(src);
            }
            await brower.close();
            return srcList;
        }
    } catch (error) {
        console.log(error);
        return false;
    }
};

这个代码就是本文的重点了:

line2:cheerio 可以使 node 以 jquery 的方式解析 html,便于我们分析提取 html 内容

line11-25:实例化一个浏览器对象

  • headless:是否已无头浏览器方式启动,false 会打开一个浏览器
  • args:浏览器启动的参数
  • executablePath:启动的浏览器的地址,默认是库中的 chromium,这里指定为系统安装的 chrome

line27-29:获取下载任务量(有多少张图片)

line32:打开一个浏览器页面

line33-41:这里是一个拦截器,在拦截器中我们终止了 css 和图片请求,目的是加快页面载入速度。不过这里我遇到了一个问题,只要打开了这个拦截器就会造成某个页面一直处于加载状态,导致 page 超时,最终我将这里注释掉了,还有待进一步研究!

line42-47:设置页面 url,并获取页面 html 内容,最终得到图片的地址

download.js

const fse = require("fs-extra");
const request = require("request");

module.exports = task => {
    return new Promise((resolve, reject) => {
        let splitUrl = task.image.split("/");
        let name = splitUrl[splitUrl.length - 1];
        let writeStream = fse.createWriteStream(
            `./${task.dir}/${task.index}-${name}`,
            {
                autoClose: true
            }
        );
        let readStream = request(encodeURI(task.image));
        readStream.pipe(writeStream);
        readStream.on("error", error => {
            resolve(false);
        });
        writeStream.on("finish", () => {
            resolve(true);
        });
        writeStream.on("error", error => {
            resolve(false);
        });
    });
};

这个是下载图片的代码,通过管道的方式用 fs 模块将图片写到本地目录

process-bar.js

// 这里用到一个很实用的 npm 模块,用以在同一行打印文本
var slog = require("single-line-log").stdout;

// 封装的 ProgressBar 工具
function ProgressBar(description, bar_length) {
// 两个基本参数(属性)
this.description = description || "Progress"; // 命令行开头的文字信息
this.length = bar_length || 25; // 进度条的长度(单位:字符),默认设为 25

    // 刷新进度条图案、文字的方法
    this.render = function(opts) {
        var percent = (opts.completed / opts.total).toFixed(4); // 计算进度(子任务的 完成数 除以 总数)
        var cell_num = Math.floor(percent * this.length); // 计算需要多少个 █ 符号来拼凑图案

        // 拼接黑色条
        var cell = "";
        for (var i = 0; i < cell_num; i++) {
            cell += "█";
        }

        // 拼接灰色条
        var empty = "";
        for (var i = 0; i < this.length - cell_num; i++) {
            empty += "░";
        }

        // 拼接最终文本
        var cmdText =
            this.description +
            ": " +
            (100 * percent).toFixed(2) +
            "% " +
            cell +
            empty +
            " " +
            opts.completed +
            "/" +
            opts.total;

        // 在单行输出文本
        slog(cmdText);
    };

}

// 模块导出
module.exports = text => {
return new ProgressBar(text, 50);
};

这个是一个控制台输出的库,便于查看任务进度!

参考链接

官网 https://pptr.dev/

中文文档 https://zhaoqize.github.io/puppeteer-api-zh_CN/

github https://github.com/GoogleChrome/puppeteer