# 5.3-非关系型数据库存储

NoSQL，全称 Not Only SQL，意为不仅仅是 SQL，泛指非关系型数据库。NoSQL 是基于键值对的，而且不需要经过 SQL 层的解析，数据之间没有耦合性，性能非常高。

非关系型数据库又可细分如下。

键值存储数据库：代表有 Redis、Voldemort 和 Oracle BDB 等。

列存储数据库：代表有 Cassandra、HBase 和 Riak 等。

文档型数据库：代表有 CouchDB 和 MongoDB 等。

图形数据库：代表有 Neo4J、InfoGrid 和 Infinite Graph 等。

对于爬虫的数据存储来说，一条数据可能存在某些字段提取失败而缺失的情况，而且数据可能随时调整。另外，数据之间还存在嵌套关系。如果使用关系型数据库存储，一是需要提前建表，二是如果存在数据嵌套关系的话，需要进行序列化操作才可以存储，这非常不方便。如果用了非关系型数据库，就可以避免一些麻烦，更简单高效。

本节中，我们主要介绍 MongoDB 和 Redis 的数据存储操作。

## 5.3.1　MongoDB 存储

MongoDB 是由 C++ 语言编写的非关系型数据库，是一个基于分布式文件存储的开源数据库系统，其内容存储形式类似 JSON 对象，它的字段值可以包含其他文档、数组及文档数组，非常灵活。在这一节中，我们就来看看 Python 3 下 MongoDB 的存储操作。

### 1. 准备工作

在开始之前，请确保已经安装好了 MongoDB 并启动了其服务，并且安装好了 Python 的 PyMongo 库。如果没有安装，可以参考第 1 章。

### 2. 连接 MongoDB

连接 MongoDB 时，我们需要使用 PyMongo 库里面的 MongoClient。一般来说，传入 MongoDB 的 IP 及端口即可，其中第一个参数为地址 host，第二个参数为端口 port（如果不给它传递参数，默认是 27017）：

```python
import pymongo
client = pymongo.MongoClient(host='localhost', port=27017)
```

这样就可以创建 MongoDB 的连接对象了。

另外，MongoClient 的第一个参数 host 还可以直接传入 MongoDB 的连接字符串，它以 mongodb 开头，例如：

```python
client = MongoClient('mongodb://localhost:27017/')
```

这也可以达到同样的连接效果。

### 3. 指定数据库

MongoDB 中可以建立多个数据库，接下来我们需要指定操作哪个数据库。这里我们以 test 数据库为例来说明，下一步需要在程序中指定要使用的数据库：

```python
db = client.test
```

这里调用 client 的 test 属性即可返回 test 数据库。当然，我们也可以这样指定：

```python
db = client['test']
```

这两种方式是等价的。

### 4. 指定集合

MongoDB 的每个数据库又包含许多集合（collection），它们类似于关系型数据库中的表。

下一步需要指定要操作的集合，这里指定一个集合名称为 students。与指定数据库类似，指定集合也有两种方式：

```python
collection = db.students
```

```python
collection = db['students']
```

这样我们便声明了一个 Collection 对象。

### 5. 插入数据

接下来，便可以插入数据了。对于 students 这个集合，新建一条学生数据，这条数据以字典形式表示：

```python
student = {
    'id': '20170101',
    'name': 'Jordan',
    'age': 20,
    'gender': 'male'
}
```

这里指定了学生的学号、姓名、年龄和性别。接下来，直接调用 collection 的 insert 方法即可插入数据，代码如下：

```python
result = collection.insert(student)
print(result)
```

在 MongoDB 中，每条数据其实都有一个\_id 属性来唯一标识。如果没有显式指明该属性，MongoDB 会自动产生一个 ObjectId 类型的\_id 属性。insert() 方法会在执行后返回\_id 值。

运行结果如下：

```python
5932a68615c2606814c91f3d
```

当然，我们也可以同时插入多条数据，只需要以列表形式传递即可，示例如下：

```python
student1 = {
    'id': '20170101',
    'name': 'Jordan',
    'age': 20,
    'gender': 'male'
}

student2 = {
    'id': '20170202',
    'name': 'Mike',
    'age': 21,
    'gender': 'male'
}

result = collection.insert([student1, student2])
print(result)
```

返回结果是对应的\_id 的集合：

```python
[ObjectId('5932a80115c2606a59e8a048'), ObjectId('5932a80115c2606a59e8a049')]
```

实际上，在 PyMongo 3.x 版本中，官方已经不推荐使用 insert() 方法了。当然，继续使用也没有什么问题。官方推荐使用 insert\_one() 和 insert\_many() 方法来分别插入单条记录和多条记录，示例如下：

```python
student = {
    'id': '20170101',
    'name': 'Jordan',
    'age': 20,
    'gender': 'male'
}

result = collection.insert_one(student)
print(result)
print(result.inserted_id)
```

运行结果如下：

```python
<pymongo.results.InsertOneResult object at 0x10d68b558>
5932ab0f15c2606f0c1cf6c5
```

与 insert() 方法不同，这次返回的是 InsertOneResult 对象，我们可以调用其 inserted\_id 属性获取\_id。

对于 insert\_many() 方法，我们可以将数据以列表形式传递，示例如下：

```python
student1 = {
    'id': '20170101',
    'name': 'Jordan',
    'age': 20,
    'gender': 'male'
}

student2 = {
    'id': '20170202',
    'name': 'Mike',
    'age': 21,
    'gender': 'male'
}

result = collection.insert_many([student1, student2])
print(result)
print(result.inserted_ids)
```

运行结果如下：

```python
<pymongo.results.InsertManyResult object at 0x101dea558>
[ObjectId('5932abf415c2607083d3b2ac'), ObjectId('5932abf415c2607083d3b2ad')]
```

该方法返回的类型是 InsertManyResult，调用 inserted\_ids 属性可以获取插入数据的\_id 列表。

### 6. 查询

