內容要點
一步一腳印實現一個爬蟲,前面的內容比較簡單,有了解可以直接跳過而且文章內容較長和多圖,建議在pc下閱讀
源碼地址
phantomjs捕獲內容
詳細介紹通過async.mapLimit并發處理,結合定時器進行延時執行
數據存放到mongodb
數據輸出成文件
(如有錯誤請大家指出,一起學習)
介紹(有了解可以直接跳過)
關于PhantomJS
首先介紹一下phantomjs
PhantomJS是一個基于WebKit的服務器端JavaScript API,它基于 BSD開源協議發布。PhantomJS無需瀏覽器的支持即可實現對Web的支持,且原生支持各種Web標準,如DOM 處理、JavaScript、CSS選擇器、JSON、Canvas和可縮放矢量圖形SVG。PhantomJS主要是通過JavaScript和CoffeeScript控制WebKit的CSS選擇器、可縮放矢量圖形SVG和HTTP網絡等各個模塊。
phantomjs的應用場景
無需瀏覽器的Web測試:無需瀏覽器的情況下進行快速的Web測試,且支持很多測試框架,如YUI Test、Jasmine、WebDriver、Capybara、QUnit、Mocha等。
頁面自動化操作:使用標準的DOM API或一些JavaScript框架(如jQuery)訪問和操作Web頁面。
屏幕捕獲:以編程方式抓起CSS、SVG和Canvas等頁面內容,即可實現網絡爬蟲應用。構建服務端Web圖形應用,如截圖服務、矢量光柵圖應用。
網絡監控:自動進行網絡性能監控、跟蹤頁面加載情況以及將相關監控的信息以標準的HAR格式導出。
基于phantomjs2.0進行實現有兩種實現方案,一種是使用基于全局的 http://phantomjs.org/ ,另外一種是封裝的模塊 phantom - Fast NodeJS API for PhantomJS -https://github.com/amir20/phantomjs-node
這里選擇phantomjs-node
關于phantomjs-node的安裝以及入門
使用可以根據百度前端學院2017中的網頁抓取分析服務系列相關內容中學習,這里放一下之前phantomjs-node 學習的筆記和demo
中的phantomjs_1~4目錄下
后文也會進一步說明使用方法。
實現思路和過程
實現思路
phantomjs就相當于一個無圖形界面的瀏覽器,那么我們提供連接給phantomjs就意味著我們能獲取這個url的內容。
這次爬蟲的內容是希望獲取到小說的所有章節以及其內容,直接以筆閣網為例,因為這次爬蟲是直接爬筆閣網的。
我們打開http://www.qu.la/book/5443,
![Uploading 14995025771427_828375.jpg . . .]## 內容要點
一步一腳印實現一個爬蟲,文章內容較長,建議在pc下閱讀
源碼地址
phantomjs捕獲內容
詳細介紹通過async.mapLimit并發處理,結合定時器進行延時執行
數據存放到mongodb
數據輸出成文件
(如有錯誤請大家指出,一起學習)
介紹(有了解可以直接跳過)
關于PhantomJS
首先介紹一下phantomjs
PhantomJS是一個基于WebKit的服務器端JavaScript API,它基于 BSD開源協議發布。PhantomJS無需瀏覽器的支持即可實現對Web的支持,且原生支持各種Web標準,如DOM 處理、JavaScript、CSS選擇器、JSON、Canvas和可縮放矢量圖形SVG。PhantomJS主要是通過JavaScript和CoffeeScript控制WebKit的CSS選擇器、可縮放矢量圖形SVG和HTTP網絡等各個模塊。
phantomjs的應用場景
無需瀏覽器的Web測試:無需瀏覽器的情況下進行快速的Web測試,且支持很多測試框架,如YUI Test、Jasmine、WebDriver、Capybara、QUnit、Mocha等。
頁面自動化操作:使用標準的DOM API或一些JavaScript框架(如jQuery)訪問和操作Web頁面。
屏幕捕獲:以編程方式抓起CSS、SVG和Canvas等頁面內容,即可實現網絡爬蟲應用。構建服務端Web圖形應用,如截圖服務、矢量光柵圖應用。
網絡監控:自動進行網絡性能監控、跟蹤頁面加載情況以及將相關監控的信息以標準的HAR格式導出。
基于phantomjs2.0進行實現有兩種實現方案,一種是使用基于全局的 http://phantomjs.org/ ,另外一種是封裝的模塊 phantom - Fast NodeJS API for PhantomJS -https://github.com/amir20/phantomjs-node
這里選擇phantomjs-node
關于phantomjs-node的安裝以及入門
使用可以根據百度前端學院2017中的網頁抓取分析服務系列相關內容中學習,這里放一下之前phantomjs-node 學習的筆記和demo
中的phantomjs_1~4目錄下
后文也會進一步說明使用方法。
實現思路和過程
實現思路
phantomjs就相當于一個無圖形界面的瀏覽器,那么我們提供連接給phantomjs就意味著我們能獲取這個url的內容。
這次爬蟲的內容是希望獲取到小說的所有章節以及其內容,直接以筆閣網為例,因為這次爬蟲是直接爬筆閣網的。
我們打開http://www.qu.la/book/5443,
上面就有這本小說的許多章節,所以就有了第一步,或者這個頁面上所有章節,通過"開發者工具"中的檢查共功能
我們可以看到知道內容是這樣的結構
<div id ="list">
<dd>
<a href="/**">第xx章</a>
</dd>
....
</div>
所以只要我們獲取 id為list 中所有的dd,就獲取了小說的所有章節,同時通過dd中a標簽的href屬性就可以連接到所有章節的內容。
爬蟲方面的思路說明到這里
實現過程
(請保證node版本高于7.9,本文基于7.10.0)
(最好先了解es7中async/await 以及child_process)
如何使用phantomjs-nodejs
如何運行代碼?。。
將代碼保存在一個js文件中例如test.js
然后運行
node test.js
const phantom = require('phantom');//導入模塊
//async解決回調問題,es7的內容
(async function() {
// await解決回調問題,創建一個phantom實例
const instance = await phantom.create();
//通過phantom實例創建一個page對象,page對象可以理解成一個對頁面發起請求和處理結果這一集合的對象
const page = await instance.createPage();
//頁面指向的是哪個一個url
await page.on("onResourceRequested", function(requestData) {
console.info('Requesting', requestData.url)
});
//得到打開該頁面的狀態碼
const status = await page.open('https://stackoverflow.com/');
console.log(status);
//輸出該頁面的內容
const content = await page.property('content');
console.log(content);
//輸出內容
//退出該phantom實例
await instance.exit();
}());
輸出結果
當然不可能直接使用這些內容,所以就需要通過
//這個方法,我的理解是跟你在chrome中的輸出臺的操作是一樣的所以看看下面栗子
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();
}
}());
輸出結果
模塊實現
獲取所有章節fetchAllChapters.js
const phantom = require('phantom');
const program = require('commander');
/*
命令行參數幫助工具
設置 option b 代表 book ,[book]表示該參數可以通過program訪問,這個參數表示書本編號
命令 eg:
node fetchAllChapters.js -b 5443
*/
program
.version('0.1.0')
.option('-b, --book [book]', 'book number')
.parse(process.argv);
//缺少書本參數直接退出
if (!program.book) {
return
}
// example "5443",獲取書本編號
const bookNumber = program.book
//訪問的url
const url = encodeURI(`http://www.qu.la/book/${bookNumber}/`);
//設置用戶代理頭
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環境
(async function() {
//創建實例
const instance = await phantom.create()
//創建頁面容器
const page = await instance.createPage()
//設置
page.setting("userAgent", userAgent)
//判斷是否訪問成功
const status = await page.open(url),
code = 1;
if (status !== 'success') {
//訪問失敗修改狀態碼
code = -1;
} else {
//獲取當前時間
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));
}
//退出實例
await instance.exit();
})()
} catch (e) {
console.log(e)
}
輸出結果
在獲取所有章節之后,我們需要獲取所有章節的內容了
fetchChapter
const phantom = require('phantom');
const mkdirp = require('mkdirp')
const program = require('commander');
const fs = require('async-file')
const path = require('path')
//設置用戶代理
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`
/*
命令行參數
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和 標簽
*/
function puer(str) {
if (!str) {
return
}
str = str.replace(/<br\s*\/?>/gi, "\r\n");
str = str.replace(/ /g, " ")
return str
}
/*
test url
node fetchChapter.js -u http://www.qu.la/book/5443/3179374.html -f -p
*/
(async function() {
//創建實例
const instance = await phantom.create()
//創建頁面容器
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() {
//移除一些無關內容(等于直接在結果網頁上的dom上進行操作)
//請注意這里如果調用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 == '') {
//內容為空捕獲失敗
console.log(JSON.stringify({
code: -1
}))
return
} else {
//判斷參數進一步處理
if (program.puer) {
var context = puer(result.content)
}
//文件模式處理后進行保存到文件.返回文件路徑
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")
//可以導入其他js lib
await page.render('germy.png');
//渲染當前頁面為圖片輸出
在這里說一下為什么可以直接使用jquery,以百度為例子
因為當前頁面加載的時候加載了jquery 這個lib,所以這里就可以直接使用了
結合使用
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 是章節開始(下標是0,所以需要手動減一,第一章就是 0)
e 是結束章節數
l 是并發數
m 模式
b 書的編號
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);
/*
第一步獲取章節連接,第二部獲取章節內容并進行輸出
輸出方式一 輸出到數據庫.(未實現)
輸出方式二 文件輸出(在關注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
//調取子進程 執行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)
//分發任務 每10s調取一次并發抓取10條記錄
//截取需要的章節數
/*根據章節,章節是一開始,默認無序章*/
//dataList, start, end, limit
//下面是把要抓取的內容放置到delayAsync中,后文講述delayAsync
try {
fetchResult = await delayAsync(dataList, parseInt(start), parseInt(end), parseInt(limit));
} catch (e) {
console.log(e)
}
})()
這里是將兩個模塊結合起來,先抓取所有章節數再進行處理
這里使用async-child-process調起子進程,然后直接獲取輸出在控制臺中的數據作為輸出結果,由于async-child-process默認控制臺輸出的最大字節流是5kb所以要調整最大字節流的限制,不然會報錯;
結合async 與計時器實現延遲并發加載
這里先要說一下async.js這個庫提供了許多控制并發的方法,關于async的demo可以看一下唐大大的async demo,里面有許多async method 的使用 ??
而我們在這里使用的是 async.mapLimit()
/*
mapLimit(coll, limit, iteratee, callbackopt)
params coll 是數據集合
limit 并發數量
iteratee 迭代器fun(fun 提供item 和callback,通過ca)
callcackopt collection執行完畢或者是錯誤出現執行的回調函數
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進行了封裝。主要是增加了秒鐘的輸出,通過秒數的差值方便大家對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的調用是在error或者全部完成后調用的,result里放著的是每次callback(null,result)調用的結果以數組的形式儲存,注意如果某個函數沒有使用該回調,在結果里顯示就是undefined
至于結束后仍輸出,就是異步機制的問題(或者說是cpu調度問題?),已經調起了控制臺的輸出后 callcackopt才調用
大概了解async.mapLimit的使用后來看一下目前我的實現和存在的問題
const async = require('async')
const execAsync = require('async-child-process').execAsync;
/*實現并發抓取的函數*/
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) => {
//需要設置延時不然ip會被封掉
let cmd = `node fetchChapter.js -u ${data.link} -f -p`,
json,
//獲取一個內容就輸出一個
{
stdout
} = await execAsync(cmd, {
//default value of maxBuffer is 200KB.
maxBuffer: 1024 * 500
});
/*將內容保存到json中*/
json = JSON.parse(stdout);
//保存index
json.index = data.index;
/*
由于設置成了async,出現了多次觸發err的情況,callback 不能正常工作,
手動推入result中,但是這樣順序是不確定的,有待解決這個問題
*/
result.push(json);
callback(null, json) //not work
}, function(err) {
//回調函數在全部都執行完以后執行
if (err) {
reject(err)
}
resolve(result)
})
})
}
/*實現延時加載的函數*/
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) {
//數據長度為空就返回
reject("error")
return;
}
//打印一下輸入情況
console.log(dataList)
try {
/*章數的開始和結束*/
console.log(`從${start}到 ${end}`)
let startIndex = start,
endIndex;
while (startIndex != end) {
/*
需要注意的是當剩余的任務不足以達到并發數的時候
要保證任務分割不能出界
*/
if (startIndex + limit < end) {
endIndex = startIndex + limit;
} else {
//截取出界
endIndex = end;
}
/*分割任務*/
chapter = dataList.slice(startIndex, endIndex);
//通過閉包實現IIFE保存當時抓取的情況,不使用閉包綁定的數據則是運行之后的值
(function(startIndex, endIndex, chapter) {
//通過tempTimer 保存下來
let tempTimer = setTimeout(async function() {
//獲得此次任務開始執行的時間
let startTime = new Date(),
time, chapterResult = [];
//進行并發捕獲執行命令
try {
chapterResult = await asyncFetch(chapter, limit);
} catch (e) {
// console.log(e)
}
result = result.concat(chapterResult)
//用于判斷任務標記
counter++;
time = new Date() - startTime;
console.log(`完成抓取 ${startIndex} 到 ${endIndex} 計數器是${counter} 時間是${time}`)
}, i * 1000);
fetchTimers.push(tempTimer);
})(startIndex, endIndex, chapter)
i++; //控制延時
//推進任務進行
startIndex = endIndex;
}
} catch (e) {
reject(e)
}
/*定時判斷任務是否完成*/
checkTimer = setInterval(function() {
console.log(`counter is ${counter} count is ${count}`)
if (counter == count) {
//清除定時器
clearTimeout(checkTimeOut);
//清除定時器
clearInterval(checkTimer);
resolve(result)
}
}, 1000);
//or use promise all ?
//30s計時器判斷超時,超時時間暫做距離
checkTimeOut = setTimeout(function() {
//超時清除所有定時器
for (let i = 0; i < fetchTimers.length; i++) {
clearTimeout(fetchTimers[i]);
}
//清除定時判斷
clearInterval(checkTimer);
console.log("timout")
reject(result)
}, 30000);
})
}
module.exports = {
asyncFetch: asyncFetch,
delayAsync: delayAsync,
}
目前在async中存在問題,callback函數不能正常工作,所以每次都是手動將結果推入結果集,導致結果集的順序不能和原數據順序對應,
然而async官方文檔中
The callback must be called exactly once, ideally on a later tick of the JavaScript event loop.
至少要調用一次callback? 但是
在延時并發中考慮用await Promise.all[] 取代定時器判斷任務是否結束
輸出結果
儲存到mongodb
這里使用的數據庫驅動模塊是 mongolass
第一步配置mongolass并添加模型
const Mongolass = require('mongolass');
const moment = require('moment');
const objectIdToTimestamp = require('objectid-to-timestamp');
const mongolass = new Mongolass();
//儲存的庫的url
mongolass.connect('mongodb://localhost:27017/novel');
// 根據 id 生成創建時間 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 對象數組 - 對象的屬性是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(); // 根據書本編號找到書本的章節,書編號全局唯一
/*
下面模型的意思是
Chapter表
字段 屬性
bookNum string
start number
end number
chapters 對象數組 - 對象的屬性是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'
}
}]
});
//抓取一次章節的模型
exports.Chapter.index({
bookNum: 1
}, {
unique: true
}).exec(); // 根據書本編號找到書本的章節,用戶名全局唯一
添加模型
Book
const Book = require('../lib/mongo').Book;
module.exports = {
// 保存章節內容
create: (book) => {
return Book.create(book).exec();
},
//通過書編號獲取記錄
getBookByBookNum: (bookNum) => {
return Book
.findOne({
bookNum: bookNum
})
.addCreatedAt()
.exec();
},
//通過編號更新書數據
updateBookByBookNum: (bookNum, book) => {
return Book.update({
bookNum: bookNum,
}, {
$set: book
}).exec();
},
};
Chapter
const Chapter = require('../lib/mongo').Chapter;
module.exports = {
// 保存章節內容
create: (chapter) => {
return Chapter.create(chapter).exec();
},
//通過書編號獲取記錄
getChapterByBookNum: (bookNum) => {
return Chapter
.find({
bookNum: bookNum
})
.addCreatedAt()
.exec();
},
//通過抓取結果序號獲取記錄
getChapterById: (id) => {
return Chapter
.findOne({
_id: id
})
.addCreatedAt()
.exec();
},
updateChapterByBookNum: (id, chapter) => {
return Chapter.update({
_id: id
}, {
$set: chapter
}).exec();
},
};
測試??(暫未使用斷言庫進行標準的測試)
const BookModel = require('../model/Books.js');
const ChapterModel = require('../model/Chapters.js');
var testStoreBook = async() => {
//模擬數據
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() => {
//模擬數據
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)
}
})()
結合mongolass保存抓取數據
存儲章節信息
const BookModel = require('./model/Books.js');
// ...
if (!dataList || data.length <= 0) {
return
}
/*儲存數據*/
let book = {
bookNum: data.bookNumber,
url: data.url,
chapters: dataList,
},
result = await BookModel.create(book);
console.log(result)
//...
輸出結果
存儲章節內容
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)
}
輸出結果
反思
目前感覺總體設計上并不是十分合理。
書本的章節可以捕獲一次保存在數據庫中,輸入書本后判斷書本是否已經捕獲過章節了
捕獲過就從數據庫里獲取需要的章節,提供方法檢驗是否有最新章節,
以文本形式儲存閱讀并不方便,如何更方便的閱讀
在大量捕獲的時候仍會被封停,缺少應對封停的機制
添加phantom proxy 進行代理,這里引出需要寫一個抓取代理并測試的服務來提供代理池
(ps =,=寢室只能用熱點上網 實在網絡不順暢)