1. 程式人生 > >Node非同步爬蟲引出的非同步流程控制的一些問題

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('請求失敗。\n' +
                             `狀態碼: ${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('請求失敗。\n' + `狀態碼: ${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的,這麼寫只是提高一些可讀性罷了。)

這次告訴我實踐很重要!要把所學和書中所看運用到業務和程式碼邏輯中!

相關推薦

Node非同步爬蟲引出非同步流程控制一些問題

前記: 想寫一個電影天堂的爬蟲,因為node很長時間落下,就想用node去寫一下。結果遇到了一些列的問題,這些問題歸根到底就是非同步流程控制的問題,在以前就一直會接觸到很多回調地獄,Promise為什麼會出現諸如此類的話題,現在終於是深刻體會到了! 開始的程式碼是: const cheerio = requi

node js 非同步執行流程控制模組Async介紹

sync是一個流程控制工具包,提供了直接而強大的非同步功能。基於Javascript為Node.js設計,同時也可以直接在瀏覽器中使用。 Async提供了大約20個函式,包括常用的 map, reduce, filter, forEach 等,非同步流程控制模式包括,序列(series),並行(para

Node.js系列--非同步流程控制

<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span><span style="font-

js非同步操作的流程控制——20181114

序列執行(程式碼可直接在console控制檯執行) var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; function async(arg, callback) { console.log('引數為 ' + arg +' , 1秒後返回

JavaScript非同步流程控制全攻略

一.js非同步流程的由來       眾所周知,Javascript語言的執行環境是單執行緒(single thread),作為瀏覽器指令碼語言,JavaScript的主要用途是與使用者互動,以及操作DOM。若以多執行緒的方式操作這些DOM,則可能出現操作

Nodejs:非同步流程控制(下)

//非同步操作,序列有關聯(瀑布流模式) //npm install async --save-dev var async = require('async'); function exec() { async.waterfall( [ fun

Nodejs:非同步流程控制(上)

function oneFun() { // setTimeout(function () { // console.log("a"); // }, 1000); i = 0; setInterval(function () {

JS的四種非同步方式:回撥/監聽/流程控制庫/promise

你可能知道,Javascript語言的執行環境是”單執行緒”(single thread)。 所謂”單執行緒”,就是指一次只能完成一件任務。如果有多個任務,就必須排隊,前面一個任務完成,再執行後面一個任務,以此類推。 這種模式的好處是實現起來比較簡單,執行環

一道promise的小題目(Promise非同步流程控制

用Promise控制非同步流程,三個非同步任務,時間可能有先後,但是要按照想要的順序輸出。 我這裡用四種方法解決,其實也就是考察你對Promise的理解,基礎題了。 //實現mergePromise函式,把傳進去的陣列順序先後執行, //並且把返回的資料先後放到陣列data中 const timeout =

async 非同步流程控制

async是一個強大的非同步流程控制庫,其語義類似於js對陣列的操作。它提供了一系列非常強大而便捷的方法,有助於我們在javascript單執行緒模型背景下寫出優雅的邏輯控制程式碼。 牛刀小試 先從檔案操作開始初步瞭解async函式庫的作用:

Node.js 使用回調函數實現串行流程控制

exc pre 流程 示例 clas ons con 函數 span 下面是一個使用Node.js回調函數實現串行流程控制的示例: setTimeout(function() { console.log(‘I excute first.‘); setTim

Node.js 使用Nimble實現串行流程控制

串行 nod clas exc ast div span 流程控制 logs Nimble是Node.js下的流程控制工具。 使用如下命令進行安裝: npm install nimble 測試代碼: var flow = require(‘nimble‘); flo

[js高手之路]Node.js模板引擎教程-jade速學與實戰2-流程控制,轉義與非轉義

title 學習 != 下一步 cas ase spa back name 一、轉義與非轉義 jade模板文件代碼: 1 doctype html 2 html 3 head 4 meta(charset=‘utf-8‘)

node.js對mongodb的連接&增刪改查(附async同步流程控制

color var literal int lba node () n! node.js 1.啟動mongodb數據庫 官網下載mongodb數據庫 在mongodb根目錄下創建文件夾:假設取名為test。 我們認為test就是mongodb新建的數據庫一枚。 創建批處理文

Node爬蟲之使用 async 控制併發

上文 《Node爬蟲之使用 eventproxy 控制併發》我們說到用 node 爬了csdn首頁資料,但是這些請求完全是併發的,如果某些網站有 “反爬” 機制,就很有可能封鎖你的 IP。這樣的情況下我們就可以使用 async 模組。主要用到的是 async 的 mapLimit(arr,

Node爬蟲之使用 eventproxy 控制併發

上一篇文章《Node實現簡單爬蟲》我們介紹瞭如何使用 superagent和cheerio來取主頁內容,那隻需要發起一次 http get 請求就能辦到。但這次,我們需要取出每個主題的第一條評論,這就要求我們對每個主題的連結發起請求,並用 cheerio 去取出其中的第一條評論。 eve

node單執行緒非同步模型

提到nodejs都知道單執行緒非同步I/O,但是能說清楚為什麼單執行緒非同步I/O,為什麼能增加網路吞吐量,怎麼充分利用cpu資源嗯,這個知道的就不多了。 首先要說的是I/O,I/O是計算機的抽象概念,指的是鍵盤,滑鼠,印表機,套接字等和記憶體之間的資料交換,I/O的速度是很慢的,知道計算機存貯模型的都知道

node.js對mongodb的連線&增刪改查(附async同步流程控制

最近嘗試了node.js和mongodb的使用。下面來一波步驟。 1.啟動mongodb資料庫 官網下載mongodb資料庫 在mongodb根目錄下建立資料夾:假設取名為test。 我們認為test就是mongodb新建的資料庫一枚。 建立批處理

python多執行緒、非同步、多程序+非同步爬蟲

安裝Tornado 非同步用到了tornado,根據官方文件的例子修改得到一個簡單的非同步爬蟲類。可以參考下最新的文件學習下。 pip install tornado 非同步爬蟲 import time from datetime import ti

利用aiohttp製作非同步爬蟲

簡介 asyncio可以實現單執行緒併發IO操作,是Python中常用的非同步處理模組。關於asyncio模組的介紹,筆者會在後續的文章中加以介紹,本文將會講述一個基於asyncio實現的HTTP框架——aiohttp,它可以幫助我們非同步地實現HTTP請求,從而使得我們的程式效率大大提高。 本文將