Python非关系型数据库

Published on 2017 - 02 - 04

有些数据库并不是关系型的,不支持 SQL。它们用来处理庞大的数据集、支持更加灵活的数据定义以及定制的数据操作。这些被统称为 NoSQL(以前的意思是 no SQL,现在理解为 not only SQL)。

dbm family

dbm 格式在 NoSQL 出现之前已存在很久了,它们是按照键值对的形式储存,封装在应用程序(例如网页浏览器)中,来维护各种各样的配置。从以下角度看,dbm 数据库和 Python 字典是类似的:

  • 给一个键赋值,自动保存到磁盘中的数据库;
  • 通过键得到对应的值。

下面简单的例子中,open() 方法的第二个参数 'r' 代表读;'w' 代表写;'c' 表示读和写,如果文件不存在则创建之:

如果文件不存在则创建之:

>>> import dbm
>>> db = dbm.open('definitions', 'c')

同字典一样创建键值对,给一个键赋值:

>>> db['mustard'] = 'yellow'
>>> db['ketchup'] = 'red'
>>> db['pesto'] = 'green'

停下来看看数据库中存放了什么:

>>> len(db)
3
>>> db['pesto']
b'green'

现在关掉数据库,然后重新打开验证它是否被完整保存:

>>> db.close()
>>> db = dbm.open('definitions', 'r')
>>> db['mustard']
b'yellow'

键和值都以字节保存,因此不能对数据库对象 db 进行迭代,但是可以使用函数 len() 得到键的数目。注意 get() 和 setdefault() 函数只能用于字典的方法。

memcached

memcached 是一种快速的、内存键值对象的缓存服务器。它一般置于数据库之前,用于存储网页服务器会话数据。Linux 和 OS X 点此链接 下载,而 Windows 系统在此 下载。如果你想要尝试使用,需要一个 memcached 服务器和 Python 的驱动程序。

当然存在很多这样的驱动程序,其中能在 Python 3 使用的是 python3-memcached ,可以通过下面这条命令安装:

$ pip install python-memcached

连接到一个 memcached 服务器之后,可以做以下事项:

  • 赋值和取值
  • 其中一个值的自增或者自减
  • 删除其中一个键

数据在 memcached 并不是持久化保存的,后面的可能会覆盖早些写入的数据,这本来就是它的固有特性,因为它作为一个缓存服务器,通过舍弃旧数据避免程序运行时内存不足的问题。

你也可以同时连接到多个 memcached 服务器。不过下面的例子只连到一个:

>>> import memcache
>>> db = memcache.Client(['127.0.0.1:11211'])
>>> db.set('marco', 'polo')
True
>>> db.get('marco')
'polo'
>>> db.set('ducks', 0)
True
>>> db.get('ducks')
0
>>> db.incr('ducks', 2)
2
>>> db.get('ducks')
2

Redis

Redis 是一种数据结构服务器(data structure server)。和 memcached 类似,Redis 服务器的所有数据都是基于内存的(现在也可以选择把数据存放在磁盘)。不同于 memcached,Redis 可以实现:

  • 存储数据到磁盘,方便断电重启和提升可靠性;
  • 保存旧数据;
  • 提供多种数据结构,不限于简单字符串。

Redis 的数据类型和 Python 很相近,Redis 服务器会是一个或多个 Python 应用程序之间共享数据的非常有帮助的中间件。据我的经验,值得用一定的篇幅介绍它。

Python 的 Redis 驱动程序 redis-py 在 GitHub 托管代码和测试用例,也可在此参考在线文档 。可以使用这条命令安装它:

$ pip install redis

Redis 服务器自身就有好用的文档。如果在本地计算机(网络名为 localhost)安装和启动了 Redis 服务器,就可以开始尝试下面的程序。

字符串

具有单一值的一个键被称作 Redis 的字符串。简单的 Python 数据类型可以自动转换成 Redis 字符串。现在连接到一些主机(默认 localhost)以及端口(默认 6379)上的 Redis 服务器:

