这段时间迷上了一本漫画ヽ(✿゚▽゚)ノ,百度了一番,一直没有找到下载的资源,只找到几个可以在线观看的网站,不过网站的观看体验是在太差,小广告飘啊飘的(ノへ ̄、),于是决定把它给爬下来放在本地观看。
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);
};
这个是一个控制台输出的库,便于查看任务进度!