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

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

笔者写爬虫时常用的数据有 MySQLmongoDBRedis,这三个各有长处,用来针对不同的业务需求;而在写 web 服务时,则常用 MySQLSQLite

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

需求分析

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

  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 的学习笔记中,有通过 bitmap 来实现大量统计数据存储的案例。

问题解决

至此,路线逐渐成形:

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

空间用量:

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

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

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

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

结果如图所示。

result.png

总结

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