插入数据后，我们可以利用 find\_one() 或 find() 方法进行查询，其中 find\_one() 查询得到的是单个结果，find() 则返回一个生成器对象。示例如下：

```python
result = collection.find_one({'name': 'Mike'})
print(type(result))
print(result)
```

这里我们查询 name 为 Mike 的数据，它的返回结果是字典类型，运行结果如下：

```python
<class 'dict'>
{'_id': ObjectId('5932a80115c2606a59e8a049'), 'id': '20170202', 'name': 'Mike', 'age': 21, 'gender': 'male'}
```

可以发现，它多了\_id 属性，这就是 MongoDB 在插入过程中自动添加的。

此外，我们也可以根据 ObjectId 来查询，此时需要使用 bson 库里面的 objectid：

```python
from bson.objectid import ObjectId

result = collection.find_one({'_id': ObjectId('593278c115c2602667ec6bae')})
print(result)
```

其查询结果依然是字典类型，具体如下：

```python
{'_id': ObjectId('593278c115c2602667ec6bae'), 'id': '20170101', 'name': 'Jordan', 'age': 20, 'gender': 'male'}
```

当然，如果查询结果不存在，则会返回 None。

对于多条数据的查询，我们可以使用 find() 方法。例如，这里查找年龄为 20 的数据，示例如下：

```python
results = collection.find({'age': 20})
print(results)
for result in results:
    print(result)
```

运行结果如下：

```python
<pymongo.cursor.Cursor object at 0x1032d5128>
{'_id': ObjectId('593278c115c2602667ec6bae'), 'id': '20170101', 'name': 'Jordan', 'age': 20, 'gender': 'male'}
{'_id': ObjectId('593278c815c2602678bb2b8d'), 'id': '20170102', 'name': 'Kevin', 'age': 20, 'gender': 'male'}
{'_id': ObjectId('593278d815c260269d7645a8'), 'id': '20170103', 'name': 'Harden', 'age': 20, 'gender': 'male'}
```

返回结果是 Cursor 类型，它相当于一个生成器，我们需要遍历取到所有的结果，其中每个结果都是字典类型。

如果要查询年龄大于 20 的数据，则写法如下：

```python
results = collection.find({'age': {'$gt': 20}})
```

这里查询的条件键值已经不是单纯的数字了，而是一个字典，其键名为比较符号 $gt，意思是大于，键值为 20。

这里将比较符号归纳为表 5-3。

表 5-3 比较符号

| 符　　号 | 含　　义  | 示　　例                         |
| ---- | ----- | ---------------------------- |
| $lt  | 小于    | {'age': {'$lt': 20}}         |
| $gt  | 大于    | {'age': {'$gt': 20}}         |
| $lte | 小于等于  | {'age': {'$lte': 20}}        |
| $gte | 大于等于  | {'age': {'$gte': 20}}        |
| $ne  | 不等于   | {'age': {'$ne': 20}}         |
| $in  | 在范围内  | {'age': {'$in': \[20, 23]}}  |
| $nin | 不在范围内 | {'age': {'$nin': \[20, 23]}} |

另外，还可以进行正则匹配查询。例如，查询名字以 M 开头的学生数据，示例如下：

```python
results = collection.find({'name': {'$regex': '^M.*'}})
```

这里使用 $regex 来指定正则匹配，^M.\* 代表以 M 开头的正则表达式。

这里将一些功能符号再归类为表 5-4。

表 5-4 功能符号

| 符　　号    | 含　　义    | 示　　例                                                | 示例含义                   |
| ------- | ------- | --------------------------------------------------- | ---------------------- |
| $regex  | 匹配正则表达式 | {'name': {'$regex': '^M.\*'}}                       | name 以 M 开头            |
| $exists | 属性是否存在  | {'name': {'$exists': True}}                         | name 属性存在              |
| $type   | 类型判断    | {'age': {'$type': 'int'}}                           | age 的类型为 int           |
| $mod    | 数字模操作   | {'age': {'$mod': \[5, 0]}}                          | 年龄模 5 余 0              |
| $text   | 文本查询    | {'$text': {'$search': 'Mike'}}                      | text 类型的属性中包含 Mike 字符串 |
| $where  | 高级条件查询  | {'$where': 'obj.fans\_count == obj.follows\_count'} | 自身粉丝数等于关注数             |

关于这些操作的更详细用法，可以在 MongoDB 官方文档找到： <https://docs.mongodb.com/manual/reference/operator/query/>。

### 7. 计数

要统计查询结果有多少条数据，可以调用 count() 方法。比如，统计所有数据条数：

```python
count = collection.find().count()
print(count)
```

或者统计符合某个条件的数据：

```python
count = collection.find({'age': 20}).count()
print(count)
```

运行结果是一个数值，即符合条件的数据条数。

### 8. 排序

排序时，直接调用 sort() 方法，并在其中传入排序的字段及升降序标志即可。示例如下：

```python
results = collection.find().sort('name', pymongo.ASCENDING)
print([result['name'] for result in results])
```

运行结果如下：

```python
['Harden', 'Jordan', 'Kevin', 'Mark', 'Mike']
```

这里我们调用 pymongo.ASCENDING 指定升序。如果要降序排列，可以传入 pymongo.DESCENDING。

### 9. 偏移

在某些情况下，我们可能想只取某几个元素，这时可以利用 skip() 方法偏移几个位置，比如偏移 2，就忽略前两个元素，得到第三个及以后的元素：

```python
results = collection.find().sort('name', pymongo.ASCENDING).skip(2)
print([result['name'] for result in results])
```

运行结果如下：

```python
['Kevin', 'Mark', 'Mike']
```

另外，还可以用 limit() 方法指定要取的结果个数，示例如下：

```python
results = collection.find().sort('name', pymongo.ASCENDING).skip(2).limit(2)
print([result['name'] for result in results])
```

运行结果如下：

```python
['Kevin', 'Mark']
```

如果不使用 limit() 方法，原本会返回三个结果，加了限制后，会截取两个结果返回。