>>> import redis
>>> conn = redis.Redis()

redis.Redis('localhost') 或者 redis.Redis('localhost', 6379) 会得到同样的结果。

列出所有的键(目前为空):

>>> conn.keys('*')
[]

给键 'secret' 赋值一个字符串;给键 'carats' 赋一个整数;给键 'fever' 赋一个浮点数:

>>> conn.set('secret', 'ni!')
True
>>> conn.set('carats', 24)
True
>>> conn.set('fever', '101.5')
True

通过键反过来得到对应的值:

>>> conn.get('secret')
b'ni!'
>>> conn.get('carats')
b'24'
>>> conn.get('fever')
b'101.5'

这里的 setnx() 方法只有当键不存在时才设定值:

>>> conn.setnx('secret', 'icky-icky-icky-ptang-zoop-boing!')
False

方法运行失败,因为之前已经定义了 'secret':

>>> conn.get('secret')
b'ni!'

方法 getset() 会返回旧的值,同时赋新的值:

>>> conn.getset('secret', 'icky-icky-icky-ptang-zoop-boing!')
b'ni!'

先不急着继续下面的内容,看之前的操作是否可以运行?

>>> conn.get('secret')
b'icky-icky-icky-ptang-zoop-boing!'

使用函数 getrange() 得到子串(偏移量 offset:0 代表开始,-1 代表结束):

>>> conn.getrange('secret', -6, -1)
b'boing!'

使用函数 setrange() 替换子串(从开始位置偏移):

>>> conn.setrange('secret', 0, 'ICKY')
32
>>> conn.get('secret')
b'ICKY-icky-icky-ptang-zoop-boing!'

接下来使用函数 mset() 一次设置多个键值:

>>> conn.mset({'pie': 'cherry', 'cordial': 'sherry'})
True

使用函数 mget() 一次取到多个键的值:

>>> conn.mget(['fever', 'carats'])
[b'101.5', b'24']

使用函数 delete() 删掉一个键:

>>> conn.delete('fever')
True

使用函数 incr() 或者 incrbyfloat() 增加值,函数 decr() 减少值:

>>> conn.incr('carats')
25
>>> conn.incr('carats', 10)
35
>>> conn.decr('carats')
34
>>> conn.decr('carats', 15)
19
>>> conn.set('fever', '101.5')
True
>>> conn.incrbyfloat('fever')
102.5
>>> conn.incrbyfloat('fever', 0.5)
103.0

不存在函数 decrbyfloat(),可以用增加负数代替:

>>> conn.incrbyfloat('fever', -2.0)
101.0

列表

Redis 的列表仅能包含字符串。当第一次插入数据时列表被创建。使用函数 lpush() 在开始处插入:

>>> conn.lpush('zoo', 'bear')
1

在开始处插入超过一项:

>>> conn.lpush('zoo', 'alligator', 'duck')
3

使用 linsert() 函数在一个值的前或者后插入:

>>> conn.linsert('zoo', 'before', 'bear', 'beaver')
4
>>> conn.linsert('zoo', 'after', 'bear', 'cassowary')
5

使用 lset() 函数在偏移量处插入(列表必须已经存在):

>>> conn.lset('zoo', 2, 'marmoset')
True

使用 rpush() 函数在结尾处插入:

>>> conn.rpush('zoo', 'yak')
6

使用 lindex() 函数取到给定偏移量处的值:

>>> conn.lindex('zoo', 3)
b'bear'

使用 lrange() 函数取到给定偏移量范围(0~-1 代表全部)的所有值:

>>> conn.lrange('zoo', 0, 2)
[b'duck', b'alligator', b'marmoset']

使用 ltrim() 函数仅保留列表中给定范围的值:

>>> conn.ltrim('zoo', 1, 4)
True

使用函数 lrange() 得到一定范围的的值(0~-1 代表全部):

