上一篇我们讲述了爬取小说的基本逻辑,但是这样还是远远不够的。一来没有一些异常的处理,二来爬取速度过快还容易引起小说网站的反爬虫机制导致IP被禁,所以接下来我们一一把这些逻辑优化处理下,让我们更加优雅的爬取到我们想要的内容。
使用浏览器代理
无论怎样,爬取小说最好都是要使用代理服务器。这样即使触发了反爬虫机制对自身的IP也不会有太大的影响,这对开发阶段尤其重要,甚至对以后再次爬取也有不小的帮助。
原理也很简单
var browser = await puppeteer.launch({
headless:true,
args: [
'--proxy-server=socks5://127.0.0.1:1080'
]
})
在创建浏览器的时候添加--proxy-server
参数即可。当然也有很多其他可选的参数。具体可以参考chrome浏览器运行参数帮助。这里我提供一个chrome浏览器运行参数的参考网站,里面涵盖了几乎所有chrome的运行参数。chrome args helper
ok,原理知道后我们就去找代理的服务器吧。好在这方面网上一点也不缺,随便搜以下代理服务器,我就找到不少。这里提供一个网站,提供 免费代理服务器。
具体怎么去获取呢?这里也不绕弯子了。还是通过puppeteer来获得这里我们所需要的信息。
新建一个文件 proxyserver.js
const puppeteer = require('puppeteer')
//max retry times
const MAX_RT = 3;
function getproxylist() {
return new Promise(async (resolve, reject) => {
var tempbrowser;
for (var i = MAX_RT; i > 0; i--) {
if (tempbrowser) {
break;
}
console.log('start to init browser...');
tempbrowser = await puppeteer.launch({
headless:true
}).catch(ex => {
if (i-1 > 0) {
console.log('browser launch failed. now retry...');
} else {
console.log('browser launch failed!');
}
});
}
if (!tempbrowser) {
reject('fail to launch browser');
return;
}
const browser = tempbrowser;
console.log('start to new page...');
var page = await browser.newPage().catch(ex=>{
console.log(ex);
});
if (!page) {
reject('fail to open page!');
return;
}
var respond;
for (var i = MAX_RT; i > 0; i--) {
if (respond) {
break;
}
console.log('start to goto page...');
respond = await page.goto("https://www.socks-proxy.net/", {
'waitUntil':'domcontentloaded',
'timeout':120000
}).catch(ex=>{
if(i-1 > 0) {
console.log('fail to goto website. now retry...');
} else {
console.log('fail to goto website!');
}
});
}
if (!respond) {
reject('fail to go to website!');
return;
}
console.log('start to find element in page...');
var layoutVisible = await page.waitForSelector('#list .container table tbody').catch(ex=>{
console.log("oh....no...!!!, i can not see anything!!!");
});
if (!layoutVisible) {
reject('layout is invisible!');
return;
}
console.log('start to get info from element...');
var proxyModelArray = await page.evaluate(async () => {
let list = document.querySelectorAll('#list .container table tbody tr');
if (!list) {
return;
}
let result = [];
for (var i = 0; i < list.length; i++) {
var row = list[i];
var cells = row.cells;
var ip = cells[0].textContent;
var port = cells[1].textContent;
var code = cells[2].textContent;
var version = cells[4].textContent;
var proxyServerModel = {
'ip' : ip,
'port' : port,
'code' : code,
'version' : version
}
result.push(proxyServerModel);
}
return result;
});
await browser.close().catch(ex=>{
console.log('fail to close the browser!');
});
console.log('close the browser');
//console.log(proxyModelArray);
if (!proxyModelArray || proxyModelArray.length === 0) {
reject();
return;
}
resolve(proxyModelArray);
})
}
// async function test() {
//
// var proxylist;
// for (var i = 0; i < MAX_RT; i++) {
//
// if (proxylist) {
// break;
// }
//
// console.log('start get proxylist from web...');
// proxylist = await getproxylist().catch(ex=> {
// if (i+1<MAX_RT) {
// console.log('fail to get proxylist. now retry...');
// } else {
// console.log('fail to get proxylist. end!!!');
// }
// });
// }
// if (!proxylist) {
// console.log('fail to get proxylist!!!');
// return;
// }
// console.log(proxylist);
// }
// test();
module.exports.getProxyList = getproxylist;
把test的注释取消运行下可以得到以下结果:
start get proxylist from web...
start to init browser...
start to new page...
start to goto page...
start to find element in page...
start to get info from element...
close the browser
[ { ip: '103.214.200.58',
port: '1080',
code: 'BD',
version: 'Socks4' },
{ ip: '45.55.202.229',
port: '10080',
code: 'US',
version: 'Socks5' },
{ ip: '150.129.207.75',
port: '6667',
code: 'IN',
version: 'Socks5' },
{ ip: '72.250.134.235',
port: '6001',
code: 'US',
version: 'Socks4' },
......
这样我们就获取到代理服务器的最关键部分了。当然啦,这个网站上的代理对于国内的IP支持并不好,大家可以找国内的代理服务器网站来抓取,仿照来写一个。
设定重试次数并使用休眠
不知道大家发现没有,在 proxyserver.js 中,我们获取浏览器实例到打开页面,都使用了一个循环来处理。循环里面才是执行的逻辑。这个循环就是重试机制,他设定了最大重试次数 MAX_RT 。即失败会再次请求当前操作。
有这个必要吗?----还真的有!!我试过很多次尝试发现,很多小说网站就是矫情。第一次死活打不开。当然啦,循环也是为了更加稳健而已,一次就获取成功也会马上跳出循环的。
休眠代码如下
function sleep(time = 0) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, time);
})
}
使用方法是(注意只能在异步函数中使用)
(async ()=> {
//休眠3秒
await sleep(3000);
})()
休眠的作用就不说了,反正尽量装的像个人呗!!
启动多个浏览器实例来加快获取内容
人多力量大!1个人的力量总是有限的。同理1个浏览器抓取大量的内容速度也是有限的,而且风险也大。这里我同时启动了20个带代理服务器的浏览器实例,相当于20个IP不同的主机在同时抓取我们所需要的内容。前面我们获取到了代理服务器列表,这里可以这样用:
/**
* 获取代理浏览器
*/
function getProxyBrowser(proxyList) {
return new Promise(async (resolve, reject) => {
var browserList = [];
for (var i = 0; i < proxyList.length; i++) {
var proxyserver = proxyList[i];
var proxyOption = proxyserver.version.toLowerCase() + '://' + proxyserver.ip + ':' + proxyserver.port;
var browser = await puppeteer.launch({
headless:true,
args: [
//'--window-size="800,600"',
//'--start-fullscreen'
'--proxy-server='+proxyOption
]
}).catch();
if (browser) {
browserList.push(browser);
}
}
if (browserList.length == 0) {
reject()
return;
}
resolve(browserList);
})
}
调用这个方法可以获取到proxyList大小的代理浏览器实例的列表。这样我们就相当于有20个人同时帮我们干活了。之前我们获取到了小说的所有章节的url,这里就交给这20个代理浏览器去获取吧!
幸运的是nodejs是单进程的。这样我们也不用考虑同步的问题。这样步骤就简单了。
单个浏览器的任务是:
似乎到这里就已经结束了?不是的。这里还是有一些问题。
内容优化
很多小说网站的正文内容中都有一些令人很不爽的广告啊、推荐啊之类的东西,更有甚者正文中间都有奇怪的东西。这能忍吗?肯定不能啊!这里我们对获取到的内容进行一定的处理:举个栗子
/**
* 处理获取到的内容
* @unHandleContent 未处理的内容(可能会包含某些奇怪的东西,统统去掉,这里写去除逻辑。可以先调试一条数据试下效果)
*/
function handleContent(unHandleContent) {
var result = unHandleContent;
// result = result.replace(/ /g,' ');
// result = result.replace(/\n|\r/g, '<br>');
// result = result.replace(/<[a-z]{1,6}\s.*\/[a-z]{1,6}>/g, '');
// result = result.replace(/<br>/g, '\n');
return result;
}
ok,到这里就结束了。有什么问题的可以获取源码来看看。