利用node实现的小爬虫以及有关异步操作的一些思考

利用node实现的小爬虫以及有关异步操作的一些思考

  在搭建唯厘UED blog的时候一直想实现一个功能,就是类似于bing首页每天的壁纸都不一样,于是就考虑使用node做个小爬虫来实现这个功能并把它应用在管理后台的登陆页面上。下面主要来分享一下实现的过程和遇到的一些坑以及一些思考。

相关工具库

  首先介绍一下实现这个功能所使用到的一些工具库。

  • cheerio: Node.js 版的jQuery
  • https:封装了一个HTPPS服务器和一个简易的HTTPS客户端
  • iconv-lite:解决爬取网页出现乱码

  这是一个简单的小爬虫,那么,要爬取所需要的数据,首先要分析页面的结构。这次爬取的是必应壁纸(https://bing.ioliu.cn/),可以看到页面的结构如下图

页面结构

  那么我们的目标很明确,只需要爬取img标签里面的src属性值就可以了。那么首先,当然是要爬取到页面的html结构,再进行筛选,代码如下


function get(url) {
  https.get(url, (sres) => {
    const chunks = []
    sres.on('data', (chunk) => {
      chunks.push(chunk)
    })
    sres.on('end', () => {
      const html = iconv.decode(Buffer.concat(chunks), 'UTF-8')
      const $ = cheerio.load(html, { decodeEntities: false })
    })
  })
}

  ondata事件获取到的结果是一堆Buffer数据流,如下图,那么我们需要把这一堆数据转换成DOM,这个时候就需要使用cheerio,cheerio是为服务器特别定制的,快速、灵活、实施的jQuery核心实现。简单来说,就是它能够把这一堆堆buffer数据流转变为DOM,并且支持使用jquery的语法来进行DOM操作。


buffer

  在上述ondata事件中补充以下代码,如果有数据,那么就爬取成功了。但是就这么容易成功了吗?文章短小一般都不是我的尿性,先剧透,下面代码执行完之后的结果是undefined。


console.log($('.mark').css('background-image'))

  ok,我们分析一下原因,当进去这个图片页面的时候,可以发现其使用了懒加载的效果,那么就意味着,图片链接是由js动态添加上去的,再去看一下js代码(如下),果不其然。这里就遇到了第一个坑,大部分简单的爬虫并没有办法获取得到动态生成的内容,要实现获取动态内容的效果,需要使用PhantomJS,有兴趣的可以深入了解一下,由于时间关系,以及社区上反映的的PhantomJS的性能问题,这里就不使用PhantomJS。


lazyload

  既然直接爬取的途径断了,那么我们能不能试一下间接爬取,实行曲线救国?先分析分析一下一张完整大图的路径和缩略图的路径有什么不一样的地方

大图:http://images.ioliu.cn/bing/Shanghai_ZH-CN10665657954_1920x1080.jpg

缩略图: https://bing.ioliu.cn/photo/Shanghai_ZH-CN10665657954?force=home_1

  可以看到,这两者不同的只是照片的名字之外的地方,那么我们需要截取照片的名字,再把域名和分辨率进行拼接,这样就能获取得到一张大图的真实路径。

lazyload

完整代码

const basicUrl = 'http://images.ioliu.cn/bing'
let picLinkArray = []

/*
*params url: 需要爬取的链接
*/

function get(url) {
  https.get(url, (sres) => {
    const chunks = []
    sres.on('data', (chunk) => {
      console.log(chunk)
      chunks.push(chunk)
    })
    sres.on('end', () => {
      const html = iconv.decode(Buffer.concat(chunks), 'UTF-8')
      const $ = cheerio.load(html, { decodeEntities: false })
      const tagArray = $('.item a.mark')
      const oriLinkArray = []
      for (let i = 0; i < tagArray.length; i += 1) {
        oriLinkArray.push(tagArray[i].attribs.href)
      }
      for (let i = 0; i < oriLinkArray.length; i += 1) {
        const link = oriLinkArray[i]
        const index = oriLinkArray[i].indexOf('force')
        const picLink = oriLinkArray[i].slice(0, index - 1).replace('/photo', '').concat('_1920x1080.jpg')
        picLinkArray.push(basicUrl + picLink)
      }
    })
  })
}

  这样,我们就实现了获取大图的函数,接下来只需要循环执行,便可以获取得到前10页的大图

for (let i = 1; i < 10; i += 1) {
  get('https://bing.ioliu.cn?p=' + i)
}

  到这里,爬虫基本就完成了。很简单的一个功能,顺带提一下另一个坑,之前爬虫函数是写在管理后台一个单独的js文件里并在login组件require进来使用,但这样一来存在一个异步的问题,二来因为同源策略存在跨域的问题,因此换了一个思路,写在了node层里面,这样就避免了跨域的问题,但是异步的问题还是有一些麻烦的,下面以实现登录这个功能为例来说明蛋疼的地方,顺便集思广益,看看有木有什么更优雅的办法来解决这个问题。

  首先简单说明一下当前整个项目所采用的结构,当前采用的是操作方法和路由操作分离,对数据库的操作方法都写在controller文件夹下的js文件里面,同时controller会暴露一个接口给路由层。

  在美迪科项目里面,这种方法是可行的,因为lowdb的链式操作是同步的,这样能够顺利保证数据库的操作结果能够顺利返回到路由层,然后由路由层来进行下一步操作,如下图。但在mongoose + mongodb的组合里,mongoose有关数据库的结果操作都放在了一个回调函数里,这样要把数据库的操作结果传递到route层就有点蛋疼。经过摸索,目前的解决方案有两种。


lazyload

1.返回一个promise对象给路由层

controller里面的login方法

login(user) {
    var promise = userdb.find({ "username": user.username, "password": user.password }).exec()
    return promise
}

路由里面的操作

router.post('/api/admin/login', (req, res, next) => {
    const promise = frontEndAdmin.login(req.body)
    promise.then(
      function(result) {
        if (result.length === 0) {
          res.json(returnData('error', 503, '唯厘这里没有这个人哦', {}))
        }
      }
    )
    res.json(data)
  })
}

  这种方法的缺点也很明显,就是对数据库的操作和路由的操作紧密结合,并没有很好的解耦,显然不是我们需要的,同样的也尝试过使用node的eventEmitter,但也有这种问题。我们需要的仅是数据库的操作结果,并且根据这个结果来返回不同的接口信息。

