RSS(Really Simple Syndication, 简易信息聚合)是一种用于聚合发布经常更新的网站内容的一种格式规范,在一些启用RSS源的网站上,通过访问其RSS源可以获得目标网站的最新发布信息,也可以通过目标网站提供的一些筛选手段,获得感兴趣的那部分内容。因此RSS最大的特征就在于其时效性,查陈年老黄历显然没办法用RSS来完成,但有时候确实需要查一下,那就需要将RSS数据持久化。在本篇文章中,我们选用关系型数据库MySQL为例,并以Python语言为工具,示范一个持久化的例子。
首先需要明确一点,RSS的定义是十分松散的,一个字段可以是任何类型的数据(尽管从技术上讲,数据的类型都是字符串),因此就数据表结构而言,就必须具体问题具体分析,从实际出发根据需求建表(那些主张一键转换的歪果仁都是些犯形而上学错误的家伙)。当然什么都不管,就是想把RSS储存下来,也不是不可以,就是数据库的体积会稍微大一些(通常会大30倍),详情可以直接跳到第二章
马克思主义活的灵魂(笑)
目标RSS源是Torrent发布站点nyaa.si,该站提供RSS源,地址为https://nyaa.si/?page=rss,观察RSS字段,我们可以发现有下列几个我们会比较感兴趣。那么接下来就根据这些字段建表,例如名叫nyaa
字段 | 字段语义 |
guid | 全局唯一编号 |
title | 描述字符串 |
nyaa:category | 分类字符串 |
nyaa:infoHash | 信息散列 |
link | 链接地址 |
pubDate | 发布时间 |
nyaa:size | 大小信息 |
CREATE TABLE `nyaa` ( `sid` INT(10) UNSIGNED NOT NULL, `title` TEXT NOT NULL COLLATE 'utf8mb4_unicode_ci', `link` TEXT NOT NULL COLLATE 'utf8mb4_unicode_ci', `pubtime` INT(10) UNSIGNED NOT NULL, `size` VARCHAR(20) NOT NULL COMMENT 'File size' COLLATE 'utf8mb4_unicode_ci', `cls` TINYTEXT NOT NULL COMMENT 'Class of feed' COLLATE 'utf8mb4_unicode_ci', `infohash` CHAR(40) NOT NULL COLLATE 'utf8mb4_unicode_ci', PRIMARY KEY (`sid`), INDEX `infohash` (`infohash`) ) COMMENT='Feeds From nyaa' COLLATE='utf8mb4_unicode_ci' ENGINE=InnoDB ROW_FORMAT=COMPRESSED ;
需要提醒的是,注意到ROW_FORMAT=COMPRESSED
,该表使用了压缩的行格式。这是因为在表定义中使用了很多不定长数据类型TEXT,同时link字段下会有大量重复的片段,因此使用压缩会有效降低数据表大小。另一方面注意到`pubtime` INT(10) UNSIGNED NOT NULL,
,时间这一项,在这里用了无符号整型来储存,当然使用TIMESTAMP类型储存也是完全没问题的,倒不如说,那样更合情合理,因此可以根据个人偏好决定。
尽管对于link字段下的重复片段可以用程序手动进行压缩(比如只储存变动部分,不变部分硬编码到程序中),但正如前面所说的,RSS是定义十分松散的,能以原始数据储存的,尽量以原始数据储存。比起在持久化完成后的后处理中Raise Exception,在持久化过程中Raise Exception更令人生厌。
接下来就是主要的Python代码,代码假定表结构不再变化,且字段顺序不变(因为那样代码会短很多)。对付重复数据,这里采用的是INSERT IGNORE
(第26行),当然用ON DUPLICATE KEY UPDATE
也是没有问题的。当然数据库连接那边的具体参数(第40行),也要实事求是。值得注意的是由于executemany(第43行)对INSERT
操作有魔法buff增益,请务必不要用for循环与execute,否则性能损失空前巨大。
PROXIES={} DB_USER=None DB_NAME=None DB_HOST=None def restruct(entry): # 按数据库列顺序重排 from time import mktime if entry: return ( int(entry.id.split('/')[-1]), entry.title, entry.link, round(mktime(entry.published_parsed)), entry.nyaa_size, entry.nyaa_category, entry.nyaa_infohash, ) return 7 def store(): rssurl = 'https://nyaa.si/?page=rss' sqlsentence = 'INSERT IGNORE INTO nyaa VALUES ({place});'.format(place=','.join(['%s'] * restruct(None)),) import requests as req from feedparser import parse ret = req.get(url=rssurl, proxies=PROXIES) if not ret.ok: raise RuntimeError('Response %s', ret.status_code) parsedfeed = parse(ret.content) from mysql.connector import connect connection = connect(user=DB_USER, db=DB_NAME, host=DB_HOST) cursor = connection.cursor() cursor.executemany(sqlsentence, map(restruct, parsedfeed.entries)) connection.commit() if __name__ == '__main__': store()
新时代中国特色方法
你有没有发现如果用马克思主义经典作家的方法,那么当你需要爬好多RSS的时候,会遇到一个写了好多相同的代码——如果能将代码妥善地包装起来,只要写一个配置文件那该多好。
🎉 锵锵锵 🎉
全球首个(暂定)RSS转SQL工具(Python实现)上线啦 !
🎉 🎉 🎉
命令行输入pip install rss2sql
即刻拥有
🎉 🎉 🎉
详情请访问https://github.com/jsjyhzy/rss2sql
安装完成之后就只需要编写配置文件,配置文件使用的yaml语法,接下来用一个例子,稍微解释一下该怎么写配置文件
rss: url: https://nyaa.si/?page=rss proxies: https: http://localhost:8188 sql: tablename: nyaa field: - name: nyaa_id val: "None" type: INT nullable: false primary_key: true autoincrement: true - name: isSukebei val: "False" type: BOOLEAN - name: title val: "x.get('title')" type: TEXT - name: link val: "x.get('link')" type: VARCHAR type_parameter: 50 unique: true - name: category val: "x.get('nyaa_category')" type: REFTABLE type_parameter: - VARCHAR - 50 - name: pubtime val: "ToolKit.struct_time_To_datetime(x.get('published_parsed'))" type: TIMESTAMP index: true
RSS 段
这里主要定义两个东西,RSS的地址(url)和访问RSS时使用的代理(proxies),如果不需要使用代理,那么proxies那里留空也许,当然proxies都不写也行
SQL 段
这一块就是最繁重的部分,主要定义两方面内容:表名(tablename)和表字段(fields),表名就不多说了,不要写些奇奇怪怪的名字就行,重点在于表字段。
自增主键
有的时候只是单纯想要一个主键,以便于其他表建立外键,虽说通常我们可以选那些unique的字段当主键,但unique的字段很有可能时VARCHAR,那么其他表建立外键时就要用这种类型,无形中就增加了其他表的大小。这时候可以选择一个INT类型作为主键。比如在例子中,我们建立了一个nyaa_id
(通过定义name
) 的主键(通过定义primary_key
为true
);其类型为INT
(通过定义type
);不可空置(通过定义nullable
为false
);自增(通过定义autoincrement
为true
);取值为None
(通过定义val
为"None"
),亦即插入记录时,该字段无值,那么数据库会自动分配一个值。这样就完成了自增主键的定义
常量
可能会奇怪为什么有常量字段,其实这通常会发生在一个RSS具有表世界与里世界两个世界,然后就是想把两个世界的记录放在一张表里。比如例子中isSukebei
字段取值恒为"False"
,要注意这个False首字母要大写,
Python 里是 True/False;
YAML 里是 true/false。
最小配置
注意到定义title
字段时取值是"x.get('title')"
,这里面的x
就是feedparser解析完成之后每一个RSS item的字典对象。因为只不过是个字典,当然写起来能写出"x['title']"
,但这不是用get方法还能设置默认值嘛。
含参数的字段
某些类型,需要一些参数,比如说VARCHAR,需要定义最大长度,那就可以通过定义type_parameter
为50
来取得VARCHAR(50)
参考表(Reference Table)
很多时候,会遇到某些字段是个类型语义的,直接储存为VARCHAR,恕我直言,过于愚蠢。而作为ENUM类型储存,倒也不是不可以,只不过ENUM要增加一个种类时,会比较令人揪心。完全不会增加删减类别的字段又可遇不可求,比如“性别”这个语义本来只需要“男”或“女”,看起来好像不需要变动,现在不也是….😂。因此在标准SQL类型上,追加了一个REFTABLE
类型,将这个愚蠢的VARCHAR移到另一张表,变成一个睿智的VARCHAR(通过 type_parameter
定义了VARCHAR(50)
),再用一个INT类型作为沟通桥梁。
一行写不下的取值
val
定义的东西,程序中是通过eval方法对其求值。有些东西对于eval方法来说,它就是不太好实现,比如说要引入其他库的方法,对取值进行一下转换, 最常见的问题就是如何将RSS中的时间字符串,变换成数据库支持的时间字符串。eval的环境变量中有一个ToolKit的类,这个类中包含着一些常见的转换方法,比如在例子中用ToolKit.struct_time_To_datetime
方法将time.struct_time
类型转换为了datetime.datetime
类型,从而让SQLAlchemy得以自动转换。
当然如果ToolKit里没有要用的函数,现阶段只能fork之后自己加上去,以后也许会加入可以向ToolKit动态追加方法的功能吧😀
当然提个Pull request也是欢迎的。
未来形而上学
首先,要搞明白什么叫做将RSS储存下来。RSS究其本质是一个XML标记文件,当我们用Python的feedparser库解析完成之后,对于每一个entry而言(或者说是RSS规定的概念 item ),就是一个Python的Dict对象。就从Duck Typing哲学的角度讲,我只要把这个Dict对象储存起来,那我就相当于把RSS储存了下来;于此同时,也可以把XML保存下来(看起来很蠢,其实这也算不上太蠢,储存的时候相当于可以做 column compression ,相比MySQL只能做 row compression ,体积会小很多。比如一个6k的json格式推特推文数据集,储存于MySQL占用22MB,然而导出为sql文件,哪怕只用zip压缩一下,也不过3.9MB 🤣 ),但只用关系型数据库实现起来,不是很合适,所以姑且不谈。
接下来就是要思考如何唯一确定一个Dict对象。关于这一点,看起来有点难,其实很简单:feedparser解析后产生的entry,必有一个id属性。
最后就是如何储存这个Dict对象。那就更简单了,MySQL在5.7.8版本后支持JSON类型,然后把这个对象变成JSON,就只需要from json import dumps; dumps(...)
。
可能需要提醒的一点就是,RSS规范要求 item 必须要有三个字段:title、link、description,对应Dict对象就是title、link、summary三个属性。设计数据表的时候如果有需要用到FULLTEXT、UNIQUE索引,可以把这几个字段单列出来;如果只是需要对这几个字段做个普通索引或者干脆不做索引,那直接做个虚列就可以了。