值得注意的是，在数据库数量非常庞大的时候，如千万、亿级别，最好不要使用大的偏移量来查询数据，因为这样很可能导致内存溢出。此时可以使用类似如下操作来查询：

```python
from bson.objectid import ObjectId
collection.find({'_id': {'$gt': ObjectId('593278c815c2602678bb2b8d')}})
```

这时需要记录好上次查询的\_id。

### 10. 更新

对于数据更新，我们可以使用 update() 方法，指定更新的条件和更新后的数据即可。例如：

```python
condition = {'name': 'Kevin'}
student = collection.find_one(condition)
student['age'] = 25
result = collection.update(condition, student)
print(result)
```

这里我们要更新 name 为 Kevin 的数据的年龄：首先指定查询条件，然后将数据查询出来，修改年龄后调用 update() 方法将原条件和修改后的数据传入。

运行结果如下：

```python
{'ok': 1, 'nModified': 1, 'n': 1, 'updatedExisting': True}
```

返回结果是字典形式，ok 代表执行成功，nModified 代表影响的数据条数。

另外，我们也可以使用 $set 操作符对数据进行更新，代码如下：

```python
result = collection.update(condition, {'$set': student})
```

这样可以只更新 student 字典内存在的字段。如果原先还有其他字段，则不会更新，也不会删除。而如果不用 $set 的话，则会把之前的数据全部用 student 字典替换；如果原本存在其他字段，则会被删除。

另外，update() 方法其实也是官方不推荐使用的方法。这里也分为 update\_one() 方法和 update\_many() 方法，用法更加严格，它们的第二个参数需要使用 $ 类型操作符作为字典的键名，示例如下：

```python
condition = {'name': 'Kevin'}
student = collection.find_one(condition)
student['age'] = 26
result = collection.update_one(condition, {'$set': student})
print(result)
print(result.matched_count, result.modified_count)
```

这里调用了 update\_one() 方法，第二个参数不能再直接传入修改后的字典，而是需要使用 {'$set': student} 这样的形式，其返回结果是 UpdateResult 类型。然后分别调用 matched\_count 和 modified\_count 属性，可以获得匹配的数据条数和影响的数据条数。

运行结果如下：

```python
<pymongo.results.UpdateResult object at 0x10d17b678>
1 0
```

我们再看一个例子：

```python
condition = {'age': {'$gt': 20}}
result = collection.update_one(condition, {'$inc': {'age': 1}})
print(result)
print(result.matched_count, result.modified_count)
```

这里指定查询条件为年龄大于 20，然后更新条件为 {'$inc': {'age': 1}}，也就是年龄加 1，执行之后会将第一条符合条件的数据年龄加 1。

运行结果如下：

```python
<pymongo.results.UpdateResult object at 0x10b8874c8>
1 1
```

可以看到匹配条数为 1 条，影响条数也为 1 条。

如果调用 update\_many() 方法，则会将所有符合条件的数据都更新，示例如下：

```python
condition = {'age': {'$gt': 20}}
result = collection.update_many(condition, {'$inc': {'age': 1}})
print(result)
print(result.matched_count, result.modified_count)
```

这时匹配条数就不再为 1 条了，运行结果如下：

```python
<pymongo.results.UpdateResult object at 0x10c6384c8>
3 3
```

可以看到，这时所有匹配到的数据都会被更新。

### 11. 删除

删除操作比较简单，直接调用 remove() 方法指定删除的条件即可，此时符合条件的所有数据均会被删除。示例如下：

```python
result = collection.remove({'name': 'Kevin'})
print(result)
```

运行结果如下：

```python
{'ok': 1, 'n': 1}
```

另外，这里依然存在两个新的推荐方法 ——delete\_one() 和 delete\_many()。示例如下：

```python
result = collection.delete_one({'name': 'Kevin'})
print(result)
print(result.deleted_count)
result = collection.delete_many({'age': {'$lt': 25}})
print(result.deleted_count)
```

运行结果如下：

```python
<pymongo.results.DeleteResult object at 0x10e6ba4c8>
1
4
```

delete\_one() 即删除第一条符合条件的数据，delete\_many() 即删除所有符合条件的数据。它们的返回结果都是 DeleteResult 类型，可以调用 deleted\_count 属性获取删除的数据条数。

### 12. 其他操作

另外，PyMongo 还提供了一些组合方法，如 find\_one\_and\_delete()、find\_one\_and\_replace() 和 find\_one\_and\_update()，它们是查找后删除、替换和更新操作，其用法与上述方法基本一致。

另外，还可以对索引进行操作，相关方法有 create\_index()、create\_indexes() 和 drop\_index() 等。

关于 PyMongo 的详细用法，可以参见官方文档：<http://api.mongodb.com/python/current/api/pymongo/collection.html>。

另外，还有对数据库和集合本身等的一些操作，这里不再一一讲解，可以参见官方文档：<http://api.mongodb.com/python/current/api/pymongo/>。

本节讲解了使用 PyMongo 操作 MongoDB 进行数据增删改查的方法，后面我们会在实战案例中应用这些操作进行数据存储。

## 5.3.2　Redis 存储

Redis 是一个基于内存的高效的键值型非关系型数据库，存取效率极高，而且支持多种存储数据结构，使用也非常简单。本节中，我们就来介绍一下 Python 的 Redis 操作，主要介绍 redis-py 这个库的用法。

### 1. 准备工作

在开始之前，请确保已经安装好了 Redis 及 redis-py 库。如果要做数据导入 / 导出操作的话，还需要安装 RedisDump。如果没有安装，可以参考第 1 章。

### 2. Redis 和 StrictRedis

redis-py 库提供两个类 Redis 和 StrictRedis 来实现 Redis 的命令操作。

StrictRedis 实现了绝大部分官方的命令，参数也一一对应，比如 set 方法就对应 Redis 命令的 set 方法。而 Redis 是 StrictRedis 的子类，它的主要功能是用于向后兼容旧版本库里的几个方法。为了做兼容，它将方法做了改写，比如 lrem 方法就将 value 和 num 参数的位置互换，这和 Redis 命令行的命令参数不一致。

