写爬小说的爬虫的一些心得

最近用 Node.js 写了爬某小说的爬虫,发现坑还是满多的。

网页中文乱码

小说网站的页面内容编码用的 GBK,如果不做处理,中文内容会是乱码。解决方案是用 iconv-lite 来对内容用 GBK 的方式来解码。大概的写法:

var iconv = require('iconv-lite')
request({
  url: BOOK_URL,
  encoding: null // 传 null,可以让 body 的类型是 Buffer。 用 iconv 进行 decode 传入的参数必须是 Buffer类型的。
}, (error, response, body) => {
  body = iconv.decode(body, 'GBK')
})

提取小说正文

发现小说的章节的 HTML 内容乱的超乎我的想象:

  1. 内容都在 <head> 标签内。浏览器竟然也能正常的输出。。。
  2. 正文外面也没有一个标签来包裹。正文的兄弟节点也没什么标志性的元素。

鉴于第 2 点,我用删除非正文内容来提取正文。下面是我的实现,以供参考

function toTxT(html) {
  html = html
    .replace(/<br ?\/?>/g, '\n')
    .replace(/ ?/g, ' ')
    .replace(/<!--.*-->/g, '') // 注释

  var $ = cheerio.load(html, { decodeEntities: false })
  var $body = $('html')
  $body.find('meta,div,style,link,script,table,h1,center,title').remove()
  var res = $body.html()
  res = res.replace(/<\/?head>/g, '')
    .replace(/^(\n|\r)+/mg, '')// 删除开头的空行
    .replace(/(\n|\r)+/mg, '\n\n')// 多个换行替换成1个
    .replace(/ {4,}/mg, '  ')// 4个以上的空格统一换成2个
  return res
}

同时发大量请求导致的服务器拒绝

那小说有一千多章。开始的做法是,对那小说网站同时发一千多个请求。每个请求请求 1 个章节的内容。尝试多次,发现每次都是只有不到 200 个请求是成功的,剩余的全部超时。

我发现可以通过减少同时发请求的数量来解决这个问题。我想了如下两个策略: 策略1 : 发 1 个请求,若请求成功,则发下 1 个请求。直到发完所有的请求。若中间某个请求返回失败,则重试若干次,若还是失败,则跳过。具体实现如下:

const RETRY_MAX = 3
var SingleQueue = function(queueArr, opts) {
  this.opts = Object.assign({
    retryMax: RETRY_MAX,
    callback: () => {}
  }, opts)
  this.queue = queueArr
  this.retryMax = this.opts.retryMax
  this.retryInfo = {}
  this.failArr = []
  this.execute()
}

SingleQueue.prototype = {
  execute: function(executeIndex) {
    executeIndex = executeIndex || 0
    if (executeIndex >= this.queue.length) {
      console.log(`完成。失败列表:${this.failArr.join()} `)
      this.opts.callback(this.failArr)
      return
    }
    if (this.retryInfo[executeIndex] && this.retryInfo[executeIndex] >= this.retryMax) {
      console.log(`执行${executeIndex+1}失败`)
      this.failArr.push(executeIndex)
      this.execute(executeIndex + 1) // 下一个
      return
    }

    this.queue[executeIndex]().then(function() {
      return this.execute(executeIndex + 1)
    }.bind(this), function() {
      this.retryInfo[executeIndex] = this.retryInfo[executeIndex] || 0
      this.retryInfo[executeIndex]++
        // 重试
        console.log(`第${this.retryInfo[executeIndex]}次 重试${executeIndex}`)
      return this.execute(executeIndex)
    }.bind(this))
  }
}

策略2 : 控制最多同时发若干个请求,没发的请求在等待队列种等待。发的请求成功,则执行等待队列中的第一个请求,若失败,则将该请求放入失败队列。最后,等待队列为空并且之前发的请求都完成时,去检查失败队列,若失败队列不为空,则对失败队列用策略 1 来处理。具体实现如下:

var MultiQueue = function(queueArr, opts) {
  this.opts = Object.assign({
    retryMax: RETRY_MAX,
    parallelNum: 20,
    callback: () => {}
  }, opts)
  this.undoneQueue = queueArr
  this.nowDoingNum = 0
  this.retryQueue = []
  this.executeDone = false
  this.execute()
}

MultiQueue.prototype = {
  execute: function() {
    if(this.executeDone) {
      return
    }
    if (this.undoneQueue.length === 0) {
      if (this.nowDoingNum === 0) {
        this.executeDone = true
        if (this.retryQueue.length > 0) {
          new SingleQueue(this.retryQueue, this.opts)
        } else {
          this.opts.callback()
        }
      } else {
        // 过段时间检查正在执行的有没结束
        setTimeout(function() {
          this.execute()
        }.bind(this), 2000)
      }
      return
    }
    if (this.nowDoingNum < this.opts.parallelNum) {
      var doThing = this.undoneQueue.shift()
      this.nowDoingNum++
      doThing().then(function() {
        this.nowDoingNum--
          this.execute()
      }.bind(this), function() {
        this.nowDoingNum--
          this.retryQueue.push(doThing)
      }.bind(this))
      this.execute()
    }
  }
}

通过实验,发现策略 1 和 2 都能把整本小说爬完,并且策略 2 比 1 的完成速度快很多。其中策略2 中我设置的同时发请求的数量是 20。

完整代码见这里


本文遵守创作共享CC BY-NC-SA 4.0协议 网络平台如需转载必须与本人联系确认。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

Go语言并发模型:以并行处理MD5为例

简介 Go语言的并发原语允许开发者以类似于 Unix Pipe 的方式构建数据流水线 (data pipelines),数据流水线能够高效地利用 I/O和多核 ...

50360
来自专栏小詹同学

Python 4 种不同的存取文件骚操作

前言:最近开始学习tensorflow框架,选修课让任选一种框架实现mnist手写数字的识别分类。小詹也就随着大流选择了 tf 框架,跟着教程边学边做,小詹用了...

19430
来自专栏项勇

笔记12 | 复习Volley(一)基本概念和用法

20240
来自专栏Python

python select模块详解

要理解select.select模块其实主要就是要理解它的参数, 以及其三个返回值。 select()方法接收并监控3个通信列表, 第一个是所有的输入的data...

54560
来自专栏一个爱瞎折腾的程序猿

常用cmd代码片段及.net core打包脚本分享

保存:set currentPath=%cd% 输出:echo %currentPath

14530
来自专栏青玉伏案

iOS开发之App主题切换完整解决方案(Swift版)

本篇博客就来介绍一下iOS App中主题切换的常规做法,当然本篇博客中只是提到了一种主题切换的方法,当然还有其他方法,在此就不做过多赘述了。本篇博客中所涉及的D...

309100
来自专栏编程

史上最全Django知识总结!神级程序员强推:掌握此文就掌握Django

一、视图函数(views.py中的函数):第一个参数类型是HttpRequest对象,返回值是HttpResponse对象 二、URLconf(urls.py)...

72270
来自专栏搞前端的李蚊子

微信返回码说明

返回码说明 返回码    说明 -1     系统繁忙 0     请求成功 40001     验证失败 40002     不合法的凭证类型 40003  ...

42760
来自专栏Python

vim显示行号、语法高亮、自动缩进的设置

28120
来自专栏技术博客

C#简单的面试题目(六)

76.HashMap和Hashtable的区别。 答:HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区别在于H...

10320

扫码关注云+社区

领取腾讯云代金券