基于Nodejs爬虫简单对比Callback、Promise与Async

爱好三维立体图多年,近期打算将网络上能找到的资源收集一下。本着“偷懒至上”的原则,写一简单爬虫脚本解放手指。作为前端狗,不敢忘本职工作。于是一式三份,分别用CallbackPromiseAsync实现一遍,权当学习ES6/7了。源码戳我

callback形式

  • 目标网站:http://www.3wtu.com/
  • 流程简述:
    图片url分别存储在http://www.3wtu.com/picture/${i}.html (9 < i <183)这些网页。首先遍历这些网址,分别执行获取图片url => 获取图片数据 => 保存至本地。
  • 不相关技术点:编码转换。

1. 取图片链接

首先我们封装一个单次请求的方法。由于我们的目标网站使用的gb2312的编码,因此我引入iconv模块用来解码。注意,不可用chunk += chunk 取代chunks.push(chunk),前者隐含了操作chunk += chunk.toString('utf8') 详见github.com/ashtuchkin/iconv-lite
了解Nodejs的朋友应该对cheerio模块不会陌生,它相当于一个服务端的JQuery。

const http = require("http")
const fs = require("fs")
const cheerio = require("cheerio")
const iconv = require('iconv-lite')

var domian = 'http://www.3wtu.com'
var config = {
    dirPath: __dirname + '/' + 'imagesByNormal/',  // 图片存储目录
    interval: 300,  // 单次请求的时间间隔
}

function getPicsUrl(url, callback) {
    http.get(url, function(res) {
        var chunks = []

        res.on("data" ,function(chunk) {
            chunks.push(chunk)
        })

        res.on("end",function() {
            // 转编码后的html
            var decodedBody = iconv.decode(Buffer.concat(chunks), 'gb2312')

            // 服务端版本的JQuery
            var $ = cheerio.load(decodedBody, { decodeEntities: false })

            // 图片的绝对地址
            var pic = domian + $('.detailed-pic img').attr('src')

            // 图片名字
            var name = $('.detailed-title h4').html()

            callback({ url: pic, name: name })
        })

    })
}

2. 请求图片数据

拿到图片的链接之后,我们就需要请求图片的数据。

function getPicData(pic, callback) {

    // 文件类型后缀名
    var fileType = pic.url.split('.').pop()

    // 命名时带上3位时间戳,降低重名的概率
    var diff = new Date().getTime().toString().substring(10)

    // 图片路径与名字
    var name = config.dirPath + pic.name + '#' + diff + '.' + fileType

    // 请求图片数据
    http.get(pic.url, function(res) {
        var data = ''
        res.setEncoding('binary')

        res.on('data', function(chunk) {
            data += chunk
        })

        res.on('end', function() {
            callback(data, name)
        })
    })
}

3. File System 下载图片至本地

最后把图片存储到我们的本地。

function download(data, name, callback) {
    fs.writeFile(name, data, 'binary', callback)
}

4.启动

以上三步就是针对目标网站将一张图片爬下来的全部过程。
现在我们只要启动遍历所有的目标网站即可

for (var i = 10; i < 183; i++) {
    (function (index) {
        var interval = (index - 10) * config.interval + Math.random() * 100
        var url = 'http://www.3wtu.com/picture/' + index + '.html'

        setTimeout(function () {  // 等待,防止请求太快
            getPicsUrl(url, function(picLink) {  // 网页 => 图片url
                getPicData(picLink, function (picData) {  // 图片url => 图片数据
                    download(picData.data, picData.name, function (err) {  // 图片数据 => 本地图片
                        if (err) {
                            console.log(err)
                        } else {
                            console.log(picData.name + ' downloaded successfully')
                        }
                    }) 
                })
            })
        }, interval)

    })(i)
}

以上代码重点看setTimeoutgetPicsUrlgetPicDatadownload四连回调。这样写代码是不是特别地不舒服呢?如果再多几个嵌套回调,代码的可读性就会非常差。感受到了痛点,才能更好的理解“我们需要一些新东西取解决痛点”。而新东西就是指PromiseAsync

Promise与Async

本文主要目的在于结合实例阐述PromiseAsync对开发效率及体验的友好度。如果一点都不了解Promise的朋友,可以先看看阮一峰老师的ES6标准入门
另外,这里将Promise和Async放在一起,就是希望大家不要把两者对立起来。
Promise本身是用于封装异步操作,同时提供了流程控制的API。而Async 函数只是对异步操作的流程控制,比Promise更加的直观和简洁,进一步提高了代码可读性。如果读到这句话一点概念也没有,可以先戳这里Generator,都说AsyncGenerator的语法糖,学习Async之前还是需要对Generator有一定了解的。(其实我觉得语法糖这个说法不太好,Class那种东西才是纯粹的语法糖好么?)



