---
title: 爬虫监控服务的数据库选择-bitmap应用
date: 2020/10/29 17:12:00
updated: 2020/10/29 17:12:00
status: publish
author: harumonia
categories:
  - 源流清泉
  - DataBase
tags:
  - Redis
noThumbInfoStyle: default
outdatedNotice: no
thumbChoice: default
thumbStyle: default
hidden: false
email: zxjlm233@gmail.com
---

在爬虫实习中遇到了这样一个情境：对一个给定的队列进行数据爬取(比如说是一个公司名称的队列，爬取对应的公司信息)，当然不是开发结束就算是完成了，代码无法保证能够应对所有的突发情况，而且我们也需要一个半透明的，甚至是透明的爬虫监控系统，了解爬虫的任务进度，以及过程中遇到的一些问题。

这个监控系统并不难实现，使用 flask 写了一个服务平台，不过是几个小时的事情，但是我在实现过程中遇到一个很有趣的问题：用什么样的数据库来保存这些数据？

笔者写爬虫时常用的数据有 **MySQL**、**mongoDB**、**Redis**，这三个各有长处，用来针对不同的业务需求；而在写 web 服务时，则常用 **MySQL**、**SQLite**。

这些数据库在不同的情境下各有优劣，正巧最近有些闲工夫，就在这个问题上做了一些发散。

<!-- more -->

## 需求分析

一切问题从需求出发。于是，我将服务的需求和可能出现的问题都列下来，从这些之中去发现解决思路。

1. 哪些公司完成了？哪些公司未完成？
2. 第一轮数据获取结束后，还会有第二轮、第三轮等数据更新。
3. 对爬虫进行时段监控，了解各个时段的爬虫效率如何？（这个时间精度不用很高，一般分析都是每 10 分钟、每半小时这样进行分析）
4. 对特殊字段进行检测（有些字段在网站上只有键，没有值。对于偏僻的键，寻找数据样例花费的时间代价太大，一般选择先让爬虫跑起来，检测到值再进行更新）

## 问题分析

到这里，基本上 mongoDB 同学就可以退场了。MySQL 可以完成我们需要的问题的，建立一张数据表，包含 id、公司名、完成时间、轮次这几个字段，基本上前 3 点需求就可以完成了。

a. **冗余数据**。多轮数据会导致数据的存量变得很大。仅仅第一轮启动就会添加 40w+的数据，第二轮、第三轮，每一轮的更新必然会有新的公司名称加入进来，到时候数据的膨胀速度会变得很快。
b. **纯粹无用数据**。爬虫的时段监控从实际的角度来说，仅在本轮有较高的使用价值。对于第二轮的爬取来说，第一轮的时段监控基本上就是冗余数据了。
c. **第 4 点需求的处理思路**。如果我们将特殊字段作为数据表的字段添加进来，那么就会构成一个十分庞大的稀疏矩阵。这个矩阵是可以优化的，就是将大矩阵变成一个字段，不用布尔值存储，而是直接将特殊字段以字符串的形式存放在这个字段中。这是一个用处理时间来换取存储空间的思路，这样又引出一个新的问题，处理时间的代价如何衡量？同时，这个第 4 点需求，仅仅存在于 **特殊字段未被检测到** ，这样一个前提下，当字段检测到，我们有了足够的数据样例，那么这个存储空间就可以释放出来。

问题 a 可以通过添加字典表来解决，因为最大的冗余就是公司名称的重复，将公司名称作字典表，就可以释放出作为大头的那一部分空间。

问题 b 没有很好的解决方法，在解决了问题 a 之后，存储的这些时间值，成为了新的最占用空间的数据。放宽精度，将时间也做成一个字典表？

对于问题 c，我纠结了很久，最后决定放弃 MySQL 的思路，因为数据库维护的代价相对 Redis 太大了。

### Redis 如何解决这个问题

首先，每一个特殊字段设置为一个 set 字段，每当检测到特殊字段，就将其推入 set 中；当特殊字段的样例获取足够，我们通过样例更新补全了代码，则销毁该 set 字段，释放空间。

思路很清晰明了，实现也十分地简单，维护代价几乎为 0 。

那么这就意味着，这样一个小的服务，却需要同时使用 MySQL、Redis 两个数据库？更直观地说，我需要写 MySQL 和 Redis 两套数据库的操作代码？

能否用 Redis 来实现 MySQL 所负责的那部分需求呢？

Redis 可以用来存储键值序列这样的二维数据，在我们引入了时间统计之后，这变成了一个三维的数据，有两个解决思路。

1. 使用 json 来扩充数据维度。
2. 将字段名作为新的维度。

使用 json 扩充维度会加大后续数据分析的难度，也会拖慢分析效率，因为要分出一部分性能用来展开多维数据。在锁定了数据最多只有三维的情况下，我选择了第二种思路。

于是新的问题又出现了，选择什么样的数据结构来存储这些数据？

在[之前 Redis 的学习笔记中](https://blog.harumonia.moe/2020/02/12/2020-02-12-Redis-note-2/)，有通过 bitmap 来实现大量统计数据存储的案例。

## 问题解决

至此，路线逐渐成形：

1. 额外维护近似静态的公司名字典(只在每轮更新结束之后对该字典进行增改，注：不会删除)；将该字典的 index 作为 bitmap 的键，完成情况作为 bitmap 的值。
2. 将粗粒度的时间作为字段名，每半小时分离出新字段。
3. 设置一个额外线程，每半小时将本轮次的运行情况合并到以轮次为名称的 bitmap 字段中。

空间用量：

```math
400000 / 1024 / 1024 / 8MB * 2 ≈ 0.1MB
```

当然还会有一些不太容易量化的额外空间消耗(键消耗等)，这些空间消耗也许要比本身数据 0.1MB 要大，不过可以肯定的是，比起 MySQL 要轻量不少。同时，维护的成本很低，调用的性能很高，这一点在实际的使用中也有所体现。

ps1. 需要说明的是，特殊字段的存储不在这个计算中，而是新开了 set，不将特殊字段的空间加入计算，因为本身特殊字段就是暂存字段，在实际作业中，第一轮进行到 34%左右时，特殊字段就已经全部被检测完毕，并且销毁掉了。

ps2. 键的生成由代码决定而不是由定时任务决定，所以也避免了空键浪费存储空间的问题。

结果如图所示。

![result.png](https://harumona-blog.oss-cn-beijing.aliyuncs.com/new_articles/spider-stat.PNG)

## 总结

MySQL->关系型数据库的泛用性很高，但是非关系型数据库、NoSql 等的存在也并非没有理由，因地制宜地选取数据库，根据数据库的特性去进行压榨，"让专业的人做专业的事"。