>>> conn.lrange('zoo', 0, -1)
[b'alligator', b'marmoset', b'bear', b'cassowary']

哈希表

Redis 的哈希表类似于 Python 中的字典,但它仅包含字符串,因此只能有一层结构,不能进行嵌套。下面的例子创建了一个 Redis 的哈希表 song,并对它进行操作。

使用函数 hmset() 在哈希表 song 设置字段 do 和字段 re 的值:

>>> conn.hmset('song', {'do': 'a deer', 're': 'about a deer'})
True

使用函数 hset() 设置一个单一字段值:

>>> conn.hset('song', 'mi', 'a note to follow re')
1

使用函数 hget() 取到一个字段的值:

>>> conn.hget('song', 'mi')
b'a note to follow re'

使用函数 hmget() 取到多个字段的值:

>>> conn.hmget('song', 're', 'do')
[b'about a deer', b'a deer']

使用函数 hkeys() 取到所有字段的键:

>>> conn.hkeys('song')
[b'do', b're', b'mi']

使用函数 hvals() 取到所有字段的值:

>>> conn.hvals('song')
[b'a deer', b'about a deer', b'a note to follow re']

使用函数 hlen() 返回字段的总数:

>>> conn.hlen('song')
3

使用函数 hgetall() 取到所有字段的键和值:

>>> conn.hgetall('song')
{b'do': b'a deer', b're': b'about a deer', b'mi': b'a note to follow re'}

使用函数 hsetnx() 对字段中不存在的键赋值:

>>> conn.hsetnx('song', 'fa', 'a note that rhymes with la')
1

集合

正如你会在下面看到的例子所示,Redis 的集合和 Python 的集合是完全类似的。

在集合中添加一个或多个值:

>>> conn.sadd('zoo', 'duck', 'goat', 'turkey')
3

取得集合中所有值的数目:

>>> conn.scard('zoo')
3

返回集合中的所有值:

>>> conn.smembers('zoo')
{b'duck', b'goat', b'turkey'}

从集合中删掉一个值:

>>> conn.srem('zoo', 'turkey')
True

新建一个集合以展示一些集合间的操作:

>>> conn.sadd('better_zoo', 'tiger', 'wolf', 'duck')
0

返回集合 zoo 和集合 better_zoo 的交集:

>>> conn.sinter('zoo', 'better_zoo')
{b'duck'}

获得集合 zoo 和集合 better_zoo 的交集,并存储到新集合 fowl_zoo:

>>> conn.sinterstore('fowl_zoo', 'zoo', 'better_zoo')
1

哪一个会在集合 fowl_zoo 中?

>>> conn.smembers('fowl_zoo')
{b'duck'}

返回集合 zoo 和集合 better_zoo 的并集:

>>> conn.sunion('zoo', 'better_zoo')
{b'duck', b'goat', b'wolf', b'tiger'}

存储并集结果到新的集合 fabulous_zoo:

>>> conn.sunionstore('fabulous_zoo', 'zoo', 'better_zoo')
4
>>> conn.smembers('fabulous_zoo')
{b'duck', b'goat', b'wolf', b'tiger'}

什么是集合 zoo 包含而集合 better_zoo 不包含的项?使用函数 sdiff() 得到它们的差集,sdiffstore() 将存储到新集合 zoo_sale:

>>> conn.sdiff('zoo', 'better_zoo')
{b'goat'}
>>> conn.sdiffstore('zoo_sale', 'zoo', 'better_zoo')
1
>>> conn.smembers('zoo_sale')
{b'goat'}

有序集合

Redis 中功能最强大的数据类型之一是有序表(sorted set 或者 zset)。它里面的值都是独一无二的,但是每一个值都关联对应浮点值分数(score)。可以通过值或者分数取得每一项。有序集合有很多用途:

  • 排行榜
  • 二级索引
  • 时间序列(把时间戳作为分数)

