• Node异步爬虫引出的异步流程控制的一些问题


    前记:
    想写一个电影天堂的爬虫,因为node很长时间落下,就想用node去写一下。结果遇到了一些列的问题,这些问题归根到底就是异步流程控制的问题,在以前就一直会接触到很多回调地狱,Promise为什么会出现诸如此类的话题,现在终于是深刻体会到了!

    开始的代码是:

    const cheerio = require('cheerio');
    const http = require('http');
    const iconv = require('iconv-lite');
    
    let baseUrl = "http://www.ygdy8.net/html/gndy/dyzz/list_23_";
    let Host = "http://www.ygdy8.net/";
    let titleHref = [];
    const totalPage = 1; //指定爬多少页数据
    let res = [];
    //获取页面电影数据
    function getTitleHref(url,page) {
      let startUrl = url+page+".html";
      http.get(startUrl,function(res) {
        let chunks = [];
    
        res.on('data',function(chunk){
          chunks.push(chunk);
        });
        res.on('end',function(){
          let title = [];
          let html = iconv.decode(Buffer.concat(chunks),'gb2312');
          let $ = cheerio.load(html, {decodeEntities: false});
          // console.log($);
          $('.co_content8 .ulink').each(function(i,d) {
            let $d = $(d);
            titleHref.push({
              href: $d.attr('href')
            });
          });
          console.log(titleHref);
        });
        if(page <= totalPage) {
          getTitleHref(url,++page);
        }else {
          console.log(page);
          getLink(titleHref);
        }
       
      });
    }
    
    //获取种子链接
    function getLink(titleHref) {
      console.log('进入getLink');
    
      titleHref.forEach(function(v,k) {
        console.log('~~~~~~~~~~~~~~~~~~~~');
        let infoUrl = Host + v.href;
        console.log(infoUrl);
        // try {
          http.get(infoUrl,function(res) {
            console.log('进入getlink http');
            
            let chunks = [];
            res.on('data',function(chunk) {
              chunks.push(chunk);
            });
            res.on('end', function(){
              let html = iconv.decode(Buffer.concat(chunks),'gb2312');
              let $ = cheerio.load(html, {decodeEntities: false});
              
              
              let reg = /.*译  名/;
              let info = '';
              let bt = '';
              let textInfo = $('.co_content8 #Zoom p').eq(0).text();
              info = textInfo.match(reg)[0];
              bt = $('#Zoom td').children('a').attr('href');
              res.push({
                Info:info,
                Bt:bt
              });
              console.log(res);
            })
            //怎么捕获错误!!!
            //res.on('error',function(){
            //  console.log('error');
            //})
          })
      // }catch(e) {
      //   console.log(e);
      // }
      });
    };
    
    getTitleHref(baseUrl,1)
    

    所以写node代码切记大多数都是异步的,上面代码就出了一个问题:
    这里写图片描述

    当前代码就不能保证下面的代码, 在 res.end 后执行,因为res.end在异步队列里可能没执行完,就进入了下面的if,就算最后进入getLink后就会出现titleHref.forEach进不去的情况的,因为titleHref是空的。

    当时遇到这个问题如果不考虑到异步流程控制的解决流程的话,一个解决方案是在each函数里,获取到一个titleHref就getLink下,titileHref定义成局部函数,getLink函数放在each里面,这样就保证titleHref不会是空的了。然后代码如下:

    const cheerio = require('cheerio');
    const http = require('http');
    const iconv = require('iconv-lite');
    
    let baseUrl = "http://www.ygdy8.net/html/gndy/dyzz/list_23_";
    let Host = "http://www.ygdy8.net/";
    
    const totalPage = 2; //指定爬多少页数据
    let ans = [];
    //获取页面电影数据
    function getTitleHref(url,page) {
      let startUrl = url+page+".html";
      http.get(startUrl,function(res) {
        const { statusCode } = res;
        let chunks = [];
        res.on('data',function(chunk){
          chunks.push(chunk);
        });
        res.on('end',function(){
          let title = [];
          
          let html = iconv.decode(Buffer.concat(chunks),'gb2312');
          let $ = cheerio.load(html, {decodeEntities: false});
          // console.log($);
          $('.co_content8 .ulink').each(function(i,d) {
            let $d = $(d);
            let titleHref = [];
            titleHref.push({
              href: $d.attr('href')
            });
            getLink(titleHref);
          });
          // console.log(ans);
        });  
      });
    }
    
    
    // /*
    //获取种子链接
    function getLink(titleHref) {
      console.log('进入getLink');
      console.log(titleHref);
      if(titleHref) {
        titleHref.forEach(function(v,k) {
          console.log('~~~~~~~~~~~~~~~~~~~~');
          let infoUrl = Host + v.href;
          // console.log(infoUrl);
        
            http.get(infoUrl,function(res) {
              const { statusCode } = res;
              const contentType = res.headers['content-type'];
            
              let error;
              if (statusCode !== 200) {
                error = new Error('请求失败。
    ' +
                                 `状态码: ${statusCode}`);
              } 
              if (error) {
                console.error(error.message);
                // 消耗响应数据以释放内存
                res.resume();
                return;
              }
              console.log('进入getlink http');
              let chunks = [];
              res.on('data',function(chunk) {  
                chunks.push(chunk);
              });
              res.on('end', function(){
                try {
                  let html = iconv.decode(Buffer.concat(chunks),'gb2312');
                  let $ = cheerio.load(html, {decodeEntities: false});
                  let bt = '';
                  bt = $('#Zoom td').children('a').attr('href');
                  // console.log(bt);
                  // console.log(typeof bt)
                  ans.push(bt);
                  // cb(ans);
                }catch (e) {
                  console.error('bt',e.message);
                }
              })
            }).on('error', (e) => {
              console.error(`错误: ${e.message}`);
            });
        });
      }
    };
    // */
    for(let i = 1; i <= totalPage; i++) {
      getTitleHref(baseUrl,i);
      console.log(ans);
    };
    
    
    

    但是这样的代码你还会发现一个问题,我们最后保存的bt链接的ans结果,打印的还是空的,同样是异步的问题,我们如果要存入数据库或者需要ans数据的话,我们不知道何时返回了这个数据。

    所以最终我们还是要用到ES6/7提出的方案Promise和async/await。
    修改之后代码如下:

    const cheerio = require('cheerio')
    const http = require('http')
    const iconv = require('iconv-lite')
    
    const baseUrl = 'http://www.ygdy8.net/html/gndy/dyzz/list_23_'
    const Host = 'http://www.ygdy8.net/'
    
    const totalPage = 2 //指定爬多少页数据
    let ans = []
    //获取页面电影数据
    function getTitleHref(url, page) {
      return new Promise((resolve, reject) => {
        let startUrl = url + page + '.html'
    
        http.get(startUrl, function(res) {
          const { statusCode } = res
          let chunks = []
          res.on('data', function(chunk) {
            chunks.push(chunk)
          })
          res.on('end', function() {
            let title = []
    
            let html = iconv.decode(Buffer.concat(chunks), 'gb2312')
            let $ = cheerio.load(html, { decodeEntities: false })
    
            let titleHref = []
            $('.co_content8 .ulink').each(function(i, d) {
              let $d = $(d)
              titleHref.push({
                href: $d.attr('href')
              })
            })
    
            resolve(getLink(titleHref))
          })
        })
      })
    }
    
    // /*
    //获取种子链接
    function getLink(titleHref, cb) {
      console.log('进入getLink')
      console.log(titleHref)
      if (titleHref) {
        return Promise.all(
          titleHref.map(function(v, k) {
            return new Promise((resolve, reject) => {
              console.log('~~~~~~~~~~~~~~~~~~~~')
              let infoUrl = Host + v.href
    
              http
                .get(infoUrl, function(res) {
                  const { statusCode } = res
                  const contentType = res.headers['content-type']
    
                  let error
                  if (statusCode !== 200) {
                    error = new Error('请求失败。
    ' + `状态码: ${statusCode}`)
                  }
                  if (error) {
                    console.error(error.message)
                    // 消耗响应数据以释放内存
                    res.resume()
                    return
                  }
                  let chunks = []
                  res.on('data', function(chunk) {
                    chunks.push(chunk)
                  })
                  res.on('end', function() {
                    try {
                      let html = iconv.decode(Buffer.concat(chunks), 'gb2312')
                      let $ = cheerio.load(html, { decodeEntities: false })
                      let bt = ''
                      bt = $('#Zoom td')
                        .children('a')
                        .attr('href')
                      resolve(bt)
                    } catch (e) {
                      reject(e)
                    }
                  })
                })
                .on('error', e => {
                  reject(e)
                })
            })
          })
        )
      } else {
        return Promise.resolve()
      }
    }
    
    async function main() {
      // */
      let results = await Promise.all(
        new Array(totalPage).fill().map((_, i) => getTitleHref(baseUrl, i + 1))
      )
    
      ans = ans.concat(...results)
      console.log('get data:', ans)
    }
    
    main()
    

    每个函数都封装成Promise,最后在主函数中用await强制同步得到最后的结果results。(注意:1。new Array出来的是稀疏数组empty,最后fill()一下填充成undefine,2。47行传递的已经不是一个只有一条数据的数组了,而是将一个页面each执行完成后的汇总,所以在函数内部会有Promise.all
    3。93行则聊胜于无,即使return null也会正确的触发resolve的,这么写只是提高一些可读性罢了。)

    Promise和async/await整理可以看我的这篇博客Promise和async/await用法整理

    代码:github传送

    这次告诉我实践很重要!要把所学和书中所看运用到业务和代码逻辑中!

  • 相关阅读:
    Android NDK 环境搭建(Native Development Kit )
    No enclosing instance of type Test is accessible. Must qualify the allocation with an enclosing in
    ASP.NET 未被授权访问所请求的资源。请考虑授予 ASP.NET 请求标识访问此资源的?
    解决FileUpload控件上传文件大小限制
    学习图表控件MsChart
    Delphi PChar与String互转
    Ext.data.Store的基本用法
    My97 DatePicker 日期格式
    ASP.net流的方式输出图片或文件
    C# 二进制、十进制、十六进制互转
  • 原文地址:https://www.cnblogs.com/zhangmingzhao/p/9234951.html
Copyright © 2020-2023  润新知