记一次略坑的过程。 源码地址

最近刚好同学毕设在做爬虫,看了一下,如果不是搞得太复杂的话主要只是写些正则,于是我尝试写个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太多会导致下载较慢。