2.把res对象传递到controller层

controller里面的login方法

login(user, res) {
    userdb.find(
      { "username": user.username, "password": user.password },
      function (err, person) {
        console.log(person)
        if (err) {
          res.json(returnData('error', 500, '出错啦,去怼写接口的人', {}))
        }
        if (person.length === 0) {
          res.json(returnData('error', 503, '唯厘这里没有这个人哦', {}))
        } else {
          res.json(returnData(null, null, null, {}))
        }
      }
    )

路由里面的方法

router.post('/api/admin/login', (req, res, next) => {
    frontEndAdmin.login(req.body, res)
}

  这样咋一看似乎简洁明了了许多,但是路由的信息操作混进了controller层,也并没有完全的解耦,目前采用的是第二种方法。


  经过新一轮的摸索,截止2017年09月27日18:54:59,上述问题作废,终于探索出一个完美的解决方案,之前一直是想尝试使用async + await这个方案的,但无奈本机node版本达不到要求,也因为同时进行的项目比较多,担心升级后会有什么副作用,但最后还是一狠心把node版本给升级了,这样就可以使用这个组合了。贴代码贴代码

controller里面的login方法

login(user) {
    var promise = userdb.find({ "username": user.username, "password": user.password }).exec().then(
      (result) => {
        if (result.length === 0) {
          return returnData('error', 503, '唯厘这里没有这个人哦', {})
        }
      }
    )
    return promise
    )

路由里面的方法

router.post('/api/admin/login', async(req, res, next) => {
    const data = await frontEndAdmin.login(req.body)
    res.json(data)
  })

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

推荐阅读更多精彩内容