官方推荐使用 StrictRedis，所以本节中我们也用 StrictRedis 类的相关方法作演示。

### 3. 连接 Redis

现在我们已经在本地安装了 Redis 并运行在 6379 端口，密码设置为 foobared。那么，可以用如下示例连接 Redis 并测试：

```python
from redis import StrictRedis  

redis = StrictRedis(host='localhost', port=6379, db=0, password='foobared')  
redis.set('name', 'Bob')  
print(redis.get('name'))
```

这里我们传入了 Redis 的地址、运行端口、使用的数据库和密码信息。在默认不传的情况下，这 4 个参数分别为 localhost、6379、0 和 None。首先声明了一个 StrictRedis 对象，接下来调用 set() 方法，设置一个键值对，然后将其获取并打印。

运行结果如下：

```
b'Bob'
```

这说明我们连接成功，并可以执行 set 和 get 操作了。

当然，我们还可以使用 ConnectionPool 来连接，示例如下：

```python
from redis import StrictRedis, ConnectionPool  

pool = ConnectionPool(host='localhost', port=6379, db=0, password='foobared')  
redis = StrictRedis(connection_pool=pool)
```

这样的连接效果是一样的。观察源码可以发现，StrictRedis 内其实就是用 host 和 port 等参数又构造了一个 ConnectionPool，所以直接将 ConnectionPool 当作参数传给 StrictRedis 也一样。

另外，ConnectionPool 还支持通过 URL 来构建。URL 的格式支持有如下 3 种：

```
redis://[:password]@host:port/db  
rediss://[:password]@host:port/db  
unix://[:password]@/path/to/socket.sock?db=db
```

这 3 种 URL 分别表示创建 Redis TCP 连接、Redis TCP+SSL 连接、Redis UNIX socket 连接。我们只需要构造上面任意一种 URL 即可，其中 password 部分如果有则可以写，没有则可以省略。下面再用 URL 连接演示一下：

```
url = 'redis://:foobared@localhost:6379/0'  
pool = ConnectionPool.from_url(url)  
redis = StrictRedis(connection_pool=pool)
```

这里我们使用第一种连接字符串进行连接。首先，声明一个 Redis 连接字符串，然后调用 from\_url() 方法创建 ConnectionPool，接着将其传给 StrictRedis 即可完成连接，所以使用 URL 的连接方式还是比较方便的。

### 4. 键操作

表 5-5 总结了键的一些判断和操作方法。

表 5-5 键的一些判断和操作方法

| 方　　法               | 作　　用                     | 参数说明             | 示　　例                             | 示例说明                 | 示例结果       |
| ------------------ | ------------------------ | ---------------- | -------------------------------- | -------------------- | ---------- |
| exists(name)       | 判断一个键是否存在                | name：键名          | redis.exists('name')             | 是否存在 name 这个键        | True       |
| delete(name)       | 删除一个键                    | name：键名          | redis.delete('name')             | 删除 name 这个键          | 1          |
| type(name)         | 判断键类型                    | name：键名          | redis.type('name')               | 判断 name 这个键类型        | b'string'  |
| keys(pattern)      | 获取所有符合规则的键               | pattern：匹配规则     | redis.keys('n\*')                | 获取所有以 n 开头的键         | \[b'name'] |
| randomkey()        | 获取随机的一个键                 |                  | randomkey()                      | 获取随机的一个键             | b'name'    |
| rename(src, dst)   | 重命名键                     | src：原键名；dst：新键名  | redis.rename('name', 'nickname') | 将 name 重命名为 nickname | True       |
| dbsize()           | 获取当前数据库中键的数目             |                  | dbsize()                         | 获取当前数据库中键的数目         | 100        |
| expire(name, time) | 设定键的过期时间，单位为秒            | name：键名；time：秒数  | redis.expire('name', 2)          | 将 name 键的过期时间设置为 2 秒 | True       |
| ttl(name)          | 获取键的过期时间，单位为秒，1 表示永久不过期 | name：键名          | redis.ttl('name')                | 获取 name 这个键的过期时间     | 1         |
| move(name, db)     | 将键移动到其他数据库               | name：键名；db：数据库代号 | move('name', 2)                  | 将 name 移动到 2 号数据库    | True       |
| flushdb()          | 删除当前选择数据库中的所有键           |                  | flushdb()                        | 删除当前选择数据库中的所有键       | True       |
| flushall()         | 删除所有数据库中的所有键             |                  | flushall()                       | 删除所有数据库中的所有键         | True       |

### 5. 字符串操作

Redis 支持最基本的键值对形式存储，用法总结如表 5-6 所示。

表 5-6 键值对形式存储

