写在开头
有时候在想,吾辈码农写代码是为了什么?工作?挣钱?是为了让生活过得好呢,还是为了让自己的腰过的不那么好?想来想去也都是各打五十大板的结果,一千个人有一千个哈姆雷特;也许不用刻意追求现实目的,在寻找有趣的过程中顺其自然也挺香。
大部分码农应该都不甘心只写写业务需求,即使是个新时代的农民,那咱也得做个有理想,有抱负,符合社会主义核心价值观的农民不是?故,在业务需求的闲暇之余,在看了掘金各路大神招式百变的花活之后,我的手告诉我,它也想挥舞几下,整点花活。所以,本着遇事不断,说干就干的原则,duang~,koa+superagent+cheerio的爬虫体验横空出世。
对这个项目的一点描述
其实在一开始,纯属对爬虫这种带点攻防色彩的技术感到好奇,一开始只想简单的用后端起个服务,写个简易的爬虫尝尝鲜,看看能不能把网页爬下来,爬下来之后,可能是DOM结构密密麻麻的字符让我看着太过杂乱,使得收获的成就感大为减少,我决定分析DOM,将需要的数据过滤出来。一番操作之后,成功过滤了数据,并通过node的fs模块,将数据写入txt文件。
本来爬虫初体验到这里应该结束了,但是呢,可能是多巴胺在这一天分泌过多,突然想写写接口,体验一下koa撸接口的感觉,于是写了个简单的前端页面,并成功从经过postman坎坷调试的接口中,获取到了想要的数据。
技术介绍
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这点就不在文章里面说了,如果有不懂的可以直接百度,资料很多
部分代码(对于代码的一些解释我会放在代码块的注释里面)
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)
}
})
})
}
最终效果
前端页面显示
txt文件
正文end