CPQ

程沛权

网站改版迁移经验记录:基于node的爬虫编写

程沛权2019/7/22 17:28:00

本文最后更新于 5 年 4 个月前,部分内容可能不适合当前所有情况,仅供参考。

以往的网站迁移服务器,数据方面的东西都是由服务端直接处理,不过这一次的需求比较特殊,先来看一下需求的几个特征点:

1、改版后的数据源都是来自另外一个平台,新服务器通过接口实现前后端分离,新的数据都来自某款 app,后台也是使用该 app 对应的后台

2、由于第一点,所以旧的 CMS 后台将被下线停用

3、这是一个 10 几年的老网站,数据量庞大,但是大部分旧数据要做舍弃处理(因为大部分内容是资讯,已经过了阅读时效性)

4、有一部分有价值的旧数据需要单独保存,但由于是基于 CMS 更新发布,从服务端导出的都是模板类的代码,无法直接使用

其实简单理解,就比如门户首页本来抓的是门户的资讯,但改版后是抓微博的动态,原来的资讯基本舍弃,只保留一部分专题和文章页,类似酱紫的一个改版迁移 emmm,所以经过商讨,需要由前端来将需要保存的旧页面从客户端保存,再发布上传到新服务器上。

注意事项

这次的需求并不是简单的爬取就完事,拿了需求需要先进行一波分析,拆分了两类注意事项如下:

缺点分析

1、旧页面的编码需要调整(由“gb2312”调整为“utf-8”),否则在新服务器会乱码。

2、旧页面的部分公共模块需要移除(页头通栏、页脚、评论模块、登陆模块等),保留的旧页面将变成纯静态页面。

3、旧页面的统计代码需要更换。

4、旧页面的保存路径必须和原来一模一样(比如文章页一定要遵循原来的 “根目录/年/月/日/hash 文件名.html” 的格式保存),这样才不会造成死链接。

5、需要保存的资源散落在各个专题的引用链接里,需要自己从专题里提取。

优点分析

1、由于是 CMS 输出的文章和专题,所以所有的同类型页面,结构、布局、资源引入完全一致,只需要写一套爬虫代码即可适配抓取(虽然有 3 套文章模板,但结构其实是一样的)。

2、几乎所有的 js 和 css 都是 CDN 资源,也就是无需单独保存这些资源,同步好 html 代码就完事,个别被定制了样式的专题再单独处理一下即可。

3、虽然文章要自己提取链接,但是专题的链接由运营整理提供,可以节省一部分工作量

最后自己这边定下来的方案是用 node 来做爬虫服务器,实现自动化抓取和保存,以下是本次涉及到的工具和模块。

工具/模块用途api 文档/github
node.js爬虫服务器http://nodejs.cn/api/
http 模块node 内置的模块,用于获取页面信息,也可以用封装好的 express 框架代替http://nodejs.cn/api/http.html
fs 模块node 内置的模块,用于文件的创建和保存http://nodejs.cn/api/http.html
mkdirpnode 内置的模块,用于创建多级文件夹(因为 fs 模块的 mkdir 只能一级一级创建,很繁琐)https://github.com/substack/node-mkdirp
iconvnode 本身只支持 utf-8,对于非 utf-8 的网站,要用这个模块进行转码,才不会乱码https://github.com/ashtuchkin/iconv-lite/
cheerio可选,运行在 node 端的 dom 选择器,api 语法和 jQuery 一样,必要时可用来提取需要的东西https://github.com/cheeriojs/cheerio/

编写爬虫

这里分两步走:

先提取需要爬取的链接

这一步是汇总成一个数组,这一步就略过了,提取 a 标签的 href,对大家应该都没什么难度。

开始爬取页面

流程:创建项目文件夹,创建一个 getHtml.js 作为主程序文件,打开 node,cd 到这个目录,运行 node getHtml 即可开始爬取。

主程序 getHtml.js 的核心代码如下(模糊掉业务相关信息,仅供参考):

// 引入需要的模块
const http = require('http')
const fs = require('fs')
const iconv = require('iconv-lite')
const mkdirp = require('mkdirp')

// 设置要抓取的网页地址,这里是举例
const urlList = [
  'http://chengpeiquan.com/14/0120/10/9KASP7MUSK324C3U.html',
  'http://chengpeiquan.com/15/0214/12/3NPRSLDUQWMZ14C3U.html',
  'http://chengpeiquan.com/16/0323/11/3S64W57MU00314C3U.html',
]

// 执行抓取
urlList.forEach(function (url) {
  // 获取文章的目录结构,提取文件夹层级与文件名
  const folder = url.replace(/http:\/\/chengpeiquan\.com\//, '').split('/')
  const filePath = `${folder[0]}/${folder[1]}/${folder[2]}`
  const fileName = folder[3]
  console.log(`文件目录:${filePath}\t文件名称:${fileName}`)

  // 在当前目录下,根据文章路径创建多级文件夹
  mkdirp(filePath, function (err) {
    err ? console.log(err) : console.log(`文件夹${filePath}创建成功!`)
  })

  // 获取文章内容并保存文件
  http
    .get(url, function (res) {
      let html = ''
      res.on('data', function (chunkBuffer) {
        // 原网页是gb2312的编码,需要进行解码,避免乱码
        html += iconv.decode(Buffer.from(chunkBuffer), 'gb2312')

        /* ******************** 下面是开始优化文章内容 ******************** */

        // 新的服务器只支持uft-8,所以要替换html文档内的编码格式
        html = html.replace(/gb2312/gi, 'utf-8')

        // 把整个文档处理为一行,才能随心所欲的替换
        html = html.replace(/\n/g, '【br】')

        /*
				接下来就是各种replace了
				- 移除原来的全局头部
				- 移除原来的跟贴模块
				- 移除原来的相关新闻模块
				- 移除原来侧边栏的头条模块
				- 移除原来侧边栏的要闻模块
				- 换成新的统计代码
				- 过滤掉原有写死的文章域名变成相对地址
			*/

        // 处理好内容,要把换行还原回来
        html = html.replace(/【br】/g, '\n')
      })
      res.on('end', function () {
        // 将抓取的内容保存到本地文件中
        fs.writeFile(`${filePath}/${fileName}`, html, function (err) {
          if (err) {
            console.log(`保存${filePath}/${fileName}时出现错误!`)
          } else {
            console.log(`文件${filePath}/${fileName}保存成功!`)
          }
        })
      })
    })
    .on('error', function (err) {
      console.log('错误信息:' + err)
    })
})

成果预览

一切顺利,最终爬了有将近 150 篇文章,数据量说大不大,但如果人工处理肯定是不小(至少创建层级文件夹就会累死人)。

刚搞完这次需求,需求方又找过来说还会有个网站想迁移,emmm,坐等新一波挑战了~