petty-spider设计思路和使用说明
|在18年的时候断断续续开始写petty-spider这个爬虫工具,是时候整理一下开发思路和相关实现了~(再不整理就快忘记了...
<!--more-->
本文主要说明当初设计这个框架的一些思路。
1. 将网络请求交给用户
爬虫框架最先需要考虑的是获取资源的内容,即如何发送网络请求并处理响应。实际上的抓取请求有各种情况,除了最基础的get请求url,还包括
- get、post请求混用,通过js触发表单post提交页面
- 各种请求返回各不相同,比如页面编码,在框架代码中封装不太合适
- 有时候希望在请求前后插入一些特定的逻辑,以及用户鉴权等信息,如果通过配置项添加,框架使用成本较高
所以为了减少使用成本,将整个请求的逻辑交给用户,框架本身只约定了一个request
接口,该方法返回对应资源的内容
interface request {
({url}: { url: string }): Promise<string>;
}
这样就需要由用户自己实现网一些特定逻辑,如
- 对需要登录访问限制的资源实现鉴权,如手动设置请求header头
- 对响应接口进行初步处理,如返回非
utf-8
的内容进行解码等
function request({url}) {
return http.get(url, {
headers: {
'User-Agent': `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36`,
'Authorization': 'Bearer xxdsfdsafdas' // 需要用户自己实现获取登录token相关逻辑
},
}).then(res => res.data)
}
2. 将保存逻辑交给用户
数据提取完毕之后,需要考虑的第二个问题就是如何保存数据。常见的方式有
- 将数据保存到本地文件,如txt、json文件中
- 将数据保存到数据库,如mongo、mysql中
- 将数据直接提交到某些后台服务中,一般用于已经存在的、需要处理特殊逻辑的接口
目前框架封装上述三种保存数据的方法,通过在初始化应用时通过配置参数saveConfig
传入
文件类型
{
type: 'file',
config: {
dist: path.resolve(__dirname, `./tmp/${count}.json`),
format(data) {
return JSON.stringify(data)
},
},
}
mongodb类型
{
type: 'mongo',
config: {
host: 'mongodb://localhost/data_source',
document: 'news',
schema: {
"name": String,
// ... 其他字段
},
}
}
接口上传类型
{
type: 'upload',
config: {
// 将上传数据的方法通过配置参数传入
request: function(data){
return axios.post(`http://127.0.0.1:7001/upload/news`, { data })
}
},
}
在完成数据的抓取和解析之后,框架将根据saveConfig
将数据保存到指定位置。
3. 多种开发模式
在进行爬虫开发时,往往需要经历各种调试和测试。一个比较常见的场景是,
- 在正式服务中,我们将直接请求资源url,解析页面并将数据保存到数据库中
- 为了避免在开发期间对源网站频繁访问,在开发期间我们会把需要抓取的html文件下载到本地,然后通过读取本地文件的方式完成后续解析的流程,并将数据保存到txt中进行调试
由于框架暴露的request接口并不要求真实访问网络资源,所以上面的request方法可以修改为
// 这也是将request接口暴露给用户的好处之一,可以单独进行mock或测试,而无需修改框架的配置
function request(url) {
let html = fs.readFileSync(path.resolve(__dirname, './tmp/1.html'), 'utf-8')
return Promise.resolve(html)
}
同时saveConfig
也可以根据环境变量动态控制是开发环境还是线上环境,从而传入不同的配置。
总体来说对我而言,在各种环境切换是一个比较常规的需求,因此框架引入了mode
概念,目前每个mode包含了request
和saveConfig
两个配置项
let config = {
test: {
request({url}) {
let html = fs.readFileSync(path.resolve(__dirname, './tmp/test.html'), 'utf-8')
return Promise.resolve(html)
},
saveConfig: {
type: 'file',
config: {
dist: path.resolve(__dirname, `./tmp/test.json`),
format(data) {
return JSON.stringify(data)
},
},
},
},
prod: {
request({url}) {
return http.get(url).then(res => res.data)
},
saveConfig:{
type: 'mongo',
config: {
host: 'mongodb://localhost/data_source',
document: 'news',
schema: {
"name": String,
},
}
}
}
}
可以通过app.addConfig
传入各种模式的配置
4. 解析策略
一般来说我们需要处理两种响应:JSON和HTML
- 对于JSON响应的处理非常简单,我们只需要保存部分或全部字段即可
- 对于HTML响应而言,我们就需要解析相关标签,从页面上提取各种需要的数据
根据http响应,解析html文档,获取对应的dom节点数据,对于框架而言这一块应该是非常灵活的,框架内部使用cheerio
解析HTML文档,同时也支持直接抓取JSON接口
设计的解析策略大致数据结构为
let strategy1 = {
rtype: /www.a.com\/1.html/,
// strategy配置项是一个数组,支持从页面上多个不同的区域提取数据,一种常见的场景是:从ul列表提取数据,从分页器提取下一页的链接加入爬取池
strategy: [{
selector: '#list .td a', // 页面上包含所需数据的选择器,与jQuery保持一致
parse($dom) {
return $dom.text().trim()
}
}]
}
let strategy2 = {
rtype: /www.a.com\/2.json/,
strategy: [{
json: true,
selector: '#pl_top_realtimehot .td-02 a',
parse(data) {
// 或者对data数据进一步过滤
return data
}
}]
}
通过rtype
确定某个或者某类页面的解析策略,页面的strategy
配置项是一个数组,包含选择器和对应选择器匹配节点的解析模式,同一个页面可以配置多个策略,最后会将收集到的数据汇总到一起,进行保存。
此外应用可以通过addStrategy
添加多个解析策略,然后根据当前请求的url与所有策略的rtype
进行匹配,找到该url对应的strategy
,然后执行数据解析或过滤等逻辑
5. IP代理池
爬虫的访问频率一般比较高,除了控制每次请求的间隔之外,还可以使用ip代理进行访问,抹除原始访问记录
参考
- https://cloud.tencent.com/developer/news/343582
- https://zhuanlan.zhihu.com/p/31431224
- 大数据时代,用爬虫拿到数据违法吗?数据可以商业化吗?
目前这里并没有单独实现,利用提供的request
配置项,接入代理也比较方便
6. 一个简单的例子
下面是一个抓取微博热搜的例子
let path = require('path')
let {http, default: App} = require('../lib')
function initApp() {
let app = new App()
let strategy = {
rtype: /weibo.com/,
strategy: [{
selector: '#pl_top_realtimehot .td-02 a',
parse($dom) {
return $dom.text().trim()
}
}]
}
let config = {
test: {
request({url}) {
return http.get(url, {
headers: {
'User-Agent': `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36`
},
}).then(res => {
return res.data
})
},
saveConfig: {
type: 'file',
config: {
dist: path.resolve(__dirname, `./tmp/weibo_hot.json`),
format(data) {
return JSON.stringify(data)
},
},
},
}
}
let startTask = app.createTask(`https://s.weibo.com/top/summary?Refer=top_hot&topnav=1&wvr=6`)
app.addStrategy(strategy)
app.addConfig(config)
app.addTask(startTask)
return app
}
let app = initApp()
// 运行test类型的mode
app.start('test').then(() => {
console.log('抓取完毕')
})
在实际的开发阶段,只需要传入
strategy
解析策略,如何从页面上提取数据config.test
配置至少一种mode类型,- 实现
request
接口,主要配置header及一些前置后置处理逻辑 - 配置
saveConfig
,指定保存方式
- 实现
createTask
创建任务
这样就可以满足大部分需求场景了。对于需要连续抓取的分页数据,可以提前addTask
添加多个任务,也可以在抓取过程中动态添加任务。
7. 小结
本文主要记录了petty-spider
的一些设计思路,由于三天打鱼两天晒网,加之我对爬虫的需求也不是特别大,偶尔抓抓表情包和段子啥的,因此一直没有正儿八经的写一个使用文档,本文就算做是一个使用说明吧。
整个项目都放在github上面了,现在回头看代码,有一些写的很烂的地方,甚至忘记了自己当时为何要如此实现,总之如果后续有需求,我再进行重构吧。