| 方　　法                          | 作　　用                                                      | 参数说明                                      | 示　　例                                                           | 示例说明                                        | 示例结果                         |
| ----------------------------- | --------------------------------------------------------- | ----------------------------------------- | -------------------------------------------------------------- | ------------------------------------------- | ---------------------------- |
| set(name, value)              | 给数据库中键名为 name 的 string 赋予值 value                          | n ame：键名；value：值                          | redis.set('name', 'Bob')                                       | 给 name 这个键的 value 赋值为 Bob                   | True                         |
| get(name)                     | 返回数据库中键名为 name 的 string 的 value                           | name：键名                                   | redis.get('name')                                              | 返回 name 这个键的 value                          | b'Bob'                       |
| getset(name, value)           | 给数据库中键名为 name 的 string 赋予值 value 并返回上次的 value             | name：键名；value：新值                          | redis.getset('name', 'Mike')                                   | 赋值 name 为 Mike 并得到上次的 value                 | b'Bob'                       |
| mget(keys, \*args)            | 返回多个键对应的 value 组成的列表                                      | keys：键名序列                                 | redis.mget(\['name', 'nickname'])                              | 返回 name 和 nickname 的 value                  | \[b'Mike', b'Miker']         |
| setnx(name, value)            | 如果不存在这个键值对，则更新 value，否则不变                                 | name：键名                                   | redis.setnx('newname', 'James')                                | 如果 newname 这个键不存在，则设置值为 James               | 第一次运行结果是 True，第二次运行结果是 False |
| setex(name, time, value)      | 设置可以对应的值为 string 类型的 value，并指定此键值对应的有效期                   | n ame：键名；time：有效期；value：值                 | redis.setex('name', 1, 'James')                                | 将 name 这个键的值设为 James，有效期为 1 秒               | True                         |
| setrange(name, offset, value) | 设置指定键的 value 值的子字符串                                       | name：键名；offset：偏移量；value：值                | redis.set('name', 'Hello') redis.setrange ('name', 6, 'World') | 设置 name 为 Hello 字符串，并在 index 为 6 的位置补 World | 11，修改后的字符串长度                 |
| mset(mapping)                 | 批量赋值                                                      | mapping：字典或关键字参数                          | redis.mset({'name1': 'Durant', 'name2': 'James'})              | 将 name1 设为 Durant，name2 设为 James            | True                         |
| msetnx(mapping)               | 键均不存在时才批量赋值                                               | mapping：字典或关键字参数                          | redis.msetnx({'name3': 'Smith', 'name4': 'Curry'})             | 在 name3 和 name4 均不存在的情况下才设置二者值              | True                         |
| incr(name, amount=1)          | 键名为 name 的 value 增值操作，默认为 1，键不存在则被创建并设为 amount            | name：键名；amount：增长的值                       | redis.incr('age', 1)                                           | age 对应的值增 1，若不存在，则会创建并设置为 1                 | 1，即修改后的值                     |
| decr(name, amount=1)          | 键名为 name 的 value 减值操作，默认为 1，键不存在则被创建并将 value 设置为 - amount | name：键名；amount：减少的值                       | redis.decr('age', 1)                                           | age 对应的值减 1，若不存在，则会创建并设置为1                 | 1，即修改后的值                    |
| append(key, value)            | 键名为 key 的 string 的值附加 value                               | key：键名                                    | redis.append('nickname', 'OK')                                 | 向键名为 nickname 的值后追加 OK                      | 13，即修改后的字符串长度                |
| substr(name, start, end=-1)   | 返回键名为 name 的 string 的子字符串                                 | name：键名；start：起始索引；end：终止索引，默认为1，表示截取到末尾 | redis.substr('name', 1, 4)                                     | 返回键名为 name 的值的字符串，截取索引为 1\~4 的字符            | b'ello'                      |
| getrange(key, start, end)     | 获取键的 value 值从 start 到 end 的子字符串                           | key：键名；start：起始索引；end：终止索引                | redis.getrange('name', 1, 4)                                   | 返回键名为 name 的值的字符串，截取索引为 1\~4 的字符            | b'ello'                      |

### 6. 列表操作

Redis 还提供了列表存储，列表内的元素可以重复，而且可以从两端存储，用法如表 5-7 所示。

表 5-7 列表操作

| 方　　法                     | 作　　用                                         | 参数说明                             | 示　　例                             | 示例说明                                         | 示例结果                |
| ------------------------ | -------------------------------------------- | -------------------------------- | -------------------------------- | -------------------------------------------- | ------------------- |
| rpush(name, \*values)    | 在键名为 name 的列表末尾添加值为 value 的元素，可以传多个          | name：键名；values：值                 | redis.rpush('list', 1, 2, 3)     | 向键名为 list 的列表尾添加 1、2、3                       | 3，列表大小              |
| lpush(name, \*values)    | 在键名为 name 的列表头添加值为 value 的元素，可以传多个           | name：键名；values：值                 | redis.lpush('list', 0)           | 向键名为 list 的列表头部添加 0                          | 4，列表大小              |
| llen(name)               | 返回键名为 name 的列表的长度                            | name：键名                          | redis.llen('list')               | 返回键名为 list 的列表的长度                            | 4                   |
| lrange(name, start, end) | 返回键名为 name 的列表中 start 至 end 之间的元素            | name：键名；start：起始索引；end：终止索引      | redis.lrange('list', 1, 3)       | 返回起始索引为 1 终止索引为 3 的索引范围对应的列表                 | \[b'3', b'2', b'1'] |
| ltrim(name, start, end)  | 截取键名为 name 的列表，保留索引为 start 到 end 的内容         | name：键名；start：起始索引；end：终止索引      | ltrim('list', 1, 3)              | 保留键名为 list 的索引为 1 到 3 的元素                    | True                |
| lindex(name, index)      | 返回键名为 name 的列表中 index 位置的元素                  | name：键名；index：索引                 | redis.lindex('list', 1)          | 返回键名为 list 的列表索引为 1 的元素                      | b'2'                |
| lset(name, index, value) | 给键名为 name 的列表中 index 位置的元素赋值，越界则报错           | name：键名；index：索引位置；value：值       | redis.lset('list', 1, 5)         | 将键名为 list 的列表中索引为 1 的位置赋值为 5                 | True                |
| lrem(name, count, value) | 删除 count 个键的列表中值为 value 的元素                  | name：键名；count：删除个数；value：值       | redis.lrem('list', 2, 3)         | 将键名为 list 的列表删除两个 3                          | 1，即删除的个数            |
| lpop(name)               | 返回并删除键名为 name 的列表中的首元素                       | name：键名                          | redis.lpop('list')               | 返回并删除名为 list 的列表中的第一个元素                      | b'5'                |
| rpop(name)               | 返回并删除键名为 name 的列表中的尾元素                       | name：键名                          | redis.rpop('list')               | 返回并删除名为 list 的列表中的最后一个元素                     | b'2'                |
| blpop(keys, timeout=0)   | 返回并删除名称在 keys 中的 list 中的首个元素，如果列表为空，则会一直阻塞等待 | keys：键名序列；timeout：超时等待时间，0 为一直等待 | redis.blpop('list')              | 返回并删除键名为 list 的列表中的第一个元素                     | \[b'5']             |
| brpop(keys, timeout=0)   | 返回并删除键名为 name 的列表中的尾元素，如果 list 为空，则会一直阻塞等待   | keys：键名序列；timeout：超时等待时间，0 为一直等待 | redis.brpop('list')              | 返回并删除名为 list 的列表中的最后一个元素                     | \[b'2']             |
| rpoplpush(src, dst)      | 返回并删除名称为 src 的列表的尾元素，并将该元素添加到名称为 dst 的列表头部   | src：源列表的键；dst：目标列表的 key          | redis.rpoplpush('list', 'list2') | 将键名为 list 的列表尾元素删除并将其添加到键名为 list2 的列表头部，然后返回 | b'2'                |

