「 有趣的那些小玩意 」nodeJS实现爬虫,koa+superagent+cheerio组合拳,爬爬更健康

1,363 阅读6分钟

写在开头

有时候在想,吾辈码农写代码是为了什么?工作?挣钱?是为了让生活过得好呢,还是为了让自己的腰过的不那么好?想来想去也都是各打五十大板的结果,一千个人有一千个哈姆雷特;也许不用刻意追求现实目的,在寻找有趣的过程中顺其自然也挺香。
大部分码农应该都不甘心只写写业务需求,即使是个新时代的农民,那咱也得做个有理想,有抱负,符合社会主义核心价值观的农民不是?故,在业务需求的闲暇之余,在看了掘金各路大神招式百变的花活之后,我的手告诉我,它也想挥舞几下,整点花活。所以,本着遇事不断,说干就干的原则,duang~,koa+superagent+cheerio的爬虫体验横空出世。

对这个项目的一点描述

其实在一开始,纯属对爬虫这种带点攻防色彩的技术感到好奇,一开始只想简单的用后端起个服务,写个简易的爬虫尝尝鲜,看看能不能把网页爬下来,爬下来之后,可能是DOM结构密密麻麻的字符让我看着太过杂乱,使得收获的成就感大为减少,我决定分析DOM,将需要的数据过滤出来。一番操作之后,成功过滤了数据,并通过node的fs模块,将数据写入txt文件。
本来爬虫初体验到这里应该结束了,但是呢,可能是多巴胺在这一天分泌过多,突然想写写接口,体验一下koa撸接口的感觉,于是写了个简单的前端页面,并成功从经过postman坎坷调试的接口中,获取到了想要的数据。

image.png

技术介绍

koa这个框架相信只要接触过nodeJS的朋友都比我更加了解,就不介绍了(我是不会告诉你我只用过express,所以才想用Koa尝尝鲜的),这里主要说说用到的另外两个东西

  • superagent
    这个东西呢,是一个内部依赖nodeJS、相对轻量的AjaxAPI,在这里我们主要用来发送请求,抓取网页。具体用法看文档,这里就不做过多赘述了superagent中文文档(译)
  • cheerio
    我之前在想,用superagent将网页元素抓取下来后,在服务端有什么简便的方法来分析DOM,这不,从掘金各路大佬的花活中,我找到了cheerio,在cheerio的gitee上对它的解释是这样的:

Cheerio 实现了核心 jQuery 的一个子集。Cheerio 从 jQuery 库中删除了所有 DOM 不一致和浏览器残留,展示了其真正华丽的 API,Cheerio 使用非常简单、一致的 DOM 模型。因此,解析、操作和渲染非常高效。

说的再明白点,cheerio是jquery核心功能的简洁实现,主要是用在服务器端需要对DOM进行操作或者分析的地方

这不,瞌睡就来了枕头,爬虫的基础技术支持已经满足了,接下来就是动手了。

正文

项目目录

如何在项目中使用Koa这点就不在文章里面说了,如果有不懂的可以直接百度,资料很多 image.png


部分代码(对于代码的一些解释我会放在代码块的注释里面)

package.json,这里使用了nodemon,不然每次修改完都得停止服务重新启动

{
  "name": "pachong",
  "version": "1.0.0",
  "description": "无描述",
  "main": "./app.js",
  "scripts": {
    "start": "nodemon app.js"
  },
  "author": "yln",
  "license": "ISC",
  "dependencies": {
    "cheerio": "^1.0.0-rc.10",
    "koa": "^2.13.4",
    "koa-cors": "0.0.16",
    "koa-router": "^10.1.1",
    "superagent": "^6.1.0"
  },
  "devDependencies": {}
}

app.js

const Koa = require('koa');
const Router = require('koa-router')
const Cors = require('koa-cors')
const SuperAgentRequest = require('superagent');
const Index = require('./index')
const app = new Koa();
const route = new Router();
//要爬哪个网站的数据,启动服务前,在options添加数据后,去更改name即可,这种写法方便后续扩展
const name = 'db';
const options = {
  'db': {
    'source': 'db',//自定义,与上面的name相对应即可
    'url': 'https://xxxxxxxxxxxxxxxx'//为了防止不必要的争端,此处隐去详细url,根据自己的需要来填写即可
  }
}

