async+phantomjs制作nodejs的小說爬蟲

內(nèi)容要點(diǎn)

一步一腳印實(shí)現(xiàn)一個(gè)爬蟲,前面的內(nèi)容比較簡單,有了解可以直接跳過而且文章內(nèi)容較長和多圖,建議在pc下閱讀
源碼地址
phantomjs捕獲內(nèi)容
詳細(xì)介紹通過async.mapLimit并發(fā)處理,結(jié)合定時(shí)器進(jìn)行延時(shí)執(zhí)行
數(shù)據(jù)存放到mongodb
數(shù)據(jù)輸出成文件
(如有錯(cuò)誤請大家指出澎嚣,一起學(xué)習(xí))

介紹(有了解可以直接跳過)

關(guān)于PhantomJS
首先介紹一下phantomjs

PhantomJS是一個(gè)基于WebKit的服務(wù)器端JavaScript API轧叽,它基于 BSD開源協(xié)議發(fā)布纪岁。PhantomJS無需瀏覽器的支持即可實(shí)現(xiàn)對Web的支持里烦,且原生支持各種Web標(biāo)準(zhǔn)匪凡,如DOM 處理、JavaScript捌袜、CSS選擇器琅轧、JSON、Canvas和可縮放矢量圖形SVG搭伤。PhantomJS主要是通過JavaScript和CoffeeScript控制WebKit的CSS選擇器只怎、可縮放矢量圖形SVG和HTTP網(wǎng)絡(luò)等各個(gè)模塊。

phantomjs的應(yīng)用場景

無需瀏覽器的Web測試:無需瀏覽器的情況下進(jìn)行快速的Web測試怜俐,且支持很多測試框架身堡,如YUI Test、Jasmine拍鲤、WebDriver贴谎、Capybara汞扎、QUnit、Mocha等擅这。
頁面自動(dòng)化操作:使用標(biāo)準(zhǔn)的DOM API或一些JavaScript框架(如jQuery)訪問和操作Web頁面澈魄。
屏幕捕獲:以編程方式抓起CSS、SVG和Canvas等頁面內(nèi)容仲翎,即可實(shí)現(xiàn)網(wǎng)絡(luò)爬蟲應(yīng)用痹扇。構(gòu)建服務(wù)端Web圖形應(yīng)用,如截圖服務(wù)谭确、矢量光柵圖應(yīng)用帘营。
網(wǎng)絡(luò)監(jiān)控:自動(dòng)進(jìn)行網(wǎng)絡(luò)性能監(jiān)控、跟蹤頁面加載情況以及將相關(guān)監(jiān)控的信息以標(biāo)準(zhǔn)的HAR格式導(dǎo)出逐哈。


基于phantomjs2.0進(jìn)行實(shí)現(xiàn)有兩種實(shí)現(xiàn)方案,一種是使用基于全局的 http://phantomjs.org/ 问顷,另外一種是封裝的模塊 phantom - Fast NodeJS API for PhantomJS -https://github.com/amir20/phantomjs-node
這里選擇phantomjs-node
關(guān)于phantomjs-node的安裝以及入門
使用可以根據(jù)百度前端學(xué)院2017中的網(wǎng)頁抓取分析服務(wù)系列相關(guān)內(nèi)容中學(xué)習(xí),這里放一下之前phantomjs-node 學(xué)習(xí)的筆記和demo
中的phantomjs_1~4目錄下
后文也會(huì)進(jìn)一步說明使用方法昂秃。

實(shí)現(xiàn)思路和過程

實(shí)現(xiàn)思路

phantomjs就相當(dāng)于一個(gè)無圖形界面的瀏覽器,那么我們提供連接給phantomjs就意味著我們能獲取這個(gè)url的內(nèi)容。
這次爬蟲的內(nèi)容是希望獲取到小說的所有章節(jié)以及其內(nèi)容,直接以筆閣網(wǎng)為例,因?yàn)檫@次爬蟲是直接爬筆閣網(wǎng)的杜窄。
我們打開http://www.qu.la/book/5443,

![Uploading 14995025771427_828375.jpg . . .]## 內(nèi)容要點(diǎn)
一步一腳印實(shí)現(xiàn)一個(gè)爬蟲,文章內(nèi)容較長,建議在pc下閱讀
源碼地址
phantomjs捕獲內(nèi)容
詳細(xì)介紹通過async.mapLimit并發(fā)處理,結(jié)合定時(shí)器進(jìn)行延時(shí)執(zhí)行
數(shù)據(jù)存放到mongodb
數(shù)據(jù)輸出成文件
(如有錯(cuò)誤請大家指出肠骆,一起學(xué)習(xí))

介紹(有了解可以直接跳過)

關(guān)于PhantomJS
首先介紹一下phantomjs

