js爬虫
最近想写一个爬虫脚本,查了查网上的脚本教程都是基于pyhton的比较多,而js就很少。问了问我的小伙伴js爬虫需要什么工具吗?大概需要的工具有axios,cheerio,当然也安装了node.js。
安装好了基本工具之后,我们就开始快乐coding了,axios是用来前后端交互比较多,这里我用来获取html,想到之前爬豆瓣的时候遇到ajax,页面元素并不是在渲染好了就有数据,导致拿不到想要的信息。
在网上看到了D版图书的网址,一看就是不会对我的爬虫有什么抵御措施,也正好练手。
首先第一步,我们要拿到html,打开网页分析一下页面元素,在这里我访问的链接是http://www.shuquge.com/txt/108449,耳根大大的书,并且寻找到小说章节的url是在.listmain dl dd a下
下面是我的代码
let instance = axios.create({
baseURL: 'http://www.shuquge.com/txt/108449',
timeout: 2500,
});
//得到html模板
async function getHtml() {
let html = await instance.get('index.html').then(res => res.data);
return getUrl(html);
}
//得到书本章节的url请求
function getUrl(html) {
let $ = cheerio.load(html);
let list = Array.from($('.listmain dl dd a'));
let urlArr = [];
// ele.attribs.href
list.forEach(ele => urlArr.push(ele.attribs.href));
return urlArr;
}
这两个函数执行后,返回的是这本小说的所有章节的url,这样我就离最后爬完整本书做了基础,剩下的就是访问这些url,获得具体小说信息,并且保存下来就好了。
function sendGetRequest(url) {
return instance.get(url);
}
async function getNovel() {
let urlArr = await getHtml();
fetchUrl(10, urlArr).then(res => {
console.log('结果为' + res.length);
for (let i = 0; i < res.length; i++) {
console.log(res[i]);
fs.appendFile('三寸人间.txt', res[i], function () {
(console.log('写入第' + i + '章' + '成功'))
})
}
})
}
这里的代码我已经修改过,原来的想法是,小说的抓取是顺序一章章抓取,那么写文件的时候的结果也是一章章。写个for循环,简单又能实现抓取小说的目的。
但是怎么能这么简单就结束呢?我的小伙伴和我说,搞个并发,一次发起n次请求效率比原来快得多。我上网查阅了这方面的资料,实在是很少。我设定了一个max值,然后发送请求就max++,如果到达n就不在发送新的。
这样想会出现几个问题,一、我的请求完成获取不到二、只能并发一次,达到上限之后就不会继续执行。
我开始想的很单纯,既然这样我就外面写个async 里面写个await 等待并发的请求结束,那其实就是和同步执行并无差别,只是等待一个批次,一个批次有n个请求, 等待这一个批次的请求都返回了,才会执行下一个批次的请求。 我对这样的结果并不满意。
接下来是我优化后的代码。
//howmany是一次抓取多少,urlArr是url数组
async function fetchUrl(howMany, urlArr) {
return new Promise((res, rej) => {
let result = [];//保存爬取记录
let count = 0;//记录url在数组中的坐标
let task = [];//任务队列
let remain = 0;//记录剩余未抓取的url
let doTask = (indexOfTasks, indexOfUrls = count) => {
//生成promise,并且执行
let req = sendGetRequest(urlArr[indexOfUrls]);
req.then(data => {
remain--;
if (count >= urlArr.length) {
if (remain === 0) return res(result);
return;
}
doTask(indexOfTasks);
result[indexOfUrls] = getContent(data.data);
count++;
remain++;
}, rej => {
console.log("请求失败" + rej);
doTask(indexOfTasks, indexOfUrls);
});
console.log('正在发送第' + count + '次请求' + '并发数为' + task.length);
task[indexOfTasks] = req;
}
for (let i = 0; i < howMany; i++) {
doTask(i);
count++;
remain++;
}
})
}
这里的内容就比较多了,先解释一下这个代码块。
for (let i = 0; i < howMany; i++) {
doTask(i);
count++;
remain++;
}
执行的时候发送howmany个请求,并记录发送的请求数,remain记录队列里还有多少未完成的请求。
比如我先发送10个请求,就相当于是叫10个工人干活,工人完成了自己的工作后,就让其他工人顶替自己的位置,而工位就只有10个。
工人完成自己的任务之后,吩咐自己的同事接下来要做什么样的工作,并且让同事坐到自己的工位上。
当工厂的剩余任务为0(remain等于零的时候) 工人就可以下班了。
但是因为是并发,所以你不知道哪个工人先完成任务,即小说的章节是乱序的,为此我又用闭包记录了当前url在数组中的位置,这样虽然是并发执行的,但是结果是按位置加入数组的。
解决了这个问题之后,我又在想万一出错了怎么办就用这个坐标值,拿到出错的url,重新获取小说内容。
爬取多部小说
完成了这个任务之后,我就在思考,如果每次都需要知道url地址,从而爬取某部小说的内容,这个脚本还不够智能。 于是我分析了首页小说的html,获取到每个版块榜首的小说url和名字,再通过这个url进行之前的操作,那不是就可以获取到这个小说网版块榜首的小说,但不用自己再去看Html。 说干就干,最后的代码是这样。
const cheerio = require('cheerio');
const axios = require('axios');
const fs = require('fs');
//获取首页热门小说url,并创建axios
async function getHotNovelUrl() {
let novel = {
Names: null,
Urls: null
};
let instance = axios.create({
baseURL: 'http://www.shuquge.com/',
timeout: 2500,
});
let result = await instance.get().then(res => res.data);
let $ = cheerio.load(result);
novel.Names = Array.from($('.block .image a img')).map(ele => ele.attribs.alt);
novel.Urls = Array.from($('.top dl dt a')).map(ele => 'txt/' + ele.attribs.href.match(/[0-9]/g).join('') + '/');
console.log(novel);
return novel;
}
//得到html模板,index代表axiosArr中的位置
async function getHtml(url) {
let instance = axios.create({
baseURL: 'http://www.shuquge.com/' + url,
timeout: 2500
})
let html = await instance.get('index.html').then(res => res.data);
return getUrl(html);
}
//得到书本章节的url请求
function getUrl(html) {
let $ = cheerio.load(html);
// let document.querySelectorAll(".listmain dl dd a")[0].getAttribute("href");
let list = Array.from($('.listmain dl dd a'));
let urlArr = [];
list.forEach(ele => urlArr.push(ele.attribs.href));
return urlArr;
}
function getContent(html) {
let $ = cheerio.load(html);
let content = $('.content h1').text() + $('#content').text();
return content;
}
function sendGetRequest(baseURL, url) {
console.log(baseURL, url);
let instance = axios.create({
baseURL: 'http://www.shuquge.com/'+ baseURL,
timeout: 2500
})
return instance.get(url);
}
async function getNovel() {
let novels = await getHotNovelUrl();
let urlArr = [];
for (let i = 0; i < novels.Urls.length; i++) {
await getHtml(novels.Urls[i]).then(result => urlArr.push(result));
}
console.log(urlArr);
for (let i = 0; i < urlArr.length; i++) {
await fetchUrl(50, urlArr[i], novels.Urls[i]).then(res => {
console.log('结果为' + res.length);
for (let j = 0; j < res.length; j++) {
console.log(res[j]);
fs.appendFile(novels.Names[i]+'.txt', res[j], function () {
(console.log('写入第' + j + '章' + '成功'))
})
}
})
}
}
//howmany是一次抓取多少,urlArr是url数组
async function fetchUrl(howMany, urlArr, baseURL) {
return new Promise((res, rej) => {
let result = []; //保存爬取记录
let count = 0; //记录url在数组中的坐标
let task = []; //任务队列
let remain = 0; //记录剩余未抓取的url
let doTask = (indexOfTasks, indexOfUrls = count) => {
//生成promise,并且执行
let req = sendGetRequest(baseURL, urlArr[indexOfUrls]);
req.then(data => {
remain--;
if (count >= urlArr.length) {
if (remain === 0) return res(result);
return;
}
doTask(indexOfTasks);
result[indexOfUrls] = getContent(data.data);
count++;
remain++;
}, rej => {
console.log("请求失败" + rej);
doTask(indexOfTasks, indexOfUrls);
});
console.log('正在发送第' + count + '次请求' + '并发数为' + task.length);
task[indexOfTasks] = req;
}
for (let i = 0; i < howMany; i++) {
doTask(i);
count++;
remain++;
}
})
}
getNovel();
希望能给大家带来帮助,另外这段代码仅为学习之用,不要给人家带来不必要的服务器负担。