### 7. 集合操作

Redis 还提供了集合存储，集合中的元素都是不重复的，用法如表 5-8 所示。

表 5-8 集合操作

| 方　　法                            | 作　　用                              | 参数说明                       | 示　　例                                             | 示例说明                                         | 示例结果                         |
| ------------------------------- | --------------------------------- | -------------------------- | ------------------------------------------------ | -------------------------------------------- | ---------------------------- |
| sadd(name, \*values)            | 向键名为 name 的集合中添加元素                | name：键名；values：值，可为多个      | redis.sadd('tags', 'Book', 'Tea', 'Coffee')      | 向键名为 tags 的集合中添加 Book、Tea 和 Coffee 这 3 个内容   | 3，即插入的数据个数                   |
| srem(name, \*values)            | 从键名为 name 的集合中删除元素                | name：键名；values：值，可为多个      | redis.srem('tags', 'Book')                       | 从键名为 tags 的集合中删除 Book                        | 1，即删除的数据个数                   |
| spop(name)                      | 随机返回并删除键名为 name 的集合中的一个元素         | name：键名                    | redis.spop('tags')                               | 从键名为 tags 的集合中随机删除并返回该元素                     | b'Tea'                       |
| smove(src, dst, value)          | 从 src 对应的集合中移除元素并将其添加到 dst 对应的集合中 | src：源集合；dst：目标集合；value：元素值 | redis.smove('tags', 'tags2', 'Coffee')           | 从键名为 tags 的集合中删除元素 Coffee 并将其添加到键为 tags2 的集合 | True                         |
| scard(name)                     | 返回键名为 name 的集合的元素个数               | name：键名                    | redis.scard('tags')                              | 获取键名为 tags 的集合中的元素个数                         | 3                            |
| sismember(name, value)          | 测试 member 是否是键名为 name 的集合的元素      | name：键值                    | redis.sismember('tags', 'Book')                  | 判断 Book 是否是键名为 tags 的集合元素                    | True                         |
| sinter(keys, \*args)            | 返回所有给定键的集合的交集                     | keys：键名序列                  | redis.sinter(\['tags', 'tags2'])                 | 返回键名为 tags 的集合和键名为 tags2 的集合的交集              | {b'Coffee'}                  |
| sinterstore(dest, keys, \*args) | 求交集并将交集保存到 dest 的集合               | dest：结果集合；keys：键名序列        | redis.sinterstore ('inttag', \['tags', 'tags2']) | 求键名为 tags 的集合和键名为 tags2 的集合的交集并将其保存为 inttag  | 1                            |
| sunion(keys, \*args)            | 返回所有给定键的集合的并集                     | keys：键名序列                  | redis.sunion(\['tags', 'tags2'])                 | 返回键名为 tags 的集合和键名为 tags2 的集合的并集              | {b'Coffee', b'Book', b'Pen'} |
| sunionstore(dest, keys, \*args) | 求并集并将并集保存到 dest 的集合               | dest：结果集合；keys：键名序列        | redis.sunionstore ('inttag', \['tags', 'tags2']) | 求键名为 tags 的集合和键名为 tags2 的集合的并集并将其保存为 inttag  | 3                            |
| sdiff(keys, \*args)             | 返回所有给定键的集合的差集                     | keys：键名序列                  | redis.sdiff(\['tags', 'tags2'])                  | 返回键名为 tags 的集合和键名为 tags2 的集合的差集              | {b'Book', b'Pen'}            |
| sdiffstore(dest, keys, \*args)  | 求差集并将差集保存到 dest 集合                | dest：结果集合；keys：键名序列        | redis.sdiffstore ('inttag', \['tags', 'tags2'])  | 求键名为 tags 的集合和键名为 tags2 的集合的差集并将其保存为 inttag  | 3                            |
| smembers(name)                  | 返回键名为 name 的集合的所有元素               | name：键名                    | redis.smembers('tags')                           | 返回键名为 tags 的集合的所有元素                          | {b'Pen', b'Book', b'Coffee'} |
| srandmember(name)               | 随机返回键名为 name 的集合中的一个元素，但不删除元素     | name：键值                    | redis.srandmember('tags')                        | 随机返回键名为 tags 的集合中的一个元素                       | Srandmember (name)           |

### 8. 有序集合操作

有序集合比集合多了一个分数字段，利用它可以对集合中的数据进行排序，其用法总结如表 5-9 所示。

表 5-9 有序集合操作