PhantomJS是一個(gè)基于WebKit的服務(wù)器端JavaScript API,它基于 BSD開源協(xié)議發(fā)布塞耕。PhantomJS無需瀏覽器的支持即可實(shí)現(xiàn)對Web的支持蚀腿,且原生支持各種Web標(biāo)準(zhǔn),如DOM 處理扫外、JavaScript莉钙、CSS選擇器、JSON筛谚、Canvas和可縮放矢量圖形SVG磁玉。PhantomJS主要是通過JavaScript和CoffeeScript控制WebKit的CSS選擇器、可縮放矢量圖形SVG和HTTP網(wǎng)絡(luò)等各個(gè)模塊驾讲。

phantomjs的應(yīng)用場景

無需瀏覽器的Web測試:無需瀏覽器的情況下進(jìn)行快速的Web測試蚊伞,且支持很多測試框架,如YUI Test吮铭、Jasmine时迫、WebDriver、Capybara谓晌、QUnit掠拳、Mocha等。
頁面自動(dòng)化操作:使用標(biāo)準(zhǔn)的DOM API或一些JavaScript框架(如jQuery)訪問和操作Web頁面扎谎。
屏幕捕獲:以編程方式抓起CSS碳想、SVG和Canvas等頁面內(nèi)容烧董,即可實(shí)現(xiàn)網(wǎng)絡(luò)爬蟲應(yīng)用。構(gòu)建服務(wù)端Web圖形應(yīng)用胧奔,如截圖服務(wù)逊移、矢量光柵圖應(yīng)用。
網(wǎng)絡(luò)監(jiān)控:自動(dòng)進(jìn)行網(wǎng)絡(luò)性能監(jiān)控龙填、跟蹤頁面加載情況以及將相關(guān)監(jiān)控的信息以標(biāo)準(zhǔn)的HAR格式導(dǎo)出胳泉。


基于phantomjs2.0進(jìn)行實(shí)現(xiàn)有兩種實(shí)現(xiàn)方案,一種是使用基于全局的 http://phantomjs.org/ 岩遗,另外一種是封裝的模塊 phantom - Fast NodeJS API for PhantomJS -https://github.com/amir20/phantomjs-node
這里選擇phantomjs-node
關(guān)于phantomjs-node的安裝以及入門
使用可以根據(jù)百度前端學(xué)院2017中的網(wǎng)頁抓取分析服務(wù)系列相關(guān)內(nèi)容中學(xué)習(xí),這里放一下之前phantomjs-node 學(xué)習(xí)的筆記和demo
中的phantomjs_1~4目錄下
后文也會(huì)進(jìn)一步說明使用方法扇商。

實(shí)現(xiàn)思路和過程

實(shí)現(xiàn)思路

phantomjs就相當(dāng)于一個(gè)無圖形界面的瀏覽器,那么我們提供連接給phantomjs就意味著我們能獲取這個(gè)url的內(nèi)容。
這次爬蟲的內(nèi)容是希望獲取到小說的所有章節(jié)以及其內(nèi)容,直接以筆閣網(wǎng)為例,因?yàn)檫@次爬蟲是直接爬筆閣網(wǎng)的宿礁。
我們打開http://www.qu.la/book/5443,

14995025771427.jpg

上面就有這本小說的許多章節(jié),所以就有了第一步,或者這個(gè)頁面上所有章節(jié),通過"開發(fā)者工具"中的檢查共功能

14995028395237.jpg

我們可以看到知道內(nèi)容是這樣的結(jié)構(gòu)

<div id ="list">
<dd>
<a href="/**">第xx章</a>
</dd>
....
</div>

所以只要我們獲取 id為list 中所有的dd,就獲取了小說的所有章節(jié),同時(shí)通過dd中a標(biāo)簽的href屬性就可以連接到所有章節(jié)的內(nèi)容案铺。

爬蟲方面的思路說明到這里

實(shí)現(xiàn)過程

(請保證node版本高于7.9,本文基于7.10.0)
(最好先了解es7中async/await 以及child_process)
如何使用phantomjs-nodejs

如何運(yùn)行代碼?梆靖。控汉。
將代碼保存在一個(gè)js文件中例如test.js
然后運(yùn)行

node test.js

我的栗子??

const phantom = require('phantom');//導(dǎo)入模塊
//async解決回調(diào)問題,es7的內(nèi)容
(async function() {
     // await解決回調(diào)問題,創(chuàng)建一個(gè)phantom實(shí)例
    const instance = await phantom.create();
    //通過phantom實(shí)例創(chuàng)建一個(gè)page對象返吻,page對象可以理解成一個(gè)對頁面發(fā)起請求和處理結(jié)果這一集合的對象
    const page = await instance.createPage();
    //頁面指向的是哪個(gè)一個(gè)url
    await page.on("onResourceRequested", function(requestData) {
        console.info('Requesting', requestData.url)
    });
  //得到打開該頁面的狀態(tài)碼
    const status = await page.open('https://stackoverflow.com/');
    console.log(status);
//輸出該頁面的內(nèi)容
    const content = await page.property('content');
    console.log(content);
    //輸出內(nèi)容
   //退出該phantom實(shí)例
    await instance.exit();
}());