1. 将异步请求封装成Promise

首先我们需要把上述的异步函数封装成Promise

  • 定时器
function _setTimeout(i) {
    var interval = i * config.interval + Math.random() * 100
    return new Promise(resolve => {
        setTimeout(() => {
            resolve()
        }, interval)
    })
}
  • 获取图片url数组
function getPicsUrl(url) {
    console.log(`开始向${url}请求图片地址...`)
    var html = ''
    return new Promise(resolve => {
        http.get(url, res => {
            res.on('data', data => { html += data })
            res.on('end', data => {
                var $ = cheerio.load(html)
                var $pics = $('#artContent img')
                var pics = [].slice.call($pics).map(pic => {
                    return pic.attribs.src
                })

                console.log(`图片链接获取完毕,共${pics.length}张图片。`)
                resolve(pics)
            })
        })
    })
}
  • 获取图片数据
    以下这段代码运用到了设置header模拟浏览器,我对这方面并无过多的了解,仅仅是针对需求而解决问题一种方案。就不过多解释了。(基础的http常识是必须具备的,只是应用层面上各取所需就好,学习是需要成本的,应该珍惜时间才对)
function getPicData(urlStr) {
    var name = urlStr.substring(56)
    var urlJson = url.parse(urlStr)
    var data = ''

    var option = {
        hostname: urlJson.hostname,
        path: urlJson.pathname,
        // 对方网站有限制爬虫,需要设置header模拟浏览器
        headers: {
            "User-Agent": `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36`,
            "Referer":  `http://www.360doc.com/content/13/0905/08/11561215_312316659.shtml`,
        }
    }

    return new Promise(resolve => {
        http.get(option, res => {
            res.setEncoding('binary')
            res.on('data', chunk => { data += chunk })
            res.on('end', () => {
                resolve({name, data})
            })
        })
    })
}
  • 下载图片至本地
function download(pic) {
    return new Promise(resolve => {
        fs.writeFile(config.dirPath + pic.name, pic.data, 'binary', err => {
            resolve({ err: err, name: pic.name })
        })
    })
}

2. Promise的流程控制

是不是很熟悉JQuery return this的链式操作?Promise的流程控制就特别相似,这样写代码是不是比嵌套回调舒服很多吧?

getPicsUrl(domain).then(picsArr => {  // 获取图片url数组
    for (let i = 0; i < picsArr.length; i++) {
        _setTimeout(i)  // 定时器等待
        .then(() => getPicData(picsArr[i]))  // 获取图片数据
        .then(pic => download(pic))  // 下载图片至本地
        .then(resJson => {
            console.log(resJson.err || `${resJson.name} downloaded successfully`)
        })
    }
})

3. Async的流程控制

是不是已经非常接近同步代码了?毕竟号称异步编程之终极方案的~

async function crawler() {
    if ( !isExit(config.dirPath) ) {
        fs.mkdirSync(config.dirPath)
    }
    var picsArr = await getPicsUrl(domain)  // 获取图片url数组
    for (let i = 0; i < picsArr.length; i++) {
        await _setTimeout()  // 定时器等待
        var pic = await getPicData(picsArr[i])  // 获取图片数据
        var resJson = await download(pic)  // 下载图片至本地
        console.log(resJson.err || `${resJson.name} downloaded successfully`)
    }
}

作品展示

源码戳我
展示一下成果,一共330张三维立体图

www.3wtu.com
www.360doc.com

如果看到这里你对三维立体图感兴趣,可以试试看破解下面这张。


雪人.jpg

原文首发于我的博客:https://www.vq0599.com/p/3
转载请注明出处

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

推荐阅读更多精彩内容

  • 《ijs》速成开发手册3.0 官方用户交流:iApp开发交流(1) 239547050iApp开发交流(2) 10...
    叶染柒丶阅读 4,740评论 0 7
  • Node基本 node的最大特性莫过于基于事件驱动的非阻塞I/O模型。 node通过事件驱动的方式处理请求,无须为...
    AkaTBS阅读 2,094评论 0 11
  • //本文内容起初摘抄于 阮一峰 作者的译文,用于记录和学习,建议观者移步于原文 概念: 所谓的Promise,...
    曾经过往阅读 1,202评论 0 7
  • 个人入门学习用笔记、不过多作为参考依据。如有错误欢迎斧正 目录 简书好像不支持锚点、复制搜索(反正也是写给我自己看...
    kirito_song阅读 2,421评论 1 37
  • 我很失望希瑟晚上没有和我在一起,原因有三:一,我喜欢有她在身边陪伴;二,我要确保在父亲家发生的事情,她不会受其影响...
    橘小芃阅读 1,088评论 0 5