一个用来爬小说的简单的Node.js爬虫
小说就准备点天下霸唱和南派三叔的系列,本人喜欢看,而且数据也好爬。貌似因为树大招风的原因,这两作者的的书被盗版的很多,乱改的也多。然后作者就直接在网上开放免费阅读了,还提供了官网,猜想作者应该是允许爬虫来爬内容的。《盗墓笔记》和《鬼吹灯》系列这两官网从第一眼的界面风格来看还差不多,最后发现还真是一个队伍开发的,服务器都是一个。因为最开始爬数据的时候两次请求之间没有间隔时间,请求太频繁了,然后突然就没法访问了。立马反映过来是不是因为服务器端的保护措施,导致被封IP了。然后在别的电脑上和手机上都还能继续访问,发现还真是被封IP了,大约要等30分钟才能解封。而且要是在爬《盗墓》数据的时候被封IP,访问《鬼吹灯》的站点也是被封了的,哈哈。后来每次爬章节内容的时候都间隔500毫秒,就没有被封过了,这个间隔感觉还可以更短些,只要不影响其他读者的正常访问都是允许的吧。爬数据的同时可以ping站点,若一直有返回,IP就没被封。
页面的结构很简单很语义化。每本小说的章节目录部分html结构都是这样的:
章节内容的结构:
获取数据非常方便,对爬虫很友好,中国好网站!
下面说下这个爬虫:
最开始准备直接用node.js + cheerio + request就搞定,爬数据不需要提供接口访问,甚至连express都不需要,直接做成一个命令行工具。最后想了想,还是不太行,因为后面数据爬下来以后,还要给APP端提供小说接口,所以还是需要一个完整的server端,而且还要和数据库交互,能有ORM最好,免得直接写SQL。于是想起了两年前曾经使用过得ThinkJS,现已经更新到2.2.x版本了。这是国内的一个基于Node.js的MVC框架。相比于Express或Koa来说提供了更强大和完善的功能,应该把它和Sails一起比较。多的就不介绍了,官网文档很全面。
先整理一个小说的条目,两个作者的小说加起来大致有28本:
// book.js
1 export default {
/**
* 天下霸唱 19本
*/
guichuideng_1: {
id: 1,
name: '鬼吹灯1之精绝古城',
uri: 'http://www.guichuideng.org/jing-jue-gu-cheng',
author: '天下霸唱',
publish_date: '2006-09',
publisher: '安徽文艺出版社',
cover: 'guichuideng_1',
},
guichuideng_2: {
id: 2,
name: '鬼吹灯2之龙岭迷窟',
uri: 'http://www.guichuideng.org/long-ling-mi-ku',
author: '天下霸唱',
publish_date: '2006-11',
publisher: '安徽文艺出版社',
cover: 'guichuideng_2',
},
guichuideng_3: {
id: 3,
name: '鬼吹灯3之云南虫谷',
uri: 'http://www.guichuideng.org/yun-nan-chong-gu',
author: '天下霸唱',
publish_date: '2006-11',
publisher: '安徽文艺出版社',
cover: 'guichuideng_3',
},
guichuideng_4: {
id: 4,
name: '鬼吹灯4之昆仑神宫',
uri: 'http://www.guichuideng.org/kun-lun-shen-gong',
author: '天下霸唱',
publish_date: '2006-12',
publisher: '安徽文艺出版社',
cover: 'guichuideng_4',
},
// ...... 省略
}
ThinkJS支持从命令行访问接口,这里直接把爬虫的实现做到了controller里,可以从命令行来直接调用这个接口,在package.json的scripts加一个命令可就能通过npm来调用。Node.js虽然在7.x以后都支持原生es6/7书写,但是还是需要harmony和谐模式来运行才可以,要不然一样报语法错误。而TinkJS运行前是先将src的代码用babel编译到app文件夹内再跑服务,实际运行的是降级编译后的js代码,所以es6/7语法可以随心所欲的写,而不用担心兼容问题。用ThinkJS命令行工具初始化了一个项目,并加入一个Npm命令:
"spider": "npm run compile && node www/production.js spider/index"
controller:
'use strict'; /**
* spider controller
*/ import Base from './base.js'; import rp from 'request-promise';
import cheerio from 'cheerio';
import books from './spider/book';
import {sleep, log} from './spider/tool'; export default class extends Base {
indexAction (){
if (this.isCli()){
this.checked = false;
this.spiderModel = this.model('book');
this.chapterModel = this.model('chapter');
this.crawlBook();
} else {
this.fail('该接口只支持在命令行调用~~~');
}
} async crawlBook (isCheck){
log('小说目录插入开始...');
// 小说先存入书籍表
var boookArr = [];
for (var x in books){
boookArr.push(books[x]);
}
await this.spiderModel.addBookMany(boookArr);
log('小说目录插入完成...');
log('小说内容抓取开始...');
// 循环抓取小说目录
for (var key in books){
var {id, name, uri} = books[key];
var bookId = id;
log(name + ' [章节条目抓取开始...]');
try {
var $ = await rp({
uri,
transform: body => cheerio.load(body)
});
var $chapters = $('.container .excerpts .excerpt a'); // 所有章节的dom节点
var chapterArr = []; // 存储章节信息
log(name + ' [章节条目如下...]');
$chapters.each((i, el) => {
var index = i + 1;
var $chapter = $(el); // 每个章节的dom
let name = $chapter.text().trim();
var uri = $chapter.attr('href');
log(name + ' ' + uri);
chapterArr.push({bookId, index, name, uri});
});
} catch (e){
return log(e.message, 1);
} log(name + ' [章节条目抓取完毕,开始章节内容抓取...]'); // 循环抓取章节内容
for (var i = 0,len = chapterArr.length;i < len;i ++){
var chapter = chapterArr[i];
// 先查询该章节是否已存在
// 爬取的途中断掉或者卡住了,再次启动蜘蛛的时候已存在的章节就不必再爬了
var res = await this.chapterModel.findChapter(chapter.name);
if (!think.isEmpty(res)){
log(name + ' [章节已存在,忽略...]');
continue;
}
await sleep(500);
await this.crawlChapter(chapter);
}
}
this.checked = isCheck; // 再检测一遍是否有遗漏
!this.checked && this.crawlBook(1);
} async crawlChapter ({bookId, index, name, uri}){
try {
log(name + ' [章节内容抓取开始...]');
var $ = await rp({
uri,
transform: body => cheerio.load(body)
});
var $content = $('.article-content'); // 只取正文内容
$('.article-content span').remove(); // 干掉翻页提示
var content = ' ' + $content.text().trim(); // 提取纯文本(不需要html标签,但保留换行和空格)
await this.chapterModel.addChapter({bookId, index, name, content, uri});
log(name + ' [章节内容抓取完毕,已写入数据库...]');
} catch (e){
log(e.message, 1);
}
}
}
爬虫很简单,就是根据book.js的小说条目,先在数据库的小说条目表写入所有小说数据。然后遍历条目,先爬到某小说的章节条目数据,再爬每个章节的内容数据写入到数据的章节表中,完成后继续爬下一本小说的数据。由于这些小说都是出版定稿了的,也不需要定时器来定时爬,和爬新闻等数据还是有区别。这个爬虫基本是一次性的,数据完整地爬完一次就没意义了。
章节表中的bookId和小说表中的id关联,表示该章节属于哪本小说。章节表中的index表示该章节是它所在小说的第几章节(序列)。
给APP端提供的小说章节目录接口,只需要小说id就行。章节内容接口,需要小说id和章节的序列index参数就能取到数据。
model:
'use strict'; /**
* book model
*/ export default class extends think.model.base {
/**
* 删除某个书籍
* @param id 要删除的书籍id
* @return {promise}
*/
removeBookById (id){
return this.where({id}).delete();
} /**
* 删除所有书籍
* @param null
* @return {promise}
*/
removeAllBooks (){
return this.where({id: ['>', 0]}).delete();
} /**
* 单个增加书籍
* @param book 书籍对象
* @return {promise}
*/
addBook (book){
return this.add(book);
} /**
* 批量增加书籍
* @param books 书籍对象数组
* @return {promise}
*/
async addBookMany (books){
await this.removeAllBooks();
return this.addMany(books);
}
}
'use strict'; /**
* chapter model
*/ export default class extends think.model.base {
/**
* 查询某个章节
* @param name 章节名称
* @return {promise}
*/
findChapter (name){
return this.where({name}).find()
} /**
* 单个增加章节
* @param chapter 章节对象
* @return {promise}
*/
addChapter (chapter){
return this.add(chapter);
} /**
* 批量增加章节
* @param chapters 章节对象数组
* @return {promise}
*/
addChapter (chapters){
return this.add(chapters);
}
}
model里面提供了操作数据库的方法,传入的数据对象的key需要和表中的cloumn一致,其他的就不用管了,很方便。
命令行cd进工程目录,执行 npm run spider ,就开始写入小说条目数据,然后爬章节数据了:
因为怕封IP,每个章节之间设置了500毫秒间隔时间,再加上网络延迟等原因,28本小说全部爬完再查漏一遍还是需要一些时间的。爬完后,总章节数有2157章,总字数就没统计了。
有了小说数据以后,就可以为自用小说阅读APP提供内容了。当然了,侵犯著作权的事情是不能干的哦。
一个用来爬小说的简单的Node.js爬虫的更多相关文章
- 一个超级简单的node.js爬虫(内附表情包)
之所以会想到要写爬虫,并不是出于什么高大上的理由,仅仅是为了下载个表情包而已-- 容我先推荐一下西乔出品的神秘的程序员表情包. 这套表情包着实是抵御产品.对付测试.嘲讽队友.恐吓前任的良品, 不过不知 ...
- Node.js爬虫-爬取慕课网课程信息
第一次学习Node.js爬虫,所以这时一个简单的爬虫,Node.js的好处就是可以并发的执行 这个爬虫主要就是获取慕课网的课程信息,并把获得的信息存储到一个文件中,其中要用到cheerio库,它可以让 ...
- Node JS爬虫:爬取瀑布流网页高清图
原文链接:Node JS爬虫:爬取瀑布流网页高清图 静态为主的网页往往用get方法就能获取页面所有内容.动态网页即异步请求数据的网页则需要用浏览器加载完成后再进行抓取.本文介绍了如何连续爬取瀑布流网页 ...
- 用简单的 Node.js 后台程序浅析 HTTP 请求与响应
用简单的 Node.js 后台程序浅析 HTTP 请求与响应 本文写于 2020 年 1 月 18 日 我们来看两种方式发送 HTTP 请求,一种呢,是命令行的 curl 命令:一种呢是直接在浏览器的 ...
- Node.js 爬虫爬取电影信息
Node.js 爬虫爬取电影信息 我的CSDN地址:https://blog.csdn.net/weixin_45580251/article/details/107669713 爬取的是1905电影 ...
- 搭建一个简单的node.js服务器
第一步:安装node.js.可以去官网:https://nodejs.org/en/进行下载. 查看是否成功,只需在控制台输入 node -v.出现版本号的话,就证明成功了. 第二步:编写node.j ...
- node.js爬虫爬取拉勾网职位信息
简介 用node.js写了一个简单的小爬虫,用来爬取拉勾网上的招聘信息,共爬取了北京.上海.广州.深圳.杭州.西安.成都7个城市的数据,分别以前端.PHP.java.c++.python.Androi ...
- Node.js爬虫实战 - 爬你喜欢的
前言 今天没有什么前言,就是想分享些关于爬虫的技术,任性.来吧,各位客官,里边请... 开篇第一问:爬虫是什么嘞? 首先咱们说哈,爬虫不是"虫子",姑凉们不要害怕. 爬虫 - 一种 ...
- 养只爬虫当宠物(Node.js爬虫爬取58同城租房信息)
先上一个源代码吧. https://github.com/answershuto/Rental 欢迎指导交流. 效果图 搭建Node.js环境及启动服务 安装node以及npm,用express模块启 ...
随机推荐
- OFFICE2007软件打开word时出现SETUP ERROR的解决方法
今天打开word时出现以下错误窗口: 在度娘上找了一下解决方案,原来每次打开word时都会启动一些无用的东西,找到这些东西的路径D:\Program Files\Common Files\micros ...
- public_handers.go
package],,) ],,) ]:],,);:],],,) ) ]],,) )) ,) )) if etagMatch { w.WriteHeader(ht ...
- [HNOI2015]菜肴制作 拓扑序
逆序最大字典序拓扑序 反向建边,逆序字典序最大.. #include<cstdio> #include<cstring> #include<iostream> #i ...
- BZOJ_1579_[Usaco2009 Feb]Revamping Trails 道路升级_分层图最短路
BZOJ_1579_[Usaco2009 Feb]Revamping Trails 道路升级_分层图最短路 Description 每天,农夫John需要经过一些道路去检查牛棚N里面的牛. 农场上有M ...
- [JSOI2008]星球大战starwar BZOJ1015
并查集 正序处理时间复杂度为n^2,考虑逆序处理,这样,时间复杂度从n^2降为nlogn 附上代码: #include <cstdio> #include <algorithm> ...
- ll 和 ls -l的详解
ll会列出该文件下的所有文件信息,包括隐藏文件 而ls -l 只会列出显示文件 ll 命令列出的信息更加详细,有时间,是否可读写等信息 ll命令和ls -l命令结果区别: 上面结果说明: 各个字段的含 ...
- Java开源生鲜电商平台-优惠券设计与架构(源码可下载)
Java开源生鲜电商平台-优惠券设计与架构(源码可下载) 说明:现在电商白热化的程度,无论是生鲜电商还是其他的电商等等,都会有促销的这个体系,目的就是增加订单量与知名度等等 那么对于Java开源生鲜电 ...
- Shell脚本的三种执行方式
Shell脚本的执行方式可以有以下几种: 方式一: ./script.sh # 利用小数点来执行 方式二: sh script.sh 或 bash script.sh # 利用bash(sh)来执 ...
- Go中原始套接字的深度实践
1. 介绍 2. 传输层socket 2.1 ICMP 2.2 TCP 2.3 传输层协议 3. 网络层socket 3.1 使用Go库 3.2 系统调用 3.3 网络层协议 4. 总结 4.1 参考 ...
- 搭建基于Docker社区版的Kubernetes本地集群
Kubernetes的本地集群搭建是一件颇费苦心的活,网上有各种参考资源,由于版本和容器的不断发展,搭建的方式也是各不相同,这里基于Docker CE的18.09.0版本,在Mac OS.Win10下 ...