| 方　　法                                                                   | 作　　用                                                                                    | 参数说明                                                                     | 示　　例                                        | 示例说明                                                            | 示例结果                                 |
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------- | --------------------------------------------------------------- | ------------------------------------ |
| zadd(name, *args, \**&#x6B;wargs)                                      | 向键名为 name 的 zset 中添加元素 member，score 用于排序。如果该元素存在，则更新其顺序                                 | name：键名；args：可变参数                                                        | redis.zadd('grade', 100, 'Bob', 98, 'Mike') | 向键名为 grade 的 zset 中添加 Bob（其 score 为 100），并添加 Mike（其 score 为 98） | 2，即添加的元素个数                           |
| zrem(name, \*values)                                                   | 删除键名为 name 的 zset 中的元素                                                                  | name：键名；values：元素                                                        | redis.zrem('grade', 'Mike')                 | 从键名为 grade 的 zset 中删除 Mike                                      | 1，即删除的元素个数                           |
| zincrby(name, value, amount=1)                                         | 如果在键名为 name 的 zset 中已经存在元素 value，则将该元素的 score 增加 amount；否则向该集合中添加该元素，其 score 的值为 amount | name：键名；value：元素；amount：增长的 score 值                                      | redis.zincrby('grade', 'Bob', -2)           | 键名为 grade 的 zset 中 Bob 的 score 减 2                              | 98.0，即修改后的值                          |
| zrank(name, value)                                                     | 返回键名为 name 的 zset 中元素的排名，按 score 从小到大排序，即名次                                             | name：键名；value：元素值                                                        | redis.zrank('grade', 'Amy')                 | 得到键名为 grade 的 zset 中 Amy 的排名                                    | 1                                    |
| zrevrank(name, value)                                                  | 返回键为 name 的 zset 中元素的倒数排名（按 score 从大到小排序），即名次                                           | name：键名；value：元素值                                                        | redis.zrevrank ('grade', 'Amy')             | 得到键名为 grade 的 zset 中 Amy 的倒数排名                                  | 2                                    |
| zrevrange(name, start, end, withscores= False)                         | 返回键名为 name 的 zset（按 score 从大到小排序）中 index 从 start 到 end 的所有元素                            | name：键值；start：开始索引；end：结束索引；withscores：是否带 score                         | redis.zrevrange ('grade', 0, 3)             | 返回键名为 grade 的 zset 中前四名元素                                       | \[b'Bob', b'Mike', b'Amy', b'James'] |
| zrangebyscore (name, min, max, start=None, num=None, withscores=False) | 返回键名为 name 的 zset 中 score 在给定区间的元素                                                      | name：键名；min：最低 score；max：最高 score；start：起始索引；num：个数；withscores：是否带 score | redis.zrangebyscore ('grade', 80, 95)       | 返回键名为 grade 的 zset 中 score 在 80 和 95 之间的元素                      | \[b'Bob', b'Mike', b'Amy', b'James'] |
| zcount(name, min, max)                                                 | 返回键名为 name 的 zset 中 score 在给定区间的数量                                                      | name：键名；min：最低 score；max：最高 score                                        | redis.zcount('grade', 80, 95)               | 返回键名为 grade 的 zset 中 score 在 80 到 95 的元素个数                      | 2                                    |
| zcard(name)                                                            | 返回键名为 name 的 zset 的元素个数                                                                 | name：键名                                                                  | redis.zcard('grade')                        | 获取键名为 grade 的 zset 中元素的个数                                       | 3                                    |
| zremrangebyrank (name, min, max)                                       | 删除键名为 name 的 zset 中排名在给定区间的元素                                                           | name：键名；min：最低位次；max：最高位次                                                | redis.zremrangebyrank ('grade', 0, 0)       | 删除键名为 grade 的 zset 中排名第一的元素                                     | 1，即删除的元素个数                           |
| zremrangebyscore (name, min, max)                                      | 删除键名为 name 的 zset 中 score 在给定区间的元素                                                      | name：键名；min：最低 score；max：最高 score                                        | redis.zremrangebyscore ('grade', 80, 90)    | 删除 score 在 80 到 90 之间的元素                                        | 1，即删除的元素个数                           |

### 9. 散列操作

Redis 还提供了散列表的数据结构，我们可以用 name 指定一个散列表的名称，表内存储了各个键值对，用法总结如表 5-10 所示。

表 5-10 散列操作

| 方　　法                         | 作　　用                           | 参数说明                        | 示　　例                                           | 示例说明                                | 示例结果                                                           |
| ---------------------------- | ------------------------------ | --------------------------- | ---------------------------------------------- | ----------------------------------- | -------------------------------------------------------------- |
| hset(name, key, value)       | 向键名为 name 的散列表中添加映射            | name：键名；key：映射键名；value：映射键值 | hset('price', 'cake', 5)                       | 向键名为 price 的散列表中添加映射关系，cake 的值为 5   | 1，即添加的映射个数                                                     |
| hsetnx(name, key, value)     | 如果映射键名不存在，则向键名为 name 的散列表中添加映射 | name：键名；key：映射键名；value：映射键值 | hsetnx('price', 'book', 6)                     | 向键名为 price 的散列表中添加映射关系，book 的值为 6   | 1，即添加的映射个数                                                     |
| hget(name, key)              | 返回键名为 name 的散列表中 key 对应的值      | name：键名；key：映射键名            | redis.hget('price', 'cake')                    | 获取键名为 price 的散列表中键名为 cake 的值        | 5                                                              |
| hmget(name, keys, \*args)    | 返回键名为 name 的散列表中各个键对应的值        | name：键名；keys：键名序列           | redis.hmget('price', \['apple', 'orange'])     | 获取键名为 price 的散列表中 apple 和 orange 的值 | \[b'3', b'7']                                                  |
| hmset(name, mapping)         | 向键名为 name 的散列表中批量添加映射          | name：键名；mapping：映射字典        | redis.hmset('price', {'banana': 2, 'pear': 6}) | 向键名为 price 的散列表中批量添加映射              | True                                                           |
| hincrby(name, key, amount=1) | 将键名为 name 的散列表中映射的值增加 amount   | name：键名；key：映射键名；amount：增长量 | redis.hincrby('price', 'apple', 3)             | key 为 price 的散列表中 apple 的值增加 3      | 6，修改后的值                                                        |
| hexists(name, key)           | 键名为 name 的散列表中是否存在键名为键的映射      | name：键名；key：映射键名            | redis.hexists('price', 'banana')               | 键名为 price 的散列表中 banana 的值是否存在       | True                                                           |
| hdel(name, \*keys)           | 在键名为 name 的散列表中，删除键名为键的映射      | name：键名；keys：键名序列           | redis.hdel('price', 'banana')                  | 从键名为 price 的散列表中删除键名为 banana 的映射    | True                                                           |
| hlen(name)                   | 从键名为 name 的散列表中获取映射个数          | name：键名                     | redis.hlen('price')                            | 从键名为 price 的散列表中获取映射个数              | 6                                                              |
| hkeys(name)                  | 从键名为 name 的散列表中获取所有映射键名        | name：键名                     | redis.hkeys('price')                           | 从键名为 price 的散列表中获取所有映射键名            | \[b'cake', b'book', b'banana', b'pear']                        |
| hvals(name)                  | 从键名为 name 的散列表中获取所有映射键值        | name：键名                     | redis.hvals('price')                           | 从键名为 price 的散列表中获取所有映射键值            | \[b'5', b'6', b'2', b'6']                                      |
| hgetall(name)                | 从键名为 name 的散列表中获取所有映射键值对       | name：键名                     | redis.hgetall('price')                         | 从键名为 price 的散列表中获取所有映射键值对           | {b'cake': b'5', b'book': b'6', b'orange': b'7', b'pear': b'6'} |

### 10. RedisDump

RedisDump 提供了强大的 Redis 数据的导入和导出功能，现在就来看下它的具体用法。

首先，确保已经安装好了 RedisDump。

RedisDump 提供了两个可执行命令：redis-dump 用于导出数据，redis-load 用于导入数据。

#### redis-dump

首先，可以输入如下命令查看所有可选项：

```
redis-dump -h
```

运行结果如下：

```
Usage: redis-dump [global options] COMMAND [command options]   
    -u, --uri=S                      Redis URI (e.g. redis://hostname[:port])  
    -d, --database=S                 Redis database (e.g. -d 15)  
    -s, --sleep=S                    Sleep for S seconds after dumping (for debugging)  
    -c, --count=S                    Chunk size (default: 10000)  
    -f, --filter=S                   Filter selected keys (passed directly to redis' KEYS command)  
    -O, --without_optimizations      Disable run time optimizations  
    -V, --version                    Display version  
    -D, --debug  
        --nosafe
```

其中 - u 代表 Redis 连接字符串，-d 代表数据库代号，-s 代表导出之后的休眠时间，-c 代表分块大小，默认是 10000，-f 代表导出时的过滤器，-O 代表禁用运行时优化，-V 用于显示版本，-D 表示开启调试。

我们拿本地的 Redis 做测试，运行在 6379 端口上，密码为 foobared，导出命令如下：

```
redis-dump -u :foobared@localhost:6379
```

如果没有密码的话，可以不加密码前缀，命令如下：

```
redis-dump -u localhost:6379
```

运行之后，可以将本地 0 至 15 号数据库的所有数据输出出来，例如：

```
{"db":0,"key":"name2","ttl":-1,"type":"string","value":"Durant","size":6}  
{"db":0,"key":"name3","ttl":-1,"type":"string","value":"Durant","size":6}  
{"db":0,"key":"name4","ttl":-1,"type":"string","value":"HelloWorld","size":10}  
{"db":0,"key":"name5","ttl":-1,"type":"string","value":"James","size":5}  
{"db":0,"key":"name6","ttl":-1,"type":"string","value":"James","size":5}  
{"db":0,"key":"age","ttl":-1,"type":"string","value":"1","size":1}  
{"db":0,"key":"age2","ttl":-1,"type":"string","value":"-5","size":2}
```

每条数据都包含 6 个字段，其中 db 即数据库代号，key 即键名，ttl 即该键值对的有效时间，type 即键值类型，value 即内容，size 即占用空间。

如果想要将其输出为 JSON 行文件，可以使用如下命令：

```
redis-dump -u :foobared@localhost:6379 &gt; ./redis_data.jl
```

这样就可以成功将 Redis 的所有数据库的所有数据导出成 JSON 行文件了。

另外，可以使用 - d 参数指定某个数据库的导出，例如只导出 1 号数据库的内容：

```
redis-dump -u :foobared@localhost:6379 -d 1 &gt; ./redis.data.jl
```

如果只想导出特定的内容，比如想导出以 adsl 开头的数据，可以加入 - f 参数用来过滤，命令如下：

```
redis-dump -u :foobared@localhost:6379 -f adsl:* &gt; ./redis.data.jl
```

其中 - f 参数即 Redis 的 keys 命令的参数，可以写一些过滤规则。

#### redis-load

同样，我们可以首先输入如下命令查看所有可选项：

```
redis-load -h
```

运行结果如下：

```
redis-load --help  
  Try: redis-load [global options] COMMAND [command options]   
    -u, --uri=S                      Redis URI (e.g. redis://hostname[:port])  
    -d, --database=S                 Redis database (e.g. -d 15)  
    -s, --sleep=S                    Sleep for S seconds after dumping (for debugging)  
    -n, --no_check_utf8  
    -V, --version                    Display version  
    -D, --debug  
        --nosafe
```

其中 - u 代表 Redis 连接字符串，-d 代表数据库代号，默认是全部，-s 代表导出之后的休眠时间，-n 代表不检测 UTF-8 编码，-V 表示显示版本，-D 表示开启调试。

我们可以将 JSON 行文件导入到 Redis 数据库中：

```
< redis_data.json redis-load -u :foobared@localhost:6379
```

这样就可以成功将 JSON 行文件导入到数据库中了。

另外，下面的命令同样可以达到同样的效果：

```
cat redis_data.json | redis-load -u :foobared@localhost:6379
```

本节中，我们不仅了解了 redis-py 对 Redis 数据库的一些基本操作，还演示了 RedisDump 对数据的导入导出操作。由于其便捷性和高效性，后面我们会利用 Redis 实现很多架构，如维护代理池、Cookies 池、ADSL 拨号代理池、Scrapy-Redis 分布式架构等，所以 Redis 的操作需要好好掌握。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://python3webspider.cuiqingcai.com/5.3-fei-guan-xi-xing-shu-ju-ku-cun-chu.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
