Python,爬虫与深度学习(15)——番外篇(四)Redis数据库与python
步骤/目录:
1.Redis简介与安装
2.redis的数据结构
(1)string
(2)list
(3)set
(4)hash
(5)zset
(6)HLL
3.redis的一些命令
4.redis事务
5.TTL(time to live)
6.排序
7.发布/订阅
8.管道
9.redis GEO
10.其他
本文首发于个人博客https://lisper517.top/index.php/archives/53/
,转载请注明出处。
本文的目的是初步介绍Redis、为爬虫使用redis打好基础,主要整理自 runoob教程 。这里介绍的内容非常浅显,更深入的内容请自行查询书籍。
本文写作日期为2022年9月15日。运行的平台为win10和树莓派,编辑器为VS Code。
1.Redis简介与安装
Redis是一个由 Salvatore Sanfilippo 在2008年开始创造的 key-value 存储系统,是一种开源的非关系型(NoSql)数据库,它的特点有:基于内存,分布式,键值对,支持多种语言的API(Redis本身是由ANSI C写的)。虽然它主要在内存中运行,但也可以将数据保存在磁盘;它支持主从式的数据备份;它的读写性能极高(所以把Redis开放到公网上时还要注意安全性);它支持事务,多个操作可以打包到一起,要么不执行,要么全部执行(all or nothing);它支持5种数据类型(字符串,列表,hash散列,集合,有序集合);它支持推送,key过期。总的来说Redis就是好。在使用Redis时,你可以把它用作主数据库,也可以用作辅数据库,不论何种方式,都是对关系型数据库功能的极大补充。
由于redis的一些特性,它特别适合做存储标签这种一对多的工作,而存储标签在mysql这样的关系型数据库中比较麻烦;redis可以设置键的过期时间TTL(time to live),到期后自动过期,这是很多服务都需要的(比如登录cookie的过期);redis的读写效率非常高,在轻度使用中很少会遇到redis是瓶颈的情况;redis的命令比sql要简单得多(因为它以键值对形式存储数据)。
下面介绍三种安装redis的方法。需要注意,redis为了效率而牺牲了安全性,所以默认情况下redis几乎没有保护措施,不要把它开放到外网。
在windows上安装redis:window版redis由热心网友维护,版本远落后于linux版或docker版(目前是5.0和7.0的差距),所以不推荐下载。需要的话,建议在windows上用docker安装redis,见 Docker入门(一) 。
如果嫌麻烦的话,到 windows版redis下载网页 下载zip文件或msi文件,解压或安装即可。解压zip文件,或用msi文件安装后,在cmd中进入redis目录,运行 redis-server.exe redis.windows.conf
即可运行redis服务器端。然后在同样的目录下,运行 redis-cli.exe -h 127.0.0.1 -p 6379
即可连接到redis数据库。但是要注意,目前它的版本只有5.0,笔者在后续将演示redis使用tls双向验证,但这是redis 6.0后才加入的功能。所以有条件的话还是用docker装redis。
在linux上:到 https://download.redis.io/releases/ 下载tar.gz文件,解压、 make
即可。具体过程为:
mkdir /usr/local/redis
cd /usr/local/redis
wget https://download.redis.io/releases/redis-7.0.4.tar.gz #最新版本的redis
tar -xzf redis-7.0.4.tar.gz
cd redis-7.0.4
apt-get install pkg-config make gcc libssl-dev #一些依赖项
make BUILD_TLS=yes
BUILD_TLS=yes
这个选项在redis 6.0以后可用,是用tls在传输层对redis通信加密的一种方式,在后续安全性的部分会提到。
这里介绍一下如何用docker安装redis(安装docker、docker-compose请参考 Docker入门(一) 和 Docker入门(二))。在linux上使用如下命令(windows上也是一样的步骤,只是路径改一下):
mkdir -p /docker/redis/data/redis
vim /docker/redis/docker-compose.yml
写入如下内容:
version: "3.9"
services:
redis:
image: redis
ports:
- "56379:6379"
volumes:
- /docker/redis/data/redis:/data
logging:
driver: json-file
restart: always
然后运行:
cd /docker/redis
docker-compose config
docker-compose up -d
然后你可以使用redis客户端尝试连接,比如本文后面将用的python,但是首先需要 pip install redis
。然后写一个py文件:
import redis, pprint
r = redis.StrictRedis(host='127.0.0.1',
port=56379,
decode_responses=True,
db=0)
pprint.pprint(r.info())
运行,如果成功连接的话它将会打印出redis服务器的一些信息。在python中,更好的连接方式是使用连接池:
import redis
pool = redis.ConnectionPool(host='127.0.0.1',
port=56379,
decode_responses=True,
db=0)
r = redis.Redis(connection_pool=pool)
或者你也可以使用redis-cli连接,这是redis服务器端自带的软件。比如你可以进入docker-redis容器:
cd /docker/redis
docker-compose exec -it redis bash
redis-cli -h localhost -p 6379
SAVE
SAVE
将返回一个 OK
,并且在/docker/redis/data下出现一个dump.rdb文件。实际上, SAVE
会把redis在内存中的数据写到磁盘中,这会在后面提及。更常用的测试连接命令为 ping
,服务器会返回一个 PONG
表示服务器已连接。另外, clear
可以清屏, exit
可以断开连接。最后,如果你想在redis中看到中文,使用 redis-cli --raw
来发起连接。除了redis-server、redis-cli,你还能找到redis-benchmark(性能测试工具,使用方法同redis-cli)、redis-check-aof(AOF文件修复)、redis-check-dump(RDB文件检查)。另外,如果想让redis把内存数据写到硬盘并关闭服务器,可使用 redis-cli SHUTDOWN
命令(由于docker-compose.yml里写了 restart: always
,所以它还会重启)。
最后再提醒一下,目前的redis安装方式,效率极高,但安全性几乎没有,不要把它开放到外网了。后续将介绍如何通过一些方法提升redis的安全性。
安装好redis后,使用 select 数据库编号
来选择一个数据库。默认情况下redis有16个database,从0~15编号,不支持自定义数据库名(你需要自己记住每个库里存了什么),不支持用户对单独一个数据库有权限。
2.redis的数据结构
正如前面提到过,redis中有5种数据结构:string字符串,list列表,hash散列,set集合,zset有序集合,接下来将一一介绍它们。下面介绍的操作是一小部分,更多操作参考 redis官网 。
(1)string
首先是字符串,它是redis最基本的类型,一般的格式就是一个键对应一个字符串。它还可以存储二进制数据,比如图片等,但它最大只能存储512MB(毕竟redis跑在内存里)。你可以在redis-cli尝试下面3种命令(set,get,del):
set test 123
OK
get test
"123"
del test
(integer) 1
get test
(nil)
del test
(integer) 0
set用于给键赋值,get用于取出键对应的值,del用于删除一个键。在上面的例子中,如果你在python中运行,代码如下:
import redis
r = redis.StrictRedis(host='localhost',
port=536379,
decode_responses=True,
db=0)
a = []
a.append(r.set('test', '123'))
a.append(r.get('test'))
a.append(r.delete('test'))
a.append(r.get('test'))
a.append(r.delete('test'))
for i in a:
print(i)
这里把每个返回的值都打印了出来,注意观察对应关系。
另外,如果字符串是纯数字的话,redis提供了 INCR
,用于让其自增1,这个函数返回自增后的数字(如果键不存在,会自动创建一个为 "0" 的值并自增为1)。常见的例子是用于存储阅读量,比如键为 page.view ,在里面存储阅读量。注意在redis键的命名中,要力求写详细、直观,一般用 : 或 . 来分隔。在mysql中对列可用指定 AUTO_INCREMENT
来让列值自增,在redis中则可以用 INCR
达到类似的效果。 INCRBY
可达到相同的效果,但是它可以指定增加多少。同系列的命令还有 DECR
、 DECRBY
(decrby每太大用,因为incrby可以设置成负值), INCRBYFLOAT
(增加浮点数)。
其他关于字符串的命令还有:APPEND
,用于向字符串的末尾追加字符串。STRLEN
,获取字符串长度。注意UTF-8编码中一个中文字符的长度为3。MSET
和 MGET
同时设置、获取多个键值。
位操作,包括 GETBIT
(获得字符串的一个bit位,超出范围时返回0)、 SETBIT
(返回旧值)、 BITCOUNT
(返回为1的bit个数)、 BITOP
(进行位运算,包括 AND 、 OR 、XOR 、 NOT)。 使用位操作,可以让存储空间非常紧凑。
最后,下面介绍的其他类型都是字符串衍生出来的,它们的键值只能是字符串,不能嵌套。
(2)list
下面介绍一下列表。列表是有序的,可以存储多个字符串,在redis-cli中它支持的方法有:
lpush 在列表头部添加一个元素
rpush 在列表末尾添加一个元素
lrange 获取从头到尾、一定范围的元素(包含两端;支持负索引)
ltrim 用法和lrange一样,但是它将会删除范围外的元素
lindex 获取从头到尾的第几个元素。支持负索引
lset 给出索引并赋值指定元素
lpop 从列表头部弹出一个或几个元素(弹出,意为返回这个元素的值,并且从列表中删除它)
rpop 从列表末尾弹出一个或几个元素
llen 获取列表中元素个数
lrem 删除列表中的几个元素,这些元素的值需要指定。格式为 LREM key count value,count指定要删除几个,当count为正时从左边开始删,为负时从右边开始删,0时删除所有
linsert 在一个元素的前或后插入元素
rpoplpush 将旧列表转移到新列表
brpop 类似rpop,区别在于列表内无元素时会被阻塞(每当列表加入一个新元素时brpop就会返回这个元素)。常用于实现任务队列(可带优先级)
如果熟悉数据结构的话,会发现这里的列表可以当作栈或队列使用。
在redis-cli中实验如下:
lpush test 123
(integer) 1
lpush test 234
(integer) 2
lpush test 345
(integer) 3
rpush test 456
(integer) 4
lrange test 0 -1
1) "345"
2) "234"
3) "123"
4) "456"
lindex test 0
"345"
lpop test
"345"
rpop test 2
1) "456"
2) "123"
lrange test 0 -1
1) "234"
del test
(integer) 1
除了这些命令,redis对列表还有其他操作,将在后面介绍。另外,一个列表最多可存储 2^32-1 个元素,即 4,294,967,295 。
(3)set
列表中可以出现相同的元素,且有序,但集合中每个元素不能相同,且set是无序的(set里的元素只有键没有值)。它支持的命令有:
sadd 添加一个元素到集合
srem 从集合中移除一个元素
sismember 检查元素是否在集合中
smembers 返回集合的所有元素。这个命令在集合较大时运行很慢
sdiff 差集
sinter 交集
sunion 并集
sdiffstore 存储运算结果
sinterstore 同上
sunionstore 同上
scard 获得集合中元素个数
srandmember 随机获取一个元素。它可以添加count参数,当count为正时取出count个不重复元素,为负时表示可以重复。但是由于redis的底层机制,某些元素出现的概率可能更大
spop 随机弹出一个或多个元素
使用例:
sadd test 123
(integer) 1
sadd test 234
(integer) 1
sadd test 234 #添加重复元素时被忽略了
(integer) 0
sadd test 345
(integer) 1
sadd test 456
(integer) 1
srem test 123
(integer) 1
sismember test "123"
(integer) 0
sismember test "234"
(integer) 1
smembers test
1) "234"
2) "345"
3) "456"
del test
(integer) 1
redis中的集合使用哈希表实现(只有键的hash),它增、删、查元素的复杂度都是O(1)。除了这些命令,redis对集合还有其他操作,将在后面介绍。另外,一个集合最多也可存储 2^32-1 个元素,即 4,294,967,295 。
在一般的实践中,常用set来存储tag,或者用列表。比如,键名为 tag:标签名:posts ,值为set,存储带有该标签的文章id
(4)hash
散列是存储键值对的集合,它存储的值可以为字符串或数字(不能嵌套字符串以外的类型)。hash的命令有:
hset 在hash中添加或修改一个键值对
hget 获取hash中指定键的值
hmset 添加或修改多个键值对
hmget 获取多个键值
hsetnx 仅当键不存在时新建一个键
hincrby 类似字符串的 incrby 操作
hgetall 获取hash中所有键值对
hexists 判断键是否存在
hdel 如果hash中存在一个或多个键,删除它
hkeys 获得所有键名
hvals 获得所有值
hlen 获得键值对数量
使用例:
hset test 1 111
(integer) 1
hset test 2 222
(integer) 1
hset test 3 333
(integer) 1
hset test 1 123
(integer) 0
hget test 1
"123"
hgetall test
1) "1"
2) "123"
3) "2"
4) "222"
5) "3"
6) "333"
hdel test 1
(integer) 1
hget test 1
(nil)
del test
(integer) 1
一个hash可以看作mysql中的一行,不同之处在于每行包含的列可以不同。hash比较适合存储对象;它同样能存储 2^32-1 个键值对,即 4,294,967,295 。
现在可用比较一下string和hash。比如你想存储一篇文章,需要存储的信息有文章名,文章url,文章内容等。那么你此时有两种选择,一是把文章对象序列化、转换为二进制数据,用string存储;二是用hash,把它当成mysql中的一行。用hash显得更直观,也不需要在读写是进行编解码。
(5)zset
有序集合,它可以看作是介于列表和集合之间,因为zset中的元素是唯一的,但它里面的元素有序;但它又像散列,存储键值对(但是zset中的键称为member,值称为score,值必须为浮点数)。zset支持的操作如下:
zadd 添加键值对到zset( +inf 和 -inf 表示正无穷和负无穷)
zrange 取出范围内的成员值(键值),按score从小到大
zrevrange 同上,但是score从大到小
zrangebyscore 给定score范围,取出所有在范围内的元素(包含极值。如果不想包含,可加上 ( 如 "80 (100")
zrem 如果zset中存在一个或多个成员,删除它
zscore 获取元素的分数
zincrby 增加score
zcard 获取元素个数
zcount 获取分数在范围之间的元素个数
zremrangebyrank 按照元素score从小到大的顺序删除范围内的元素
zremrangebyscore 删除指定分数范围的元素
zrank 获取元素的排名,从小到大排列
zrevrank 同上,从大到小
zinsertstore 计算并导出有序集合的交集。新zset中的分数会使用聚合函数,支持sum、min、max。还能设置权重
zunionstore 计算并导出并集
使用例:
zadd test 123 1
(integer) 1
zadd test 234 2
(integer) 1
zadd test 345 3
(integer) 1
zrange test 0 -1
1) "1"
2) "2"
3) "3"
zrangebyscore test 124 345
1) "2"
2) "3"
zrem test 1
(integer) 1
del test
(integer) 1
它也支持 2^32-1 个键值对。
在应用场景上,一般string用于存储文件、文字;hash用于存储对象(描述对象的各种属性),可以当成mysql中的一行来使用;list用于列表、队列;set可用于去重;zset用于排行、权重等。
(6)HLL
在2.8.9版本中,redis添加了HyperLogLog,这种结构专用于数据量很大的集合,在一定误差内返回集合中元素的个数(不能返回元素本身),它主要用于计算视频点击量(上千万、上亿次)等。它的特点有:占用空间小,一个HLL键在redis中只占12KB,就能统计2^64量级的元素;使用了一些概率算法,标准差在 0.81% 左右(可以设置辅助因子降低,但要付出空间)。
redis中HLL相关命令有:
pfadd 添加元素到HLL中
pfcount 返回个数估算值
pfmerge 将多个HLL合并为一个HLL
3.redis的一些命令
除了前面提到的 ping
, exit
, clear
, save
, shutdown
, select
,数据类型相关的命令,还有一些命令:keys 模式
,返回所有匹配模式的键。在模式中, ?
匹配一个字符, *
匹配0到任意个字符, []
用于给出单个字符的范围, \
用于转义。DEL
,删除一个或多个键。不能用通配符,但是可在linux命令行借助管道与xargs实现类似的功能: redis-cli KEYS "模式" | xargs redis-cli DEL
。DUMP
,把一个键序列化,类似python中decode的功能。EXISTS
,检查键是否存在。EXPIRE
,为键设定过期时间(单位为秒)。到过期时间后,键会被删除。EXPIREAT key timestamp
,和 EXPIRE
功能相同,但是使用UNIX时间戳(从1970年1月1日到现在的秒数)。PEXPIRE
,和 EXPIRE
功能相同,只是单位为毫秒。PERSIST
,取消键的过期时间。TTL
(time to live),以秒为单位返回键的剩余时间。PTTL
,以毫秒为单位返回键的剩余时间。KEYS
,给出一个pattern,返回符合模式的键。MOVE
,把键转移到另一个库中(redis中默认有16个库)。RANDOMKEY
,返回一个随机键。RENAME
,修改键名。RENAMENX
,仅当要改成的键名不存在时,将旧键名改为新键名。SCAN
,返回可迭代的数据库键(数据库编号)。TYPE
,返回键对应值的类型。
更多命令可参考 redis官网 。
4.redis事务
事务(transaction)是现在很流行的一个概念。设想我进行一个转账1000¥的操作,银行的数据库系统需要进行两步:从我的账户扣去1000,然后给目标账户增加1000。但是如果中间出了差错,比如我的账户扣钱、对方账户却没有收到钱,这怎么办呢?这种类型的问题可用事务解决,包括在事务内的多条命令要么全部执行,要么全部都不执行,不存在执行一半的状态。
在redis-cli中执行事务如下:
multi
命令1
命令2
...
exec
redis先接受各条命令,当接收到exec时redis会将所有命令执行。如果在客户端输入命令时断线,redis就不会执行命令。另外,事务不会被其他命令打断,redis在执行一个事务时会把它执行完,再执行其他命令或事务;redis中的事务也不支持回滚。另外,如果事务中的某条命令有错误,其他命令也会继续执行。
事务相关的命令除了 multi
和 exec
,还有:
discard 取消事务
watch 监视键
unwatch 放弃监视键
watch
是事务的重要功能之一,它用于监视键,保证在事务执行前相关的一些键不被改动,一般格式为:
watch
multi
exec
如果watch监视到了键被改动,事务的执行将被阻止(watch的作用不是阻止键被改动,而是阻止事务执行)。
5.TTL(time to live)
这里介绍一下redis的键过期功能。
使用 expire
可以为一个键设置过期时间,单位为秒,到时间后该键会自动被删除。
使用 ttl
则可以返回一个键剩余的时间,单位为秒(当键不存在时返回-2,键没有设置过期时间时返回-1)。
使用 persist
可以取消键的过期时间,成功取消时返回1,否则返回0。另外,如果用 set
、 getset
等命令为键赋值,也会取消过期时间(incr
、 lpush
、 hset
、 zrem
等修改键值的命令则不会)。pexpire
以毫秒设置ttl。对应的有 pttl
。
最后,还有 expireat
和 pexpireat
,它们给出的是UNIX时间,到该时间后删除键。
另外,如果键是被expire相关命令删除,watch不会认为键被改动过。
使用ttl功能,可以方便地为网页提供cookies等服务。
6.排序
redis中使用 sort
排序(升序),用 sort key desc
降序排序,用 sort key alpha
对字符串排序。
在hash中,使用 sort 键名 by 参考键名*->字段名
可以指定排序的字段,redis会用字段值来替换 *
,by只能出现一次; sort by GET 参考键名*->字段名
可以返回指定的字段,get可以出现多次,用 #
可以代替元素本身的值; sort by get STORE 键名
可以把结果保存到新的键中。
7.发布/订阅
redis中提供了发布、订阅的功能。类似youtube,发布者向频道(channel)中发布一个消息,订阅者会收到这个消息。发布消息的命令为 publish
,订阅频道的命令为 subscribe
。另外还有以下命令:unsubscribe
可以取订一个或所有频道;psubscribe
按一定的模式订阅频道。支持的匹配符有 ? 、 * 等;punsubscribe
按模式退订频道。
在分布式的scrapy中,主机发布待爬url就可以用publish,所有订阅url频道的机器就能收到待爬url。
另外,redis 5.0后新增了stream,它主要用于消息队列。publish的一个特点是不会存储之前发布的消息,就是说新订阅的用户不能接受到订阅之前发布的消息。stream就是为了解决这个问题而出现的,它对历史发布的消息进行了持久化
8.管道
在redis的使用中,上面提到的方法都有返回值。日常使用中,redis-cli发出指令,server收到后执行并返回结果,如果client要执行多条语句,有时只需要最后的值,就可以使用管道。管道应用于多条命令,每条命令的执行不依赖于前面的命令,只需要最后的结果时。管道的具体实现和编程语言有关,比如用python:
import redis
pool = redis.ConnectionPool(host='127.0.0.1',
port=56379,
db=0,
decode_responses=True)
r = redis.Redis(connection_pool=pool)
pipe = r.pipeline()
pipe.set('test', '123')
#pipe的其他操作
pipe.execute()
你会发现这和pymysql差不多。
使用管道,即使在局域网中,也可以大大降低延迟。
9.redis GEO
在redis 3.2版本新增了geo功能,主要用于计算实际的地理位置。这里就不过多介绍了。
10.其他
save
命令可将内存中的数据写入磁盘,默认情况下是dump.rdb文件。恢复数据时将这个文件放到redis安装目录(用 config get dir
查看)即可(docker-redis则是放到容器的/data下)。bgsave
在后台执行save。client
相关操作,包括 client list
(查看所有连接的客户端)、 client setname
(设置当前连接的名称)、 client getname
(获取连接名称)、 client pause
(客户端挂起多少毫秒)、 client kill
关闭客户端连接。
redis支持分区,就是把一个很大的键分成小份,每个redis实例只保存它的一个子集,这个技术稍微有些复杂,不拓展了。
redis命令行使用Lua语言,并且支持编写脚本(如同mysql)。客户端提供一个脚本后,其会永久存储在redis中,便于其他客户端使用。Lua的语法这里也不介绍了。config
系列命令,可获取server的配置。
redis支持主从模式,这主要是因为一台机器的资源有限。当需要扩展时,你可以在一台机器上运行主redis,其他机器上运行从redis;主redis负责写入的操作,从redis会把数据拷贝到本地、负责读取的操作,也称为读写分离。
最后,要提醒一下:
注意redis的安全性;安装redis的时候提到过 make BUILD_TLS=yes
这个选项,但是redis应该尽量追求效率,只在内网运行,尽量不用tls等安全手段,因为会降低redis的速度。
redis内部使用Lua语言,有时你使用爬虫入库的东西可能被当成脚本执行,比如说给你把redis清空( flushall
),这点要注意(反爬手段之一);
如果想更深入学习redis,可以参考 redis的官方文档 或 《redis实战》 等书籍。