//使用koa-cors解决跨域问题
//前端是使用live-server起的服务,端口号是http://127.0.0.1:5500,可以根据自己的live-server配置来填写
app.use(Cors({
  origin: function() {
    return 'http://127.0.0.1:5500'
  },
  maxAge: 5, //指定本次预检请求的有效期,单位为秒。
  credentials: true, //是否允许发送Cookie
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], //设置所允许的HTTP请求方法
  allowHeaders: ['Content-Type', 'Authorization', 'Accept'], //设置服务器支持的所有头信息字段
  exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'] //设置获取其他自定义字段
}));
let webData = {}

// 这里单独封装一个promise的方法
function getData() {
  return new Promise((resolve, reject) => {
    SuperAgentRequest.get(options[name].url).end(async (err, res) => {
      let sourceData = {}
      if (res.ok) {
      //这里用switch主要是为了后续扩展,毕竟不可能只爬这一个网站不是
      //当然,也可以以文件的形式作区分,方法很多
        switch (options[name].source) {
          case 'db':
            sourceData = await Index.dbHandle(res.text);
            break;

          default:
            break;
        }
        webData = {
          status: 200,
          data: sourceData
        };
        resolve()
      } else {
        console.error('失败', err);
        webData = {
          status: 404,
          data: {}
        };
        reject();
      }

    }, err => {
      webData = {
        status: 404,
        data: {}
      };
      console.error('数据获取失败了');
      reject()
    })
  })

}
//创建接口
route.get('/hello', async (ctx) => {
  await getData();
  ctx.body = webData;
  // 也可用promise函数来显式返回
  // return getData().then(res => {
  //   ctx.body = webData
  // })
})

app.use(route.routes()).use(route.allowedMethods())// 注册接口
app.listen(3000)

index.js我选择在这里调用方法进行数据处理,并且做txt文件的写入操作

const FS = require('fs');
const Handle = require('./handle');
// 项目文件夹使用中文名称是不符合规范的,因为只是写个示例,就没有去纠结了,小伙伴谨记
const dbWriteUrl = '/Users/apple/Desktop/node-爬虫/textStore.txt';
async function dbHandle(params) {
  const data = await Handle.dbNewBookHandle(params);//数据过滤放在handle.js中处理
  FS.writeFileSync(dbWriteUrl, data);//文件写入
  return data
}
const Index = {
  dbHandle
}
module.exports = Index

handle.js
这里花了一点时间,主要问题是出现在换行符上,从下面可以看到,我是用字符串累加的方式来输出最终的字符串,其实一开始我是想用JSON.stringify将数组中的对象转为字符串输出,但是我发现用JSON.stringify后,\n换行失效了,一开始并没有意识到是JSON.stringify转换的问题,走了不少弯路。
由于爬下来的数据有很多含有空格、\n等的字符,这不是我所需要的,所以需要处理一下。

这里的数据过滤是需要分析原网页的DOM,再一层一层的获取到我们想要的数据。

const cheerio = require('cheerio');
function dbNewBookHandle(data) {
//cheerio使用之前,需要先cheerio.load,类似初始化,下面的$操作就跟jquery没什么两样,
  $ = cheerio.load(data);
  const newBookData = $('.slide-list ul li');
  let disposeBook = '';
  Array.prototype.forEach.call(newBookData, function(element) {
    const node = $(element);
    disposeBook += `{\n
    书名: ${$(node.find('.title a')).html().replace(/(\n)/g, "").trim()}
    作者: ${$(node.find('.author')).html().replace(/(\n)/g, "").trim()}
    出版时间: ${$(node.find('.more-meta .year')).html().replace(/(\n)/g, "").trim()}
    出版社: ${$(node.find('.more-meta .publisher')).html().replace(/(\n)/g, "").trim()}
    书籍简介: ${$(node.find('.more-meta .abstract')).html().replace(/(\n)/g, "").trim()}
    书籍封面图: ${$(node.find('.cover img')).attr("src").replace(/(\n)/g, "").trim()}
}\n`

  })
  return disposeBook

}
const Handle = {
  dbNewBookHandle,
}
module.exports = Handle

view.js ajax请求部分代码

   getCrawlData() {
      return new Promise((resolve, reject) => {
        $.ajax({
          type: 'GET',
          url: 'http://localhost:3000/hello',
          data: {},
          success: function(result) {
            resolve(result)
          },
          error: function(err) {
            reject(err)
          }
        })
      })
   }

最终效果

前端页面显示 image.png

txt文件 image.png 正文end