从小说爬虫入门 Node.js 吧 基于 Node.js 开发一个完整的项目

2,011 阅读4分钟

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力

前言

这篇文章是我的小说爬虫项目简化版实现

本文不会讲 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()

检验一下效果

🆗,完成!

一些问题

我们完成的这个爬虫其实还是有许多问题

  1. 信息提示并不友好
  2. 没有实现控制并发数和错误重试,发生错误的概率高且出错之后没有补救措施
  3. 不能自定义,只是用于一个“笔趣阁”

如果你想写一个比较完善的爬虫的话,欢迎学习我的小说爬虫项目,也欢迎大家的 pr

希望这篇文章对你有帮助,拜拜!