async+phantomjs制作nodejs的小说爬虫

async+phantomjs制作nodejs的小说爬虫

内容要点

一步一脚印实现一个爬虫,文章内容较长,建议在pc下阅读

源码地址

phantomjs捕获内容 详细介绍通过async.mapLimit并发处理,结合定时器进行延时执行

数据输出成文件保存到本地 (如有错误请大家指出,一起学习)

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
.
├── README.md
├── asyncFetch.js
├── book
│   └── default
├── mock
│   └── bookInfo.json
├── fetchAllChapters.js
├── fetchChapter.js
├── note.md
├── package.json
├── taskHandler.js
├── test.js


介绍(有了解可以直接跳过)


关于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,

上面就有这本小说的许多章节,所以就有了第一步,或者这个页面上所有章节,通过"开发者工具"中的检查共功能

我们可以看到知道内容是这样的结构

1
2
3
4
5
6
<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 然后运行

1
node test.js

参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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();
}());

输出结果

当然不可能直接使用这些内容,所以就需要通过

1
2
//这个方法,我的理解是跟你在chrome中的输出台的操作是一样的所以看看下面栗子
await page.evaluate(function() {});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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和&nbsp标签
*/
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 () {
let code = 1
//创建实例
const instance = await phantom.create()
//创建页面容器
const page = await instance.createPage()
page.setting("userAgent", userAgent)
const status = await page.open(URL)
if (status !== 'success') {
code = -1;
return;
} else {
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 ({
novelName: $("#page_set").next().next().html(),
title: $("h1").html(),
content: $("#content").html()

});
})
if (result.title == '' || result.content == '') {
//内容为空捕获失败
console.log(JSON.stringify({
code: -1
}))
return
} else {
//判断参数进一步处理
if (program.puer) {
var context = '---\n' +
'layout: \'[literature]\'\n' +
'title: ' + result.title + '\n'+
'date: ' + new Date().getFullYear() + '-' + ((new Date().getMonth() + 1) > 10 ? (new Date().getMonth() + 1) : ('0' + (new Date().getMonth() + 1))) + '-' + (new Date().getDate() > 10 ? new Date().getDate() : ('0' + new Date().getDate())) + ' ' + new Date().getHours() + ':' + new Date().getMinutes() + ':' + new Date().getSeconds() + '\n' +
'tags: [文学,小说,'+ result.novelName +']\n' +
'copyright: true\n' +
'toc_number: true\n' +
'categories: [文学,小说,'+ result.novelName +']\n' +
'comments: true\n' +
'toc: true\n' +
'abbrlink: f6d1cfff\n' +
'description: ' + result.content.trim().substring(0,20) + '...' +'\n' +
'---'
context += puer(result.content)
}
//文件模式处理后进行保存到文件.返回文件路径
if (program.file) {

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

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


拓展


1
2
3
4
await page.includeJs("https://cdn.bootcss.com/jquery/1.12.4/jquery.js")
//可以导入其他js lib
await page.render('germy.png');
//渲染当前页面为图片输出

在这里说一下为什么可以直接使用jquery,以百度为例子

因为当前页面加载的时候加载了jquery 这个lib,所以这里就可以直接使用了


结合使用


taskHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
const exec = require('child_process').exec;
const execAsync = require('async-child-process').execAsync;
const async = require('async')
const delayAsync = require('./asyncFetch').delayAsync;
const program = require('commander');
var fs = require('fs');
const util = require('util')
const co = require('co')
const readAsync = util.promisify(fs.readFile)
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() {
let stdout = ''
await execAsync(cmd, {
//default value of maxBuffer is 200KB.
maxBuffer: 1024 * 500
});
console.log(111)
await fs.readFile('./mock/bookInfo.json',function(err,res){
if(err){
console.error('write error');
}
stdout = res.toString()
console.log(222)
console.log(333)
let data = JSON.parse(stdout),
start = parseInt(program.start),
end = parseInt(program.end),
limit = parseInt(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(444)
//分发任务 每10s调取一次并发抓取10条记录
//截取需要的章节数
/*根据章节,章节是一开始,默认无序章*/
//dataList, start, end, limit
//下面是抓每章内容
try {
fetchResult = delayAsync(dataList, start, end, limit);
console.log(fetchResult)
} catch (e) {
console.log(e)
}
})

})()
const exec = require('child_process').exec;
const execAsync = require('async-child-process').execAsync;
const async = require('async')
const delayAsync = require('./asyncFetch').delayAsync;
const program = require('commander');
var fs = require('fs');
const util = require('util')
const co = require('co')
const readAsync = util.promisify(fs.readFile)
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() {
let stdout = ''
await execAsync(cmd, {
//default value of maxBuffer is 200KB.
maxBuffer: 1024 * 500
});
console.log(111)
await fs.readFile('./mock/bookInfo.json',function(err,res){
if(err){
console.error('write error');
}
stdout = res.toString()
console.log(222)
console.log(333)
let data = JSON.parse(stdout),
start = parseInt(program.start),
end = parseInt(program.end),
limit = parseInt(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(444)
//分发任务 每10s调取一次并发抓取10条记录
//截取需要的章节数
/*根据章节,章节是一开始,默认无序章*/
//dataList, start, end, limit
//下面是抓每章内容
try {
fetchResult = delayAsync(dataList, start, end, limit);
console.log(fetchResult)
} catch (e) {
console.log(e)
}
})

})()

这里是将两个模块结合起来,先抓取所有章节数再进行处理

这里使用async-child-process调起子进程,然后直接获取输出在控制台中的数据作为输出结果,由于async-child-process默认控制台输出的最大字节流是5kb所以要调整最大字节流的限制,不然会报错;


结合async 与计时器实现延迟并发加载


这里先要说一下async.js这个库提供了许多控制并发的方法,关于async的demo可以看一下唐大大的async demo,里面有许多async method 的使用 🌰

而我们在这里使用的是 async.mapLimit()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
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
*/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//在看另外一段

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的使用后来看一下目前我的实现和存在的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
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[] 取代定时器判断任务是否结束

输出结果


获取章节保存到本地json文件中


这里使用的node的fs文件的fs.writeFile模块


反思


书本的章节可以捕获一次保存在/mock/bookInfo.json本地文件中,输入书本后判断书本是否已经捕获过章节了

捕获过就从本地文件/mock/bookInfo.json中获取需要的章节,提供方法检验是否有最新章节,

以文本形式储存阅读并不方便,如何更方便的阅读

在大量捕获的时候仍会被封停,缺少应对封停的机制

添加phantom proxy 进行代理,这里引出需要写一个抓取代理并测试的服务来提供代理池


参考


本文参考

---本文结束感谢您的阅读---

本文标题:async+phantomjs制作nodejs的小说爬虫

文章作者:zhangdongfei

发布时间:2019年04月26日 - 16:50:41

最后更新:2019年05月11日 - 09:45:16

原始链接:http://yoursite.com/2019/04/26/node-novel/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

请作者喝杯咖啡吧~
0%