輸出結(jié)果

14995107245755.jpg

當(dāng)然不可能直接使用這些內(nèi)容,所以就需要通過

//這個(gè)方法,我的理解是跟你在chrome中的輸出臺(tái)的操作是一樣的所以看看下面栗子
await page.evaluate(function() {});

const phantom = require('phantom');
let url = encodeURI(`https://www.baidu.com/s?wd="hello"`);
(async function() {
    const instance = await phantom.create();
    const page = await instance.createPage();
    const status = await page.open(url);
    if (status !== 'success') {
        console.log("訪問失敗");
        return;
    } else {
        let start = Date.now();
        let result = await page.evaluate(function() {
            return document.title
        });
        let data = {
            cose: 1,
            msg: "抓取成功",
            time: Date.now() - start,
            dataList: result
        }
        console.log(JSON.stringify(data));
        await instance.exit();
    }

}());

輸出結(jié)果

14995115113865.jpg

模塊實(shí)現(xiàn)

獲取所有章節(jié)fetchAllChapters.js

const phantom = require('phantom');
const program = require('commander');
/*
  命令行參數(shù)幫助工具
  設(shè)置 option b 代表 book ,[book]表示該參數(shù)可以通過program訪問,這個(gè)參數(shù)表示書本編號(hào)
  命令 eg:
  node fetchAllChapters.js -b 5443  
*/
program
    .version('0.1.0')
    .option('-b, --book [book]', 'book number')
    .parse(process.argv);

//缺少書本參數(shù)直接退出
if (!program.book) {
    return
}
// example "5443",獲取書本編號(hào)
const bookNumber = program.book
    //訪問的url
