Nodejs大批量下载图片入坑指南(使用async和bagpipe处理大并发量请求)

前言

故事还得从头说起。乌云网挂掉之后,乌云知识库也无法访问了。曾经,在上面看到那么多优秀的安全类文章,一下子看不到了,颇觉得有点不适应。还好网上流传着民间的各种版本,于是我收集了一下,放在了Github上。这些文章只是一些html文件,并不包含页面上的图片。幸运的是,图片的域名static.wooyun.com还可以继续访问,因此有必要把这些图片也抓取下来。

Wooyun Drops 文章在线浏览

Wooyun Drops 文章在线浏览
Github: wooyun_articles

使用Nodejs下载图片

抓取图片链接的过程在此不再详述,无非就是打开每个html页面,找到其中img标签的src属性。我们拿到了这些html页面的图片链接,是这个样子的:

var imageLinks = [
    'http://static.wooyun.org//drops/20160315/2016031513484536696130.png',
    'http://static.wooyun.org//drops/20160315/2016031513484767273224.png',
    'http://static.wooyun.org//drops/20160315/2016031513485057874324.png',
    'http://static.wooyun.org//drops/20160315/2016031513485211060411.png',
    'http://static.wooyun.org//drops/20160315/201603151348542560759.png',
    'http://static.wooyun.org//drops/20160315/201603151348563741969.png',
    'http://static.wooyun.org//drops/20160315/201603151348582588879.png',
    'http://static.wooyun.org//drops/20160315/201603151349001461288.png',
    'http://static.wooyun.org//drops/20160315/201603151349032455899.png',
    ......
    'http://static.wooyun.org//drops/20150714/2015071404144944570.png',
    'http://static.wooyun.org//drops/20150714/2015071404144966345.png',
    'http://static.wooyun.org//drops/20150714/2015071404144915704.png',
    'http://static.wooyun.org//drops/20150714/2015071404144980399.png',
    'http://static.wooyun.org//drops/20150714/2015071404144927633.png'
];

我们将其定义为一个模块备用(总共大约有13000个图片链接)。

在Nodejs中下载图片有很多解决方案,比如可以使用npm包download。本文中使用比较简单的版本,具体实现如下:

var fs = require("fs");
var path = require('path');
var request = require('request');

var downloadImage = function(src, dest, callback) {
    request.head(src, function(err, res, body) {
        // console.log('content-type:', res.headers['content-type']);
        // console.log('content-length:', res.headers['content-length']);
        if (src) {
            request(src).pipe(fs.createWriteStream(dest)).on('close', function() {
                callback(null, dest);
            });
        }
    });
};

代码来源: stackoverflow

比如想要下载一个图片,可以这样子来做:

downloadImage("http://static.wooyun.org/20140918/2014091811544377515.png", "./1.png", function(err, data){
    if (err) {
        console.log(err)
    }
    if (data) {
        console.log("done: " + data);
    }
})

对于我们想要的批量下载图片,最直观的做法就是写一个循环,然后去调用downloadImage方法。但是由于图片链接比较多,总是在下载到一部分图片后出现一些错误:

events.js:160
      throw er; // Unhandled 'error' event
      ^

