记一次略坑的过程。 源码地址
最近刚好同学毕设在做爬虫,看了一下,如果不是搞得太复杂的话主要只是写些正则,于是我尝试写个Node版的简单点的,很喜欢逛花瓣网,有时想批量下载,但是却没办法下载整个画板的图片,因此不妨把喜欢的画板图片爬下来并一次性下载到本地。做个功能给自己用也好。
首先是找到任意画板的地址,于是我随意找了个宫崎骏的画板进去,http://huaban.com/boards/25498000/,其实多研究几个url,观察其区别,不难发现,这个url对我们的有效信息就是25498000
,也就是画板ID。
然而当我打开源代码查看时,什么鬼,网页全是用js渲染的,不能用cheerio
来解决问题了,只能想一下有什么捷径,看到有可以用Phantom.js
,感觉比较麻烦,我只是想下载图片而已,继续研究下数据看看是否有必要再说。
于是我打开Chrome控制台,可以发现,当我们点开画板中的图片或者下拉加载更多时,ajax会去请求这么一个url:http://huaban.com/boards/25498000/?iwlq03dw&max=580817693&limit=20&wfl=1,这个请求对应的响应信息中pins
字段下有画板中20张图片的信息,并且是规规则则的js对象,很明显url中limit=20
就是获取前20张图片的意思,我们可以改变这个值来获取,而pin_count
:408
就是这个画板的总图片数量。感觉还是勉强可以通过正则来取出我们要的json
数据的。
于是就试了下,先把数据打印出来看看
var http = require('http');
var url = 'http://huaban.com/boards/25498000/?iwlq03dw&max=580817693&limit=20&wfl=1';
http.get(url, function(res) {
var html = '';
res.on('data', function(data) {
html += data;
});
res.on('end', function() {
console.log(html)
});
});
找到我要的信息是放在app.page["board"]
,于是我从这里开始截取,截取到对象的结束处,对象结束处的标志是};
的出现,于是构造成json数据。
/**
* 取到画板数据
*/
var getBoardObj = function(html) {
var board = /(app\.page\["board"\]).*};/.exec(html)[0];
board = board.substring(17, board.length - 1).trim().substring(1);
return JSON.parse(board);
};
尝试打印出来,并替换多个url测试,最终确认是可行的,感觉快可以实现了。
但是经过一番研究,发现当把limit
的值变成画板总图片数量时并不能获取到所有数据,经测试花瓣网限制住了只能获取到100张,路又走不通了。
最终经过重重失败和测试,终于发现max是上一页的最后一张图片的id,而那个没有值的参数iwlq03dw
是会变的,最后一位从a-z再从0-9重复循环,可能只是个标识吧,无法知道具体是做什么用的,但发现不影响得到的数据。
于是就可以构造加载更多的url了。
/**
* 加载更多
*/
var loadMore = function(url) {
var nextUrl = url.replace(/max=\d*&/, 'max=' + images[images.length - 1].pin_id + '&');
fetchData(nextUrl, downloadAll);
};
发现这些规律后好做多了,然而新的问题又来了,我们应该从哪一张图片开始获取呢?我所取到的url是加载第二页的数据的url,从这个url作为入口去获取只能获取到从第二页到最后一页的数据,第一页丢失了,又把自己带坑里了。折腾一番之后,猜测后台可能的实现逻辑,假如这个字段为空,一般我们写后台会从第一页数据开始获取吧,于是测试下,果然,发现不填max值也可以获取数据,并且是从第一页,感觉又可以有追求了。
程序实现思路:先分页请求,把图片全部取到,放到images数组里面,然后再下载images数组中的图片。
下载的时候使用async
模块来控制并发,一次只给下载3张,每下载完成一张则callback一次,如果没控制的话,下载太多图最后会一直等待没响应。
async.mapLimit(images, 3, function(image, callback) {
// 下载
download(image, callback);
}, function (err, result) {
console.log('下载完成情况:' + result);
});
下载方法的实现
var ws = fs.createWriteStream(filePath);
ws.on('finish', function() {
console.log('' + filename + ' 已下载');
callback(null, filename + '下载成功');
});
http.get(imgUrl, function(res) {
res.pipe(ws);
}).on('finish', function() {
console.log('http请求完成: ', imgUrl);
}).on('error', function() {
console.log('error');
});
最终程序
& 源码地址 爬取花瓣网画板图片
var http = require('http'),
fs = require('fs'),
async = require('async');
var url = 'http://huaban.com/boards/155643/?ip44g0nc&max=&limit=20&wfl=1',
imageUrlBase = 'http://img.hb.aicdn.com/',
downloadPath = 'download/';
// 保存所有图片
var images = [],
// 图片类型
imagesTypes = {
'image/png': '.png',
'image/jpeg': '.jpg',
'image/bmp': '.bmp',
'image/gif': '.gif',
'image/x-icon': '.ico',
'image/tiff': '.tif',
'image/vnd.wap.wbmp': '.wbmp'
};
/**
* 获取花瓣网数据
*/
var fetchData = function(url, callback) {
console.log('开始抓取花瓣网图片地址');
// 爬取数据
http.get(url, function(res) {
var html = '';
res.on('data', function(data) {
html += data;
});
res.on('end', function() {
// 取到画板数据
var board = getBoardObj(html);
var pins = board.pins;
images = images.concat(pins);
// 画板图片总数量
var count = board.pin_count;
console.log('已抓取到' + board.pins.length + '张图片的地址');
if (images.length == count || pins.length == 0) {
// 停止抓取
console.log('抓取结束,即将下载' + images.length + '张图片');
callback && callback();
return;
} else {
// 加载更多
loadMore(url);
}
});
}).on('error', function() {
console.log('error');
});
};
/**
* 取到画板数据
* @return {[type]} [description]
*/
var getBoardObj = function(html) {
var board = /(app\.page\["board"\]).*};/.exec(html)[0];
board = board.substring(17, board.length - 1).trim().substring(1);
return JSON.parse(board);
};
/**
* 加载更多
*/
var loadMore = function(url) {
var nextUrl = url.replace(/max=\d*&/, 'max=' + images[images.length - 1].pin_id + '&');
fetchData(nextUrl, downloadAll);
};
var downloadAll = function() {
// 创建名为画板ID的文件夹
downloadPath += images[0].board_id + '/';
if(!fs.existsSync(downloadPath)) {
fs.mkdirSync(downloadPath);
}
async.mapLimit(images, 3, function(image, callback) {
// 下载
download(image, callback);
}, function (err, result) {
console.log('下载完成情况:' + result);
});
};
/**
* 下载图片
*/
var downloadCount = 0;
var download = function(image, callback) {
var imgUrl = imageUrlBase + image.file.key;
var filename = image.file.id + (imagesTypes[image.file.type] || '.jpg');
var filePath = downloadPath + filename;
if (fs.existsSync(filePath)) {
console.log('图片 ', filePath, ' 已存在');
++downloadCount;
callback(null, '图片已存在');
} else {
var ws = fs.createWriteStream(filePath);
ws.on('finish', function() {
console.log('' , filename, ' 已下载,总下载进度', 100 * (++downloadCount / images.length).toFixed(2), '%');
callback(null, filename + '下载成功');
});
http.get(imgUrl, function(res) {
res.pipe(ws);
}).on('finish', function() {
console.log('http请求完成: ', imgUrl);
}).on('error', function() {
console.log('error');
});
}
};
// begin
fetchData(url, downloadAll);
下载其它画板的话,将画板ID替换就可以了。
注:图片太大or太多会导致下载较慢。