《简单爬虫》——抓取一部小说
优采云 发布时间: 2022-06-05 02:52《简单爬虫》——抓取一部小说
例子是基于nodejs的,也不是很复杂,有兴趣的可以自己也来敲敲代码。
情不知从何起
很多人喜欢手机阅读,当然我也是,不过我比较喜欢看一些盗墓偏玄幻类的小说,比如《鬼吹灯》、《盗墓笔记》这样的。现在用手机来阅读也是很方便的事情,下个app就好了,什么起点、追书神器、QQ阅读之类的,但是这些多数都是要收费的,特别是那种还未完结的。收费还贵,1章少一点的5毛,多一点的那种要收1块,这类小说不写个1000多章都不好意思出来混。
对于我这样的免(dao)费(ban)程序受益者,怎么可能花那么大的代(jin)价(qian)来支持他。所以,第一原则:百度找免费的。百度一下果然有免费的,而且还是txt的,下载完成。开始看书。
当然,不可能所有事情都是那么顺利就解决的。比如下载的txt有时候会少章节,有时候少内容,看的不够尽兴。然后我又开始找网站了,一搜一大把,然后问题又来了,一章一个页面,每次看好一章就要去刷一下页面,每次还会加载很多小广告,这里的小广告就厉害了,不点掉会挡住部分文字,点一下就不知道跳转到哪里去了,有时候点击下一章也会是这样的。这流量刷刷刷的就没有了,对于我这样的程(qiong)序(diao)员(si),一个月300M流量不够用啊。
怎么办呢?毕竟咱们是个程序员啊,想点办法吧。那么我们用爬虫把页面里面的文字爬下来就好了嘛,于是就又了今天的内容了《简单爬虫》——抓取一部小说。
而一往情深
选角
爬虫当然有很多可以选择的,各种后端语言(java、.net)、脚本语言(php、nodejs、python),很多都可以的,但是作为一名前端工程师,在现在这个大环境下,多少需要知道一些nodejs,就算不会,但是至少还是需要知道的,不然怎么能成为一名合格的前端工程师呢?所以这次试着用用nodejs。
开搞
首先,我们要找到一个我们需要抓取的文章页面。这里抓一下天蚕土豆的《大主宰》。天蚕土豆应该都知道吧,当年的《斗破苍穹》就是这家伙写的,火的不行。
思路
我们需要抓取每一章小说内容,然后将每一章按照顺序排列好之后,写入文件——大主宰.txt中。好了思路大致就这样,接下来开撸代码。
var superagent = require("superagent")//用superagent去抓取
var cheerio = require("cheerio");//分析网页结构
//大主宰第一章页面
var first_url = "";
superagent.get(first_url).end(function(err,sres){
var $ = cheerio.load(sres.text);
var con = $("#content").text();
consoe.log(con);
})
先说一下代码superagent是一个http的库,这边我们用来发起http请求。然后cheerio这东西就更好了,说白了就是一个node版的jquery,这里我通过页面的代码结构知道文章都在一个ID为content的div里面。
运行一下居然是一堆乱码。。。一看就知道是编码问题了,看一下啊html编码是gbk的,一般我们的网站都是utf-8的,但是很多小说的网站都是gbk的编码,这是为什么呢?查看一下GBK编码:是指中国的中文字符,它包含了简体中文与繁体中文字符,另外还有一种字符“gb2312”,这种字符仅能存储简体中文字符。GBK编码格式,它的功能少,仅限于中文字符,当然它所占用的空间大小会随着它的功能而减少,打开网页的速度比较快。
知道为什么用gbk了,现在就开始转码吧
var superagent = require("superagent")//用superagent去抓取
var cheerio = require("cheerio");//分析网页结构
require('superagent-charset')(superagent);//引入gbk转换插件
//大主宰第一章页面
var first_url = "";
superagent.get(first_url).charset('gbk').end(function(err,sres){
var $ = cheerio.load(sres.text);
var con = $("#content").text();
console.log(con);
})
哈哈,搞定了,单个页面抓取成功了,那么就剩多页面抓取啦。
思路:列表页抓取单个章节的文章链接,然后再去抓取单页的文章,最后拼接写入文件。恩,思路可行,那么我们来看一下列表页面...
抓取list下的里面的a标签的href即可
//大主宰列表页面
var list_url = "";
superagent.get(list_url).end(function(err,sres){
var $ = cheerio.load(sres.text);
var listurl = new Array();//存放单章的url
$("#list dl dd a").each(function(idex,element){
var $ele = $(element);
listurl.push(list_url+$ele.attr("href").replace("/0_757/",""));
});
console.log(listurl);
})
我们这里打印一下列表页面的链接地址看一下。
恩 打印的链接没问题
superagent.get(list_url).end(function(err,sres){
var $ = cheerio.load(sres.text);
var listurl = new Array();//存放单章的url
$("#list dl dd a").each(function(idex,element){
var $ele = $(element);
listurl.push(list_url+$ele.attr("href").replace("/0_757/",""));
});
var content = "";//最终要的文章内容
var tmplisturl = listurl.slice(0,10);
tmplisturl.forEach(function(item){
superagent.get(item).charset('gbk').end(function(err,sres){
var $ = cheerio.load(sres.text);
var con = $("#content").text();
content+=con;
})
});
console.log(content)
});
那么来看一下代码多了一个循环,但是我打印出来的content居然是空...why?哦原来这个superagent抓取的时候是一个异步操作。所以我需要一个*敏*感*词*事件来告诉我,异步完成了才行。
我们这里用eventproxy,用来处理异步协作是很有用的,有兴趣的可以去#%E9%87%8D%E5%A4%8D%E5%BC%82%E6%AD%A5%E5%8D%8F%E4%BD%9C 学习一下。
superagent.get(list_url).end(function(err,sres){
var $ = cheerio.load(sres.text);
var listurl = new Array();//存放单章的url
$("#list dl dd a").each(function(idex,element){
var $ele = $(element);
listurl.push(list_url+$ele.attr("href").replace("/0_757/",""));
});
var content = "";//最终要的文章内容
var tmplisturl = listurl.slice(0,10);
tmplisturl.forEach(function(item){
superagent.get(item).charset('gbk').end(function(err,sres){
var $ = cheerio.load(sres.text);
var name = $(".bookname h1").text();
var con = $("#content").text().replace(/\n/g,'');
content+=name+"\n"+con+"\n";
ep.emit("finish");
})
});
ep.after("finish",tmplisturl.length,function(){
console.log(content);
});
});
ok啦!文章都能打印出来了,这里试了一下搞了前面10篇,加了点去除换行之类的小东西。但是还是有点问题,看下面的打印截图:
因为是异步的原因,所以没有按照我们原来设想的那样按照章节的顺序输出。想想是不是得先用对象保存起来然后再来排序输出呢?
tmplisturl.forEach(function(item){
superagent.get(item.url).charset('gbk').end(function(err,sres){
var $ = cheerio.load(sres.text);
var name = $(".bookname h1").text();
var con = $("#content").text().replace(/\n/g,'');
item.con = name+"\n"+con+"\n";
ep.emit("finish");
})
});
ep.after("finish",tmplisturl.length,function(){ //模拟10次
var content = "";
for(var i=0;i
content+=tmplisturl[i].con;
}
console.log(content);
});
试了一下果然ok啦。打完收工,领盒饭~
哦,还有最后需要写入文件
//'a+' - 以读取和追加模式打开文件。如果文件不存在,则会被创建
fs.open("大主宰.txt",'a+',function(){
fs.appendFile("大主宰.txt",content,function(){
console.log("写完了");
});
})
这里还有一点要提一下,别一次写入太多东西到文件中,内存不足会崩溃的。我这边做了一下分割循环。大概是30章一分割,也就是每次请求30章内容然后写入文件,待文件写入成功后继续请求30章,直到完成为止。但是我这边却只是写了600多章就没有了,这让我很惆怅...检查一下代码好像没什么问题,然后再次跑了一次,依然是600章不到的地方不动了..
看看打印的log信息:
想了想是不是我的内存不够用啊?看了一下,还真是这么一回事!
然后开始清理一下自己开的东西。微信,chrome,qq,outlook,sourcetree,phpstorm关了,然后瞬间清爽了。
剩余了10个G,继续跑一遍试试看,靠...还是不行,这次比之前的都少了,才200多章就停了...还不是内存占用的问题,那就是异步写入导致的I/O阻塞?
fs.appendFile("大主宰.txt",content,function(){
console.log("写入成功"+go)
if(go){
setTimeout(function(){
write();
},1000);
}else{
console.log('完成');
}
});
改成这样试试看,为了测试猜想,我只是写入了文章的标题。看一下运行结果:
还真的可以啊,当然这里的用的setTimeout,回头改成文件的关闭事件就好了。
完整代码
var superagent = require("superagent")//用superagent去抓取
var cheerio = require("cheerio");//分析网页结构
var eventproxy = require("eventproxy");//控制并发
require('superagent-charset')(superagent);//引入gbk转换插件
var list_url = "";//大主宰列表页面
var ep = new eventproxy();//得到一个 eventproxy 的实例
var fs = require("fs");//文件模块
console.time("start")
superagent.get(list_url).end(function(err,sres){
var $ = cheerio.load(sres.text);
var listurl = new Array();//存放单章的url
$("#list dl dd a").each(function(idex,element){
var $ele = $(element);
var tmpurl = $ele.attr("href");
var tmpid = tmpurl.replace("/0_757/","").replace(".html","")*1;
var tmp = {
url:list_url+tmpurl.replace("/0_757/",""),
id:tmpid,
con:""
}
listurl.push(tmp);
});
write();
function write(){
var tmplisturl = listurl.splice(0,30);
var go = true;
if(tmplisturl.length