本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力
前言
这篇文章是我的小说爬虫项目简化版实现
本文不会讲 node.js 的基础知识,如果你对 node.js 一无所知的话,建议先去了解一下基础,推荐一下我的node.js 入门文章
接下来进入正题,今天我们要来开发一个小说爬虫,首先我们得选择一个适合我们爬取的网站笔趣阁,我们发现我们可以通过接口直接拿到dom
信息,即这是一个“静态页面”
当然网上的笔趣阁有很多,但理论上经过我们的配置,我们可以达到写一个爬虫就可以爬尽各种“笔趣阁”。
话不多说,正式开始
项目初始化
选一个你喜欢的目录,新建book-spider
文件夹,进入之后,打开终端
yarn init -y
初始化我们的package.json
,我这里用的是yarn
,当然你用npm
或者其他的都可以,完全没有影响,只需要替换成对应的命令就可以啦
然后我们来分析一下写一个爬虫需要哪些依赖?
首先,我们需要发送网络请求,我在这选择axios
拿到返回值后,我们还需要一个分析dom
结构的工具,cheerio
就可以帮助我们完成这一任务
接下来,我们来安装依赖
yarn add axios cheerio
这样项目就初始化好啦!
获取搜索信息
接下来我们来分析一下怎么拿到搜索信息,先在网页上随便搜索一本小说,我们以民国谍影
这本书为例
搜索之后,跳转到了搜索结果页面,我们先来分析一下url
,
https://www.biqugeu.net/searchbook.php?keyword=民国谍影
我们可以发现url
的规则是/searchbook.php?keyword={小说名}
,这样我们获取搜索页的 url 就很简单了
然后来分析一下页面结构,我们需要一个可以跳转小说信息的链接
一目了然对不对,我们只需要先拿到a标签
,然后拿到它的href
属性就可以了
相信熟悉 css 的同学很快就能写出获取我们需要的a标签
的 css 选择器了
#hotcontent .item .image a
分析完之后,我们就可以开始写代码啦
我们在src
目录下建一个index.js
文件,
引入依赖
const axios = require('axios')
const cheerio = require('cheerio')
获取搜索 url,encodeURI
是防止 url 没有转义请求报错
const BASE_URL = 'https://www.biqugeu.net'
const getSearchUrl = bookName =>
encodeURI(`${BASE_URL}/searchbook.php?keyword=${bookName}`)
获取 href
async function getBookUrl(bookName) {
const { data } = await axios.get(getSearchUrl(bookName))
const $ = cheerio.load(data)
return BASE_URL + $('#hotcontent .item .image a').attr('href')
}
获取小说信息
接下来,就该获取小说信息了
一样的步骤,我们先来分析一下页面结构
我们需要小说名
、作者
、简介
,分别写出它们的 css 选择器
-
小说名:
#info h1
-
作者:
#info h1+p
-
简介:
#intro
然后是章节链接的 css 选择器
.box_con dl dt:nth-child(2n)~dd a
接下来开始写代码
获取小说名
、作者
、简介
和所有章节链接
async function getBookInfo(bookUrl) {
const { data } = await axios.get(bookUrl)
const $ = cheerio.load(data)
const bookName = $('#info h1').text()
const author = $('#info h1+p').text().split(':')[1]
const des = $('#intro').text().trim()
const contentUrls = []
$('.box_con dl dt:nth-child(2n)~dd a').each((_, ele) => {
const url = BASE_URL + $(ele).attr('href')
contentUrls.push(url)
})
return {
bookName,
author,
des,
contentUrls,
}
}
获取章节内容
拿到章节链接
后,我们就可以获取章节内容了,一样的流程,写出章节内容的 css 选择器
-
标题:
.bookname h1
-
内容:
#content
代码如下
async function getContents(contentUrls) {
const requests = []
contentUrls.forEach(url => requests.push(axios.get(url).catch(e => e)))
const results = await Promise.all(requests)
const contents = []
results.forEach(e => {
const { data } = e
if (data) {
const $ = cheerio.load(data)
const title = $('.bookname h1').text()
const content = $('#content')
.text()
.replace(/ | /g, '\n\n') // 空格换为空行
contents.push({
title,
content,
})
}
})
return contents
}
这里用到了Promise.all
进行并发请求,优化效率
写入文件
最后一步当然就是把我们爬取到的信息保存下来啦,你也可以使用数据库等地方,我这里只保存到本地
const path = require('path')
const fs = require('fs')
const DOWNLOAD_PATH = path.resolve(__dirname, '../download')
async function writeFile(contents, bookName, author, des) {
fs.mkdir(DOWNLOAD_PATH, { recursive: true }, err => {
if (err) console.log(err)
})
contents.forEach((item, index) => {
if (index === 0) {
console.log(`正在写入:${bookName}...`)
const info = `『${bookName}』\n『作者:${author}』\n『简介:${des}』\n`
fs.appendFileSync(`${DOWNLOAD_PATH}/${bookName}.txt`, info)
}
if (index === contents.length - 1) {
console.log(`${bookName} 写入完成!`)
}
let content = `\n${item.title}\n${item.content}\n\n`
fs.appendFileSync(`${DOWNLOAD_PATH}/${bookName}.txt`, content)
})
}
这样我们就完成了全部流程了
完整代码
const axios = require('axios')
const cheerio = require('cheerio')
const fs = require('fs')
const path = require('path')
const process = require('process')
const BASE_URL = 'https://www.biqugeu.net'
const DOWNLOAD_PATH = path.resolve(__dirname, '../download')
const getSearchUrl = bookName =>
encodeURI(`${BASE_URL}/searchbook.php?keyword=${bookName}`)
async function getBookUrl(bookName) {
const { data } = await axios.get(getSearchUrl(bookName))
const $ = cheerio.load(data)
return BASE_URL + $('#hotcontent .item .image a').attr('href')
}
async function getBookInfo(bookUrl) {
const { data } = await axios.get(bookUrl)
const $ = cheerio.load(data)
const bookName = $('#info h1').text()
const author = $('#info h1+p').text().split(':')[1]
const des = $('#intro').text().trim()
const contentUrls = []
$('.box_con dl dt:nth-child(2n)~dd a').each((_, ele) => {
const url = BASE_URL + $(ele).attr('href')
contentUrls.push(url)
})
return {
bookName,
author,
des,
contentUrls,
}
}
async function getContents(contentUrls) {
const requests = []
contentUrls.forEach(url => requests.push(axios.get(url).catch(e => e)))
const results = await Promise.all(requests)
const contents = []
results.forEach(e => {
const { data } = e
if (data) {
const $ = cheerio.load(data)
const title = $('.bookname h1').text()
const content = $('#content')
.text()
.replace(/ | /g, '\n\n') // 空格换为空行
contents.push({
title,
content,
})
}
})
return contents
}
async function writeFile(contents, bookName, author, des) {
fs.mkdir(DOWNLOAD_PATH, { recursive: true }, err => {
if (err) console.log(err)
})
contents.forEach((item, index) => {
if (index === 0) {
console.log(`正在写入:${bookName}...`)
const info = `『${bookName}』\n『作者:${author}』\n『简介:${des}』\n`
fs.appendFileSync(`${DOWNLOAD_PATH}/${bookName}.txt`, info)
}
if (index === contents.length - 1) {
console.log(`${bookName} 写入完成!`)
}
let content = `\n${item.title}\n${item.content}\n\n`
fs.appendFileSync(`${DOWNLOAD_PATH}/${bookName}.txt`, content)
})
}
async function main() {
try {
const searchKey = process.argv.slice(2)[0]
const bookUrl = await getBookUrl(searchKey)
const { bookName, author, des, contentUrls } = await getBookInfo(bookUrl)
const contents = await getContents(contentUrls)
writeFile(contents, bookName, author, des)
} catch (e) {}
}
main()
检验一下效果
🆗,完成!
一些问题
我们完成的这个爬虫其实还是有许多问题
- 信息提示并不友好
- 没有实现控制并发数和错误重试,发生错误的概率高且出错之后没有补救措施
- 不能自定义,只是用于一个“笔趣阁”
如果你想写一个比较完善的爬虫的话,欢迎学习我的小说爬虫项目,也欢迎大家的 pr
希望这篇文章对你有帮助,拜拜!