$ ls ~yifei/notes/

爬虫数据存储的一些选择

Posted on:

Last modified:

爬虫是一种典型的「读少写多」的应用场景。而且爬虫产生的数据一般是作为离线数据,而不是在线数据。 也就是说这些数据主要是用于离线数据分析,而不是直接供线上用户查询使用。

即使线上需要使用爬虫数据,也会需要经过清洗处理过后再放到在线库。总之,不会直接供线上直接使用。

本文总结了一些经验和踩过坑,希望能对读者有帮助。

写入通用原则

  1. 要批量写,不要挨个写
  2. 要顺序写,不要随机写

如果我们开了一个多线程的爬虫,然后每个线程每爬到一条数据就调一下 db.insert(item) 插入数据,数据一多就是灾难性的。

首先,每个线程持有一个数据库链接对数据库的负载就产生了不小压力。其次,每条数据都去调用数据库,那么每次插入时间都得 加上数据库的往返时间,也就是 2RTT(round trip time)。再者,每次插入的都是不同的数据,可能在磁盘的不同位置,导致 磁盘的写入时间大部分都花在寻道上了,磁盘 IO 时间会大幅提升。最后,如果是 SQL 型的数据库,默认配置下, 可能还会有事务的影响。

正确的做法是——用队列。不同的线程都把数据先发到一个队列中,不管是你用的语言自带的内存里的 Queue,还是 redis list, 或者是 kafka,都可以。然后由另一个线程或者脚本读取这个队列,把数据整理之后,定期或者定量写入数据库。

这样做基本上解决了上面提到的每个问题。只有存储线程持有数据库链接,每一条数据不会在需要 2RTT,摊薄下来可能是 2RTT / 1000, 数据经过整理后,每次可以都插入统一中类型的数据,磁盘不需要总是在寻道。

当然,这种方案也会引入一些新的问题,需要注意解决:

  1. 处理流程变成了异步,不能实时在数据中看到最新的爬取结果
  2. 多了消息队列的环节,多了丢数据的可能
  3. 如果消息队列是内存性的,不要让消息队列爆了

以上这些问题都是使用 MQ 的常见问题了,这里不再展开。

数据库选型

大概有这些选择:

  1. CSV、JSON、SQLite 或者其他单机文件
  2. SQL 数据库
  3. MongoDB 等文档数据库
  4. HBase 等 Hadoop 生态圈存储
  5. S3 类型对象存储
  6. Kafka 等持久性消息队列

如果你只是简单地「单线程」爬几页数据分析用,那么存个 CSV 或者 JSON 就可以了。如果你开始上多线程,甚至多机了, 既要考虑写入的时候加锁,而且没法分布式写入,单文件存储就不太合适了。

规模再大一点可以考虑 MySQL。虽然 MySQL 是一个 OLTP 数据库,通常意义上来说更适用于线上数据库。但是对于数据量不大的爬虫来说, 比如说总数据量不会超过 100GiB,也已经足够用了。而且 MySQL 可以添加 unique 索引,一定程度上还能帮助解决数据去重的问题。 这里有几点需要注意:

  1. 使用 ORM 创建的表中包含了不少外键约束之类的东西,对于爬来的数据,中间插入的时候可能还不满足这个外键,最好把这个约束删除掉

有些人喜欢用 MongoDB 来存储爬虫数据,他们给出的理由也很有吸引力——爬虫数据多是半结构化的,而且数据结构可能经常跟着源网站变, 用 MongoDB 这种 schemaless 的文档数据库再合适不过了。然而我个人非常不推荐用 MongoDB,原因如下:

  1. 我觉得定义好表结构不是一个缺点,反而是一个优点,这样能够在开发调试阶段就发现各种异常情况,保证程序稳定
  2. MySQL 的图形化客户端太多了,比如说 Navicat,Sequel Pro 等等。对于小公司来说,这个客户端就已经够用了,根本不需要开发什么单独的管理后台 相反,MongoDB 基本没有什么特别好用的客户端
  3. MySQL 和 Postgres 也早就支持了 JSON 字段,实在不是特别规整的数据,存在 JSON 字段就行了
  4. 数据分析的第三方库,比如 pandas,对 SQL 的支持都是原生的,一个 read_sql 就把数据读出来了

数据再多一些,或者并发量再大一些的话,可能单独使用 MySQL 就不合适了。这时候你可以对 MySQL 做定期归档,比如说把添加时间在一个月以上的数据 都按日期写入到 Hive 或者 S3 中,然后删除掉 MySQL 中的数据。这样的做法,其实相当于隐式地把 MySQL 作为了一个消息队列,并起到了缓冲的作用。

再者,MySQL 这种毕竟是行式数据库,如果你的数据数值居多,也可以跳过 MySQL,考虑直接存储到列式数据库中。下游的 Spark,Flink 这些消费端可能更喜欢读取列式数据。

最后一种选项是直接使用 Kafka 这种持久化的消息队列作为存储。DDIA 这本书中提到一个有趣的观点:数据库是日志的积分,日志是数据库的导数。 从某种意义上来说,两者所含有的信息是等价的,可以相互转换。所以直接使用消息队列作为数据存储也未尝不可。

总之,对于爬虫这种场景来说,最重要的特点是「读少写多」,按照这个思路去选择问题不大。除了这里提到的一些数据库,还有 Cassandra,FoundationDB 等一些 数据库没有提到,在特定的场景下也都值得考虑。对于存储的选择也不只是一个技术问题,可能更重要的是你的公司现在有什么,选一个比较合适的用就好了。

存储到 sql 数据库

爬到的数据往往会出现这样一种情况:一个对象的不同字段分别在两个接口里。这时候,比较直接的 思路是存储到一张表里,先存一部分数据,再从另一个接口里爬到数据之后,update 对应的数据。

这样的思路虽然直接,但是实际使用中会遇到一些问题:

  1. 使用数据时,需要判断一条数据完整还是不完整
  2. 爬虫是高并发应用,如果两个脚本同时 insert 和 update 一个表,可能引起性能下降甚至发生死锁。

所以,更好的方式是一个 API 就对应一个表,比如一个帖子的数据要从两个 API 爬到,那就分成 post_summary, post_detail 两个表,分别存储,使用的时候再去 join 就好了。

参考

  1. https://www.zhihu.com/question/479761564
  2. https://www.zhihu.com/question/36110917
WeChat Qr Code

© 2016-2022 Yifei Kong. Powered by ynotes

All contents are under the CC-BY-NC-SA license, if not otherwise specified.

Opinions expressed here are solely my own and do not express the views or opinions of my employer.

友情链接: MySQL 教程站