const url = encodeURI(`http://www.qu.la/book/${bookNumber}/`);
//設(shè)置用戶代理頭
const userAgent = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36`
try {
    //提供async環(huán)境
    (async function() {
        //創(chuàng)建實(shí)例
        const instance = await phantom.create()
            //創(chuàng)建頁面容器
        const page = await instance.createPage()
            //設(shè)置
        page.setting("userAgent", userAgent)
            //判斷是否訪問成功
        const status = await page.open(url),
            code = 1;
        if (status !== 'success') {
            //訪問失敗修改狀態(tài)碼
            code = -1;
        } else {
            //獲取當(dāng)前時(shí)間
            var start = Date.now();
            var result = await page.evaluate(function() {
                var count = 1;
                return $('#list dl dd').map(function() {
                    return ({
                        index: count++,
                        title: $(this).find('a').html(),
                        link: url + ($(this).find('a').attr('href')).substring(($(this).find('a').attr('href')).lastIndexOf("/")),
                    })
                }).toArray()
            })
            let data = {
                code: code,
                bookNumber: "5443",
                url: url,
                time: Date.now() - start,
                dataList: result
            }
            console.log(JSON.stringify(data));
        }
        //退出實(shí)例
        await instance.exit();
    })()
} catch (e) {
    console.log(e)
}

輸出結(jié)果

14995146067785.jpg

在獲取所有章節(jié)之后,我們需要獲取所有章節(jié)的內(nèi)容了

fetchChapter

const phantom = require('phantom');
const mkdirp = require('mkdirp')
const program = require('commander');
const fs = require('async-file')
const path = require('path')
    //設(shè)置用戶代理
const userAgent = `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36`
    /*
    命令行參數(shù)
    p -替換原文本中的換行空格
    f -保存為文件
    t 自定義輸出路徑
    u 抓取單章的url
    */
program
    .version('0.1.0')
    .option('-p, --puer', 'puerMode')
    .option('-f, --file', 'save2File')
    .option('-t, --path [path]', 'outPutPath')
    .option('-u, --url [url]', 'url')
    .parse(process.argv);
if (!program.url) {
    return;

}
const URL = program.url;
const DEFAULT_PATH = '/book/default/';

/*
替換br和&nbsp標(biāo)簽
*/
function puer(str) {
    if (!str) {
        return
    }
    str = str.replace(/<br\s*\/?>/gi, "\r\n");
    str = str.replace(/&nbsp;/g, " ")
    return str
}
/*
test url 
node fetchChapter.js -u http://www.qu.la/book/5443/3179374.html -f -p
*/

(async function() {
    //創(chuàng)建實(shí)例
    const instance = await phantom.create()
        //創(chuàng)建頁面容器
    const page = await instance.createPage()
    page.setting("userAgent", userAgent)
    const status = await page.open(URL),
        code = 1;
    if (status !== 'success') {
        code = -1;
        return;
    } else {
        // await page.includeJs("https://cdn.bootcss.com/jquery/1.12.4/jquery.js")
        // await page.render('germy.png');
        var start = Date.now();
        var result = await page.evaluate(function() {
            //移除一些無關(guān)內(nèi)容(等于直接在結(jié)果網(wǎng)頁上的dom上進(jìn)行操作)
            //請注意這里如果調(diào)用console.log()是無效的!
            $("#content a:last-child").remove()
            $("#content script:last-child").remove()
            $("#content div:last-child").remove()
            $("#content script:last-child").remove()
            return ({
                title: $("h1").html(),
                content: $("#content").html()
            });
        })
        if (result.title == '' || result.content == '') {
            //內(nèi)容為空捕獲失敗
            console.log(JSON.stringify({
                code: -1
            }))
            return
        } else {
            //判斷參數(shù)進(jìn)一步處理
            if (program.puer) {
                var context = puer(result.content)
            }
            //文件模式處理后進(jìn)行保存到文件.返回文件路徑
            if (program.file) {

                let path = ""
                if (program.path) {
                    //自定義路徑
                } else {
                    path = DEFAULT_PATH;
                    //避免文件夾不存在,__dirname指向的是文件所在路徑
                    mkdirp(__dirname + path, (err) => {
                        if (err) {
                            console.log(err);
                        }
                    });
                    //拼接出文件輸出的路徑
                    path += result.title + ".txt";
                    await fs.writeFile(__dirname + path, context)
                        // return;
                        //輸出文件名
                    console.log(JSON.stringify({
                        code: 1,
                        filePath: path
                    }))
                }
            } else {
                console.log(JSON.stringify({
                    code: 1,
                    content: result
                }));
            }

        }
    }
    //exit
    await instance.exit();
})()

拓展

 await page.includeJs("https://cdn.bootcss.com/jquery/1.12.4/jquery.js")
 //可以導(dǎo)入其他js lib
 await page.render('germy.png');
 //渲染當(dāng)前頁面為圖片輸出
 

在這里說一下為什么可以直接使用jquery,以百度為例子

14995249028718.jpg

因?yàn)楫?dāng)前頁面加載的時(shí)候加載了jquery 這個(gè)lib,所以這里就可以直接使用了

結(jié)合使用

taskHandler

const exec = require('child_process').exec;
const execAsync = require('async-child-process').execAsync;
const delayAsync = require('./asyncFetch').delayAsync;
const program = require('commander');
let cmd;
/*
s 是章節(jié)開始(下標(biāo)是0,所以需要手動(dòng)減一,第一章就是 0)
e 是結(jié)束章節(jié)數(shù)
l 是并發(fā)數(shù)
m 模式
b 書的編號(hào)
test command:
node taskHandler.js -s 0 -e 10 -l 3 -b 5443
*/
program
    .version('0.1.0')
    .option('-s, --start [start]', 'start chapter', 0)
    .option('-e, --end [end]', 'end chapter')
    .option('-l, --limit [limit]', 'limit async', 3)
    .option('-m, --mode [mode]', 'Add bbq sauce', 2)
    .option('-b, --book [book]', 'book number')
    .parse(process.argv);
/*
 第一步獲取章節(jié)連接,第二部獲取章節(jié)內(nèi)容并進(jìn)行輸出
 輸出方式一 輸出到數(shù)據(jù)庫.(未實(shí)現(xiàn))
 輸出方式二 文件輸出(在關(guān)注react-pdf,希望支持pdf輸出)
*/
if (!program.book) {
    return
} else {
    cmd = `node fetchAllChapters.js -b ${program.book}`;
}
if (!program.start || !program.end) {
    console.log("must input with start-chapter and end-chapter ")
    return;
}

//
(async function() {

    const {
        stdout
        //調(diào)取子進(jìn)程 執(zhí)行cmd
    } = await execAsync(cmd, {
        //default value of maxBuffer is 200KB.
        maxBuffer: 1024 * 500
    });
    let data = JSON.parse(stdout),
        start = program.start,
        end = program.end,
        limit = program.limit,
        dataList = data['dataList'],
        fetchResult = null;
        //use to debug 
        // let dataList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
        if (!dataList || data.length <= 0) {
            return
        }



        // console.log(dataList)
        //分發(fā)任務(wù) 每10s調(diào)取一次并發(fā)抓取10條記錄 
        //截取需要的章節(jié)數(shù)
        /*根據(jù)章節(jié),章節(jié)是一開始,默認(rèn)無序章*/
        //dataList, start, end, limit
        //下面是把要抓取的內(nèi)容放置到delayAsync中,后文講述delayAsync
    try {
        fetchResult = await delayAsync(dataList, parseInt(start), parseInt(end), parseInt(limit));
    } catch (e) {
        console.log(e)
    }
})()

這里是將兩個(gè)模塊結(jié)合起來,先抓取所有章節(jié)數(shù)再進(jìn)行處理

這里使用async-child-process調(diào)起子進(jìn)程,然后直接獲取輸出在控制臺(tái)中的數(shù)據(jù)作為輸出結(jié)果,由于async-child-process默認(rèn)控制臺(tái)輸出的最大字節(jié)流是5kb所以要調(diào)整最大字節(jié)流的限制,不然會(huì)報(bào)錯(cuò);

結(jié)合async 與計(jì)時(shí)器實(shí)現(xiàn)延遲并發(fā)加載

這里先要說一下async.js這個(gè)庫提供了許多控制并發(fā)的方法,關(guān)于async的demo可以看一下唐大大的async demo,里面有許多async method 的使用 ??

而我們在這里使用的是 async.mapLimit()

/*
mapLimit(coll, limit, iteratee, callbackopt)
params coll 是數(shù)據(jù)集合
       limit 并發(fā)數(shù)量
       iteratee 迭代器fun(fun 提供item 和callback,通過ca)
       callcackopt collection執(zhí)行完畢或者是錯(cuò)誤出現(xiàn)執(zhí)行的回調(diào)函數(shù)
    
A callback which is called when all iteratee 
functions have finished, or an error occurs.
 Results is an array of the transformed items 
 from the coll. Invoked with (err, results).      
*/
//
var arr = [{name:'Jack', delay:200}, 
{name:'Mike', delay: 100},
 {name:'Freewind', delay:300}, 
 {name:'Test', delay: 50}];
async.mapLimit(arr,2, function(item, callback) {
    log('1.5 enter: ' + item.name);
    setTimeout(function() {
        log('1.5 handle: ' + item.name);
        if(item.name==='Jack') callback('myerr');
        else callback(null, item.name+'!!!');
    }, item.delay);
}, function(err, results) {
    log('1.5 err: ', err);
    log('1.5 results: ', results);
});
/*
20.675> 1.5 enter: Jack
20.682> 1.5 enter: Mike
20.786> 1.5 handle: Mike
20.787> 1.5 enter: Freewind
20.887> 1.5 handle: Jack
20.887> 1.5 err: myerr
20.887> 1.5 results: [ undefined, 'Mike!!!' ]
21.091> 1.5 handle: Freewind
*/

//在看另外一段

const async = require('async');
const moment = require('moment');
var arr = [{
    name: 'Jack',
    delay: 200
}, {
    name: 'Mike',
    delay: 100
}, {
    name: 'Freewind',
    delay: 300
}, {
    name: 'Test',
    delay: 50
}];
var log = function(msg, obj) {
    //對log進(jìn)行了封裝姑子。主要是增加了秒鐘的輸出,通過秒數(shù)的差值方便大家對async的理解测僵。
    process.stdout.write(moment().format('ss.SSS') + '> ');
    if (obj !== undefined) {
        process.stdout.write(msg);
        console.log(obj);
    } else {
        console.log(msg);
    }
}
async.mapLimit(arr, 2, function(item, callback) {
    log('1.5 enter: ' + item.name);
    setTimeout(function() {
        log('1.5 handle: ' + item.name);
        // if (item.name === 'Jack') callback('myerr');
        callback(null, item.name + '!!!');
    }, item.delay);
}, function(err, results) {
    log('1.5 err: ', err);
    log('1.5 results: ', results);
});

/*
18.951> 1.5 enter: Jack
18.958> 1.5 enter: Mike
19.062> 1.5 handle: Mike
19.063> 1.5 enter: Freewind
19.162> 1.5 handle: Jack
19.162> 1.5 enter: Test
19.217> 1.5 handle: Test
19.367> 1.5 handle: Freewind
19.367> 1.5 err: null
19.369> 1.5 results: [ 'Jack!!!', 'Mike!!!', 'Freewind!!!', 'Test!!!' ]
*/

更直觀的看出callcackopt的調(diào)用是在error或者全部完成后調(diào)用的,result里放著的是每次callback(null,result)調(diào)用的結(jié)果以數(shù)組的形式儲(chǔ)存,注意如果某個(gè)函數(shù)沒有使用該回調(diào),在結(jié)果里顯示就是undefined
至于結(jié)束后仍輸出,就是異步機(jī)制的問題(或者說是cpu調(diào)度問題街佑?),已經(jīng)調(diào)起了控制臺(tái)的輸出后 callcackopt才調(diào)用

大概了解async.mapLimit的使用后來看一下目前我的實(shí)現(xiàn)和存在的問題

const async = require('async')
const execAsync = require('async-child-process').execAsync;
/*實(shí)現(xiàn)并發(fā)抓取的函數(shù)*/
var asyncFetch = function(data, number, method) {
        return new Promise(function(resolve, reject) {
            if (!data || data.length <= 0) {
                reject("data not exist")
            }
            let result = [];
            async.mapLimit(data, number, async(data, callback) => {
                //需要設(shè)置延時(shí)不然ip會(huì)被封掉
                let cmd = `node fetchChapter.js  -u ${data.link} -f -p`,
                    json,
                    //獲取一個(gè)內(nèi)容就輸出一個(gè)
                    {
                        stdout
                    } = await execAsync(cmd, {
                        //default value of maxBuffer is 200KB.
                        maxBuffer: 1024 * 500
                    });
                /*將內(nèi)容保存到j(luò)son中*/
                json = JSON.parse(stdout);
                //保存index
                json.index = data.index;
                /*
                由于設(shè)置成了async,出現(xiàn)了多次觸發(fā)err的情況,callback 不能正常工作,
                手動(dòng)推入result中,但是這樣順序是不確定的,有待解決這個(gè)問題
                */
                result.push(json);
                callback(null, json) //not work 
            }, function(err) {
                //回調(diào)函數(shù)在全部都執(zhí)行完以后執(zhí)行
                if (err) {
                    reject(err)
                }
                resolve(result)
            })
        })
    }
    /*實(shí)現(xiàn)延時(shí)加載的函數(shù)*/
var delayAsync = function(dataList, start, end, limit) {
    return new Promise(function(resolve, reject) {
        var result = [],
            counter = 0,
            checkTimer,
            checkTimeOut,
            fetchTimers = [],
            count = Math.ceil((end - start) / limit),
            remain = start - end,
            i = 0;
        if (dataList.length <= 0) {
            //數(shù)據(jù)長度為空就返回
            reject("error")
            return;
        }
        //打印一下輸入情況
        console.log(dataList)
        try {
            /*章數(shù)的開始和結(jié)束*/
            console.log(`從${start}到 ${end}`)
            let startIndex = start,
                endIndex;
            while (startIndex != end) {
                /*
                需要注意的是當(dāng)剩余的任務(wù)不足以達(dá)到并發(fā)數(shù)的時(shí)候
                要保證任務(wù)分割不能出界
                */
                if (startIndex + limit < end) {
                    endIndex = startIndex + limit;
                } else {
                    //截取出界
                    endIndex = end;
                }
                /*分割任務(wù)*/
                chapter = dataList.slice(startIndex, endIndex);
                //通過閉包實(shí)現(xiàn)IIFE保存當(dāng)時(shí)抓取的情況,不使用閉包綁定的數(shù)據(jù)則是運(yùn)行之后的值
                (function(startIndex, endIndex, chapter) {
                    //通過tempTimer 保存下來
                    let tempTimer = setTimeout(async function() {
                        //獲得此次任務(wù)開始執(zhí)行的時(shí)間
                        let startTime = new Date(),
                            time, chapterResult = [];
                        //進(jìn)行并發(fā)捕獲執(zhí)行命令
                        try {
                            chapterResult = await asyncFetch(chapter, limit);
                        } catch (e) {
                            // console.log(e)
                        }
                        result = result.concat(chapterResult)
                            //用于判斷任務(wù)標(biāo)記 
                        counter++;
                        time = new Date() - startTime;
                        console.log(`完成抓取 ${startIndex} 到 ${endIndex} 計(jì)數(shù)器是${counter} 時(shí)間是${time}`)
                    }, i * 1000);
                    fetchTimers.push(tempTimer);

                })(startIndex, endIndex, chapter)
                i++; //控制延時(shí)
                //推進(jìn)任務(wù)進(jìn)行
                startIndex = endIndex;
            }
        } catch (e) {
            reject(e)
        }
        /*定時(shí)判斷任務(wù)是否完成*/
        checkTimer = setInterval(function() {
            console.log(`counter is ${counter} count is ${count}`)
            if (counter == count) {
                //清除定時(shí)器
                clearTimeout(checkTimeOut);
                //清除定時(shí)器
                clearInterval(checkTimer);
                resolve(result)
            }
        }, 1000);
        //or use promise all ?
        //30s計(jì)時(shí)器判斷超時(shí),超時(shí)時(shí)間暫做距離
        checkTimeOut = setTimeout(function() {
            //超時(shí)清除所有定時(shí)器
            for (let i = 0; i < fetchTimers.length; i++) {
                clearTimeout(fetchTimers[i]);
            }
            //清除定時(shí)判斷
            clearInterval(checkTimer);
            console.log("timout")
            reject(result)
        }, 30000);
    })
}

module.exports = {
    asyncFetch: asyncFetch,
    delayAsync: delayAsync,
}

目前在async中存在問題,callback函數(shù)不能正常工作,所以每次都是手動(dòng)將結(jié)果推入結(jié)果集,導(dǎo)致結(jié)果集的順序不能和原數(shù)據(jù)順序?qū)?yīng),
然而async官方文檔中

The callback must be called exactly once, ideally on a later tick of the JavaScript event loop.

至少要調(diào)用一次callback? 但是

在延時(shí)并發(fā)中考慮用await Promise.all[] 取代定時(shí)器判斷任務(wù)是否結(jié)束

輸出結(jié)果

14995701558979.jpg

儲(chǔ)存到mongodb

這里使用的數(shù)據(jù)庫驅(qū)動(dòng)模塊是 mongolass

第一步配置mongolass并添加模型

const Mongolass = require('mongolass');
const moment = require('moment');
const objectIdToTimestamp = require('objectid-to-timestamp');
const mongolass = new Mongolass();
//儲(chǔ)存的庫的url 
mongolass.connect('mongodb://localhost:27017/novel');
// 根據(jù) id 生成創(chuàng)建時(shí)間 created_at
mongolass.plugin('addCreatedAt', {
  afterFind: function(results) {
    results.forEach(function(item) {
      item.created_at = moment(objectIdToTimestamp(item._id)).format('YYYY-MM-DD HH:mm');
    });
    return results;
  },
  afterFindOne: function(result) {
    if (result) {
      result.created_at = moment(objectIdToTimestamp(result._id)).format('YYYY-MM-DD HH:mm');
    }
    return result;
  }
});
/*
  下面模型的意思是
  Book表
  字段      屬性
  bookNum  string
  url      stirng
  chapters 對象數(shù)組 - 對象的屬性是index - number ...類推
*/
exports.Book = mongolass.model('Book', {
  bookNum: {
    type: 'string'
  },
  url: {
    type: 'string'
  },
  chapters: [{
    index: {
      type: "number"
    },
    link: {
      type: "string"
    },
    title: {
      type: "string"
    }
  }]
});
//書模型
exports.Book.index({
  bookNum: 1
}, {
  unique: true
}).exec(); // 根據(jù)書本編號(hào)找到書本的章節(jié)捍靠,書編號(hào)全局唯一

/*
  下面模型的意思是
  Chapter表
  字段      屬性
  bookNum  string
  start    number
  end      number
  chapters 對象數(shù)組 - 對象的屬性是code - number ...類推
*/
exports.Chapter = mongolass.model('Chapter', {
  bookNum: {
    type: 'string'
  },
  start: {
    type: 'number'
  },
  end: {
    type: 'number'
  },
  chapters: [{
    code: {
      type: 'number'
    },
    filePath: {
      type: 'string'
    },
    index: {
      type: 'number'
    }
  }]
});

//抓取一次章節(jié)的模型
exports.Chapter.index({
  bookNum: 1
}, {
  unique: true
}).exec(); // 根據(jù)書本編號(hào)找到書本的章節(jié)沐旨,用戶名全局唯一

添加模型

Book

const Book = require('../lib/mongo').Book;

module.exports = {
  // 保存章節(jié)內(nèi)容
  create: (book) => {
    return Book.create(book).exec();
  },
  //通過書編號(hào)獲取記錄
  getBookByBookNum: (bookNum) => {
    return Book
      .findOne({
        bookNum: bookNum
      })
    .addCreatedAt()
      .exec();
  },
  //通過編號(hào)更新書數(shù)據(jù)
  updateBookByBookNum: (bookNum, book) => {
    return Book.update({
      bookNum: bookNum,
    }, {
      $set: book
    }).exec();
  },
};

Chapter

const Chapter = require('../lib/mongo').Chapter;

module.exports = {
  // 保存章節(jié)內(nèi)容
  create: (chapter) => {
    return Chapter.create(chapter).exec();
  },
  //通過書編號(hào)獲取記錄
  getChapterByBookNum: (bookNum) => {
    return Chapter
      .find({
        bookNum: bookNum
      })
      .addCreatedAt()
      .exec();
  },
  //通過抓取結(jié)果序號(hào)獲取記錄
  getChapterById: (id) => {
    return Chapter
      .findOne({
        _id: id
      })
      .addCreatedAt()
      .exec();
  },
  updateChapterByBookNum: (id, chapter) => {
    return Chapter.update({
      _id: id
    }, {
      $set: chapter
    }).exec();
  },
};

測試??(暫未使用斷言庫進(jìn)行標(biāo)準(zhǔn)的測試)

const BookModel = require('../model/Books.js');
const ChapterModel = require('../model/Chapters.js');



var testStoreBook = async() => {
    //模擬數(shù)據(jù)
    let data = {
            bookNum: "4445",
            url: "www.google123.com",
            chapters: [{
                index: 5,
                link: "333",
                title: "123132"
            }, {
                index: 6,
                link: "333",
                title: "123132"
            }, {
                index: 7,
                link: "333",
                title: "123132"
            }]
        },
        bookNum = "4445"

    try {
        var query = await BookModel.getBookByBookNum(bookNum);
        // var result = await BookModel.create(data);
    } catch (e) {
        console.log(e)
    }
    console.log(result.result.ok)
        // process.exit()
}
var testStoreChapters = async() => {
        //模擬數(shù)據(jù)
        let data = {
                bookNum: "4445",
                start: 0,
                end: 10,
                chapters: [{
                    index: 5,
                    code: 1,
                    filePath: "123132"
                }, {
                    index: 6,
                    code: 1,
                    filePath: "123132"
                }, {
                    index: 7,
                    code: 1,
                    filePath: "123132"
                }]
            },
            bookNum = "4445"

        try {
            // var result = await ChapterModel.updateChapterByBookNum(bookNum, data);
            var result = await ChapterModel.getChapterByBookNum(bookNum);
            console.log(result)
        } catch (e) {
            console.log(e)
        }
    // console.log(result.result.ok)
            // process.exit()
    }
    (async function() {
        try {
            // await testStoreChapters()
            // var query = await testStoreBook()
            var query = await testStoreChapters()
        } catch (e) {
            console.log(e.message)
        }
    })()
    

結(jié)合mongolass保存抓取數(shù)據(jù)

存儲(chǔ)章節(jié)信息

const BookModel = require('./model/Books.js');
// ...
if (!dataList || data.length <= 0) {
        return
    }
    /*儲(chǔ)存數(shù)據(jù)*/
    let book = {
            bookNum: data.bookNumber,
            url: data.url,
            chapters: dataList,
        },
        result = await BookModel.create(book);
    console.log(result)
//...

輸出結(jié)果


14996015073753.jpg

存儲(chǔ)章節(jié)內(nèi)容


const ChapterModel = require('./model/Chapters.js');

//....
    try {
        fetchResult = await delayAsync(dataList, start, end, limit);
        console.log(fetchResult)
        var chapters = await Chapter.create({
            bookNum: data.bookNumber,
            start: start,
            end: end,
            chapters: fetchResult,
        });
        console.log(chapters)
    } catch (e) {
        console.log(e)
    }

輸出結(jié)果

image.png

反思

目前感覺總體設(shè)計(jì)上并不是十分合理。

書本的章節(jié)可以捕獲一次保存在數(shù)據(jù)庫中,輸入書本后判斷書本是否已經(jīng)捕獲過章節(jié)了

捕獲過就從數(shù)據(jù)庫里獲取需要的章節(jié),提供方法檢驗(yàn)是否有最新章節(jié),

以文本形式儲(chǔ)存閱讀并不方便,如何更方便的閱讀

在大量捕獲的時(shí)候仍會(huì)被封停,缺少應(yīng)對封停的機(jī)制

添加phantom proxy 進(jìn)行代理,這里引出需要寫一個(gè)抓取代理并測試的服務(wù)來提供代理池

(ps =,=寢室只能用熱點(diǎn)上網(wǎng) 實(shí)在網(wǎng)絡(luò)不順暢)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末剂公,一起剝皮案震驚了整個(gè)濱河市希俩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌纲辽,老刑警劉巖颜武,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異拖吼,居然都是意外死亡鳞上,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進(jìn)店門吊档,熙熙樓的掌柜王于貴愁眉苦臉地迎上來篙议,“玉大人,你說我怎么就攤上這事」砑” “怎么了移怯?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長这难。 經(jīng)常有香客問我舟误,道長,這世上最難降的妖魔是什么姻乓? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任嵌溢,我火速辦了婚禮,結(jié)果婚禮上蹋岩,老公的妹妹穿的比我還像新娘赖草。我一直安慰自己,他們只是感情好剪个,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布秧骑。 她就那樣靜靜地躺著,像睡著了一般禁偎。 火紅的嫁衣襯著肌膚如雪腿堤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天如暖,我揣著相機(jī)與錄音,去河邊找鬼忌堂。 笑死盒至,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的士修。 我是一名探鬼主播枷遂,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼棋嘲!你這毒婦竟也來了酒唉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤沸移,失蹤者是張志新(化名)和其女友劉穎痪伦,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體雹锣,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡网沾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蕊爵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辉哥。...
    茶點(diǎn)故事閱讀 40,110評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出醋旦,到底是詐尸還是另有隱情恒水,我是刑警寧澤,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布饲齐,位于F島的核電站钉凌,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏箩张。R本人自食惡果不足惜甩骏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望先慷。 院中可真熱鬧饮笛,春花似錦、人聲如沸论熙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽脓诡。三九已至无午,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間祝谚,已是汗流浹背宪迟。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留交惯,地道東北人次泽。 一個(gè)月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像席爽,于是被迫代替她去往敵國和親意荤。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評論 2 355

推薦閱讀更多精彩內(nèi)容