Error: Invalid URI "undefined"
    at Request.init (/Users//dirread/node_modules/request/request.js:275:31)
    at new Request (/Users/dirread/node_modules/request/request.js:129:8)
    at request (/Users/dirread/node_modules/request/index.js:55:10)
    at Request._callback (/Users//down.js:21:11)
    at self.callback (/Users//dirread/node_modules/request/request.js:187:22)
    at emitOne (events.js:96:13)
    at Request.emit (events.js:188:7)

因此只有考虑使用异步方式来处理。

解决方案一: 使用async异步批量下载图片

Async是一个流程控制工具包,提供了直接而强大的异步功能。其提供了大约20个函数,包括常用的 map, reduce, filter, forEach 等,异步流程控制模式包括,串行(series),并行(parallel),瀑布(waterfall)等。

安装async

npm install async --save

其使用方式也比较简单,比如下面的示例:

async.map(['file1','file2','file3'], fs.stat, function(err, results){
    // results is now an array of stats for each file
});

async.filter(['file1','file2','file3'], function(filePath, callback) {
  fs.access(filePath, function(err) {
    callback(null, !err)
  });
}, function(err, results){
    // results now equals an array of the existing files
});

async.parallel([
    function(callback){ ... },
    function(callback){ ... }
], function(err, results) {
    // optional callback
};

async.series([
    function(callback){ ... },
    function(callback){ ... }
]);

关于async的示例在此不再展开,具体可以参考alsotang的demo和教程

使用async下载图片

回到我们的程序,我们可以使用来实现。具体代码如下:

async.mapSeries(imageLinks, function(item, callback) {
    //console.log(item);
    setTimeout(function() {
        if (item.indexOf("http://static.wooyun.org") === 0) {
            var destImage = path.resolve("./images/", item.split("/")[item.split("/").length -1]);
            downloadImage(item, destImage, function(err, data){
                console.log("["+ index++ +"]: " + data);
            });
            
        }
        callback(null, item);
    }, 100);


}, function(err, results) {});

完整代码可以参看github上

运行

node down.js

即可看到结果。

踩坑提醒

在Mac上使用的时候,偶尔会出现这样的错误:

Error: EMFILE, too many open files

有可能是因为Mac对并发打开的文件数限制的比较小,一般为256.所以需要修改一下。
在命令行执行:

echo kern.maxfiles=65536 | sudo tee -a /etc/sysctl.conf
echo kern.maxfilesperproc=65536 | sudo tee -a /etc/sysctl.conf
sudo sysctl -w kern.maxfiles=65536
sudo sysctl -w kern.maxfilesperproc=65536
ulimit -n 65536 65536

然后可以在你的.bashrc文件中加入一行:

ulimit -n 65536 65536

解决方案来自stackoverflow.

解决方案二: 使用bagpipe批量下载图片

bagpile是朴灵大大做的一个在nodejs中控制并发执行的模块。

安装bagpipe

其安装和使用也比较简单:

npm install bagpipe --save

使用示例:

var bagpipe = new Bagpipe(10);

var files = ['这里有很多很多文件'];
for (var i = 0; i < files.length; i++) {
  // fs.readFile(files[i], 'utf-8', function (err, data) {
  bagpipe.push(fs.readFile, files[i], 'utf-8', function (err, data) {
    // 不会因为文件描述符过多出错
    // 妥妥的
  });
}

使用bagpipe批量下载图片

将我们的代码稍做修改,就可以使用bagpipe了。具体代码如下:

for (var i = 0; i < imageLinks.length; i++) {
    if (imageLinks[i].indexOf("http://static.wooyun.org") === 0) {
        var destImage = path.resolve("./images/", imageLinks[i].split("/")[imageLinks[i].split("/").length -1]);
        bagpipe.push(downloadImage, imageLinks[i], destImage, function(err, data) {
            console.log("["+ index++ +"]: " + data);
        });
    }
}

完整代码可以参考github上

运行

node down.js

即可看到结果。

结语

async和bagpipe都是很优秀的nodejs包,本身async功能十分强大,bagpipe使用起来简单方便,对原有的异步代码几乎不必做太多改动。因此可以根据自己喜好选择使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,857评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,066评论 4 62
  • 这么帅气的人儿大家肯定都认识吧,没错,他就是郑凯,随着第三季跑男的结束,他也与跑男一起走过了三个季度,他,没有邓叔...
    coffee漫阅读 307评论 2 2
  • subvention(subsidy:补贴a grant, aid, or subsidy, as from a ...
    相关知情人士阅读 620评论 0 0
  • 从夜市上买回了衣服和芭比娃娃。在挑选衣服时,起初看到了一件全棉的连衣裙,不论是款式、质地还是价格都符合内心的标准。...
    云淡风轻66阅读 424评论 0 0