我们把最后一个(时间序列)作为例子,通过时间戳跟踪用户的登录。在这里,时间表达使用 Unix 的 epoch 值,它由 Python 的 time() 函数返回:

>>> import time
>>> now = time.time()
>>> now
1361857057.576483

首先增加第一个访客:

>>> conn.zadd('logins', 'smeagol', now)
1

5 分钟后,又一名访客:

>>> conn.zadd('logins', 'sauron', now+(5*60))
1

两小时后:

>>> conn.zadd('logins', 'bilbo', now+(2*60*60))
1

一天后,负载并不是很多:

>>> conn.zadd('logins', 'treebeard', now+(24*60*60))
1

那么 bilbo 登录的次序是什么?

>>> conn.zrank('logins', 'bilbo')
2

登录时间呢?

>>> conn.zscore('logins', 'bilbo')
1361864257.576483

按照登录的顺序查看每一位访客:

>>> conn.zrange('logins', 0, -1)
[b'smeagol', b'sauron', b'bilbo', b'treebeard']

附带上他们的登录时间:

>>> conn.zrange('logins', 0, -1, withscores=True)
[(b'smeagol', 1361857057.576483), (b'sauron', 1361857357.576483),
(b'bilbo', 1361864257.576483), (b'treebeard', 1361943457.576483)]

位图

位图(bit)是一种非常省空间且快速的处理超大集合数字的方式。假设你有一个很多用户注册的网站,想要跟踪用户的登录频率、在某一天用户的访问量以及同一用户在固定时间内的访问频率,等等。当然,你可以使用 Redis 集合,但如果使用递增的用户 ID,位图的方法更加简洁和快速。

首先为每一天创建一个位集合(bitset)。为了测试,我们仅使用 3 天和部分用户 ID:

>>> days = ['2013-02-25', '2013-02-26', '2013-02-27']
>>> big_spender = 1089
>>> tire_kicker = 40459
>>> late_joiner = 550212

每一天是一个单独的键,对应的用户 ID 设置位,例如第一天(2013-02-25)有来自 big_spender(ID 1089) 和 tire_kicker(ID 40459) 的访问记录:

>>> conn.setbit(days[0], big_spender, 1)
0
>>> conn.setbit(days[0], tire_kicker, 1)
0

第二天用户 big_spender 又有访问:

>>> conn.setbit(days[1], big_spender, 1)
0

接下来的一天,朋友 big_spender 再次访问,并又有新人 late_joiner 访问:

>>> conn.setbit(days[2], big_spender, 1)
0
>>> conn.setbit(days[2], late_joiner, 1)
0

现在统计得到这三天的日访客数:

>>> for day in days:
...     conn.bitcount(day)
...
2
1
2

查看某一天某个用户是否有访问记录?

>>> conn.getbit(days[1], tire_kicker)
0

显然 tire_kicker 在第二天没有访问。

有多少访客每天都会访问?

>>> conn.bitop('and', 'everyday', *days)
68777
>>> conn.bitcount('everyday')
1

让你猜三次他是谁:

>>> conn.getbit('everyday', big_spender)
1

最后,这三天中独立的访客数量有多少?

>>> conn.bitop('or', 'alldays', *days)
68777
>>> conn.bitcount('alldays')
3

缓存和过期

所有的 Redis 键都有一个生存期或者过期时间(expiration date),默认情况下,生存期是永久的。也可以使用 expire() 函数构造 Redis 键的生存期,下面看到的设置值是以秒为单位的数:

>>> import time
>>> key = 'now you see it'
>>> conn.set(key, 'but not for long')
True
>>> conn.expire(key, 5)
True
>>> conn.ttl(key)
5
>>> conn.get(key)
b'but not for long'
>>> time.sleep(6)
>>> conn.get(key)
>>>

expireat() 命令给一个键设定过期时间,对于更新缓存是有帮助的,并且可以限制登录会话。

参考文档