PyMongo 库使用基础使用速成教程

mongodb symbol.jpeg

为什么要使用 MongoDB 以及 Pymongo

在程序开发实践中,除了学习代码、算法之外,其他开发有用的程序一定离不开数据库。然而,传统的 CS 专业里面教授数据库往往会从 SQL 数据库开始学起,但是,要使用 SQL 数据库,那么第一步一定离不开如何建表,设置字段、设置键的类型等,而作为初学者,加上没有真是的项目背景的情况下,数据库的设计往往只能是那种一个 User 用户表,然后再一个 username 、 passwd、 nickname 之类的字段设计,再在这个‘小朋友’基础版本的数据库上完成一些 CURD 操作后,向再进一步开发一些小 DEMO 时,由于无法预先知道所需要的数据模型到底是什么样的,就会出现不断的改表、改数据库的情况,再面对不熟悉的项目,最后弄得焦头烂额,反倒是打击了学习的兴趣。

再者,在一些个人项目,或者是一些小微创业公司、新孵化的项目,同样存在市场不明确、需求不明确的情况,使用传统的数据库分析、设计流程,一来耗费时间经历,二来辛辛苦苦设计出来的模型可能刚设计出来又不符合需求了。

针对这些情况,使用 NoSQL 数据库,比如 MongoDB,就能很好避免上述的问题。NoSQL 数据库有一个特点,就是并没有表的概念,以 MongoDB 为例,它只有 数据库、集合、文档三种结果,到文档这个维度级别,还可以在其中创建子文档、数组等数据类型。基于这种特性,我们就很容易做到“让产品跑起来,最小可用”的理念。当然,NoSQL 数据库并不仅仅只有 MongoDB 一款,不过由于 MongoDB 安装比较简单,社群完善,网上能找到比较多的教程,因此我们可以考虑使用 MongoDB 进行 NoSQL 数据库的学习及使用。

目前人工智能非常火,在人工智能流行的同时,Python 凭借其“胶水语言”的特点,也越来越受欢迎。使用 Python,我们可以借助庞大的库的支持,很快、很方便就能创造出很多有意思的项目,因此,本文下面都将以 Python 作为开发语言,MongoDB 作为数据库,来学习如何使用 Pymongo 这一个 MongoDB 操作的封装库来实现对 MongoDB 的使用。

本文虽然标题称为教程,但我却并不认为这篇文章能称得上教程,主要有以下两个原因:

  • 人都好为人师而易自满。在软件,或者说可以行业,几乎所有东西都是日新月异,很可能这篇教程发布的时候,这些库或者软件就更新了,或者当中许多方法都变了,按照文章的方法可能就行不通了,这样的教程肯定是没有多大效用的。所以,分享这篇文章,主要是想分享一下从发现一个需求,到寻求答案、解决方案,再到最终能实施落地的一连串思考、思路的分享,这才是值得分享、交流传播的东西
  • 人学习的过程,需要一个科学的步骤来完成。科学表明,看一次,跟着做一次的学习效果,远没有在看完、跟着做完,再呈现出来分享给别人的效果好。再有,把东西分享出去的过程当中,能收获到不少自己没有意识到,遗漏甚至错漏的地方,这对深造非常有帮助。

文章整体脉络
本文参考了 《MongoDB权威指南》以及 MongoDB 的官方文档,Pymongo 的官方文档作为参考。实际中,《MongoDB权威指南》主要以 mongo 终端交互为例进行数据库操作,因此本文借鉴了其中第三章、第四章的部分案例进行演示。而 Pymongo 的官方文档中,有一些方法也并没有提供详细的解释,因此通过推敲摸索以及综合部分Stack Overflow的讨论形成,关于这一部分,比较多的存在于修改器的使用部分,如对游标的 min() 及 max() 的使用等。

本文主要以五大部分来讲解如何利用 Pymongo 操作 MongoDB,具体如下:

  • 环境的搭建
    • Pymongo 的安装
  • Pymongo的基本使用
    • 数据库连接
    • 数据库、集合相关操作
  • 基本的文档 CURD
    • insert_one() insert_many()
    • find() find_one()
    • update()
    • remove()
  • 修改器在更新、查询中的使用
    • $set
    • $unset
    • $inc
    • $push
    • $addToSet
    • $pull
    • $each
    • $pop
    • $lt $lte
    • $gt $gte
    • $ne
    • $in nin
    • $or
    • $exist
    • $all
    • $size
    • $slice
    • min() max()
    • $where
  • 游标的使用
    • sor()
    • limit()
    • skip()

PyMongo 安装

pymongo 可以使用 pip 安装,方法如下:

python -m pip install pymongo

如果是使用 Anaconda 环境的,可以使用 conda install 来安装。方法如下:

conda install pymongo

pymongo check installed.jpeg

安装完成后,可以使用 conda list 命令查看 pymongo 模块是否成功安装,如果成功安装,可以在输出中看到上图结果。

Pymongo 数据库连接,数据库、集合相关操作

本文介绍使用 MongoDB 为 Docker 的镜像,系统为 Mac OS 10.13.4。MongoDB Docker 容器启动后,地址为默认的 localhost:27017

pymongo 连接数据库

我们先引入 pymongoMongoClient 模块。再创建一个连接。

from pymongo import MongoClient

client = MongoClient()

print(client)
>> MongoClient(host=['localhost:27017'], document_class=dict, tz_aware=False, connect=True)

pymongo client 数据库相关操作

pymongo client 数据库的相关操作主要有三种,分别是使用现有数据库,创建不存在的数据库,删除数据库。具体操作如下:

# 列出当前所有数据库名称
database_names = client.database_names()
# 使用现有数据库,假设已存在数据库,数据库名称为 test_database
test_database = client.test_database
# 创建不存在的数据库并使用
new_database = client.new_database
# 删除现有数据库
client.drop_database('new_database')

小结一下 pymongo client 的数据库操作,可以简单的理解为三类常见操作,一类是选择或者创建数据库,都可以直接指定一个数据库名称,如果该数据库存在就使用该数据库,否则将创建一个;第二类为删除数据库操作,可以使用 drop_database() 方法。最后则为查看当前 MongoDB 连接中的所有数据库,可以使用 database_names() 方法。

pymongo 集合相关操作

pymongo 的集合操作和数据库的操作类似,同样是指定一个集合名称,如何集合存在就使用该集合,否则创建一个新的集合。以下以使用 test_database 数据库进行集合操作:

# 查看当前数据库中的所有集合
collection_names = test_database.collection_names()
# 使用一个已存在数集合,假设集合名称为 posts
posts = test_database.posts
# 创建一个新的集合
new_collection = test_database.new_collection
# 删除一个集合
test_database.drop(''new_collection)

这里有一点值得注意,当我们创建一个新集合时,如果我们只是创建集合,但并没有向集合中添加数据,这是新的集合其实并没有创建,可以看下面的例子:

test_database.collection_names()
>> []
posts = test_database.posts
# 创建一个新集合,但未向集合中存放数据
test_database.collection_names()
>> []
# 向集合中存放数据
doc = {'test': 1}
post_doc = posts.insert_one(doc)
test_database.collection_names()
>> ['posts']

向集合中添加文档

pymongo 提供两种添加新文档的方法,分别是 insert_oneinsert_many。其中,insert_one 是将一个文档添加到集合中, insert_many() 是将多个文档一次性添加到集合中。例子如下:

# insert_one() 用法
foo = test_database.foo
foo.insert_one({'_id': 0})
for data in foo.find():
  print(data)
>> {'_id': 0}
# insert_many() 用法
foo.insert_many([{'_id': 1}, {'_id': 2}])
for data in foo.find():
  print(data)
>> {'_id': 0}
>> {'_id': 1}
>> {'_id': 2}

删除集合中的文档

我们可以使用 remove() 方法完成删除集合中的文档。

# 删除指定文档
foo.remove({'_id': 2})
for data in foo.find():
  print(data)
>> {'_id': 0}
>> {'_id': 1}
# 删除所有文档
foo.remove()
foo.count()
>> 0

更新文档数据

我们先在 test_databse 数据库中创建一个 user 数据库,然后在数据库中添加一条用户文档记录,具体操作如下:

user = test_database.user
joe = {'name': 'joe', 'friends': 32, 'enemies': 2}
user.insert_one(joe)
user.find_one()
>> {'_id': ObjectId('5ac8d33a29561f64220f6f9a'),
 'enemies': 2,
 'friends': 32,
 'name': 'joe'}

接下来,我们将对这个文档作出几处修改,修改如下:

  • 将 friedns 和 enemies 移动到 relationships 子文档之下
  • 将字段 name 更改为 username

具体操作如下:

joe = user.find_one({'name': 'joe'})
print(joe)
>> {'_id': ObjectId('5ac8d33a29561f64220f6f9a'),
 'enemies': 2,
 'friends': 32,
 'name': 'joe'}
joe['relationships'] = {'firends': joe['friends'], 'enemies': joe['enemies']}
joe['username'] = joe['name']
del joe['name']
del joe['enemies']
del joe['friends']
print(joe)
>> {'_id': ObjectId('5ac8d33a29561f64220f6f9a'),
 'relationships': {'enemies': 2, 'firends': 32},
 'username': 'joe'}
user.replace_one({'name': 'joe'}, joe)
joe = user.find_one()
>> {'_id': ObjectId('5ac8d33a29561f64220f6f9a'),
 'relationships': {'enemies': 2, 'firends': 32},
 'username': 'joe'}

替换文档我们使用了replace_one() 方法,该方法传入两个参数,第一个参数是需要被替换的文档查找条件,第二个参数是更新的文档数据。

更新操作中的修改器

在实际中,更新文档往往是更新文档的一部分内容,在 MongoDB 中,我们可以使用更新修改器 (update modifier) 来对文档中某些字段进行更新,常用的修改器有以下几个:

  • $set 用来指定一个字段的值,如果不存在,将创建一个新的字段
  • $unset 删除一个字段
  • $inc 用来增加(或减少)一个已有键的值,如果不存在将会创建一个
  • $push 向已有的数组末尾添加一个元素
  • $addToSet 避免插入重复数据
  • $pull 删除元素,基于特定条件
  • $each 遍历列表操作
  • $pop 删除元素

以下将展开演示上述修改器的使用方法。

$set

# 先清除之前存储的 user 集合中的数据
user.drop()
user.count()
>> 0

joe = {'name': 'joe', 
          'age': 30,
          'sex': 'male',
          'location': 'Wisconsin' }
user.insert_one(joe)
print(user.find_one())
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
 'age': 30,
 'location': 'Wisconsin',
 'name': 'joe',
 'sex': 'male'}
# 为用户添加一项 favorite 的字段
user.update_one({'_id': ObjectId('5ac9836829561f64220f6f9d')}, {'$set' : {'favorite': 'War adn Peace'}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
 'age': 30,
 'favorite': 'War adn Peace',
 'location': 'Wisconsin',
 'name': 'joe',
 'sex': 'male'}
# 将 favorite 字段的值修改为 Green Eggs and Ham
user.update_one({'name': 'joe'}, {'$set': {'favorite': 'Green Eggs and Ham'}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
 'age': 30,
 'favorite': 'Green Eggs and Ham',
 'location': 'Wisconsin',
 'name': 'joe',
 'sex': 'male'}
# 将 favorite 字段的值替换为一个数组
user.update_one({'name': 'joe'}, {'$set': {'favorite': ["Cat's Cradle": , "Foundation Trilogy", "Ender's Game"]}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
 'age': 30,
 'favorite': ["Cat's Cradle", 'Foundation Trilogy', "Ender's Game"],
 'location': 'Wisconsin',
 'name': 'joe',
 'sex': 'male'}
# 内嵌文档修改
blog = test_database.blog # 创建一个 blog 集合
posts = {'title': 'A Blog Post', 'content': '...', 'author': {'name': 'joe', 'email': 'joe@example.com'}} # 创建一个 posts 文档
blog.insert_one(posts)
blog.find_one()
{'_id': ObjectId('5ac98a0a29561f64220f6f9e'),
 'author': {'email': 'joe@example.com', 'name': 'joe'},
 'content': '...',
 'title': 'A Blog Post'}
# 将作者名称字段 name 的值修改为 joe schmoe
blog.update_one({'author.name': 'joe'}, {'$set': {'author.name': 'joe schmoe'}})
blog.find_one()
>> {'_id': ObjectId('5ac98a0a29561f64220f6f9e'),
 'author': {'email': 'joe@example.com', 'name': 'joe schmoe'},
 'content': '...',
 'title': 'A Blog Post'}

$unset

# 删除 user 集合中 joe 的 favorite 字段
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
 'age': 30,
 'favorite': ["Cat's Cradle", 'Foundation Trilogy', "Ender's Game"],
 'location': 'Wisconsin',
 'name': 'joe',
 'sex': 'male'}
user.update_one({'name': 'joe'}, {'$unset': {'favorite': 1}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
 'age': 30,
 'location': 'Wisconsin',
 'name': 'joe',
 'sex': 'male'}

$inc

# 创建一个 games 的集合,并向集合中添加数据
games = test_database.games
games.insert_one({'game': 'pinball', 'user': 'joe'})
games.find_one()
>> {'_id': ObjectId('5ac9c55f29561f64220f6f9f'), 'game': 'pinball', 'user': 'joe'}
# 增加一个分数字段 score
games.update_one({'game': 'pinball', 'user': 'joe'}, {'$inc': {'score': 50}})
games.find_one()
>> {'_id': ObjectId('5ac9c55f29561f64220f6f9f'),
 'game': 'pinball',
 'score': 50,
 'user': 'joe'}
# 为 score 字段的值增加 5000
games.update_one({'game': 'pinball', 'user': 'joe'}, {'$inc': {'score': 5000}})
games.find_one()
>> {'_id': ObjectId('5ac9c55f29561f64220f6f9f'),
 'game': 'pinball',
 'score': 5050,
 'user': 'joe'}

$push

# 选择 blog 数据库
blog = test_database.blog
blog.find_one()
>> {'_id': ObjectId('5ac98a0a29561f64220f6f9e'),
 'author': {'email': 'joe@example.com', 'name': 'joe schmoe'},
 'content': '...',
 'title': 'A Blog Post'}
# 添加一项评论字段 comment
blog.update_one({'title': 'A Blog Post'}, {'$push' : {'comments': {'name': 'joe', 'email': 'joe@example.com', 'content': 'nice post.'}}})
blog.find_one()
>> {'_id': ObjectId('5ac98a0a29561f64220f6f9e'),
 'author': {'email': 'joe@example.com', 'name': 'joe schmoe'},
 'comments': [{'content': 'nice post.',
   'email': 'joe@example.com',
   'name': 'joe'}],
 'content': '...',
 'title': 'A Blog Post'}
# 在添加一条由 bob 发表的评论
blog.update_one({'title': 'A Blog Post'}, {'$push' : {'comments': {'name': 'bob', 'email': 'bob@example.com', 'content': 'good post.'}}})
blog.find_one()
>> {'_id': ObjectId('5ac98a0a29561f64220f6f9e'),
 'author': {'email': 'joe@example.com', 'name': 'joe schmoe'},
 'comments': [{'content': 'nice post.',
   'email': 'joe@example.com',
   'name': 'joe'},
  {'content': 'good post.', 'email': 'bob@example.com', 'name': 'bob'}],
 'content': '...',
 'title': 'A Blog Post'}

$addToSet

# 为 user 集合中的 joe 文档添加 emails 字段
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
 'age': 30,
 'location': 'Wisconsin',
 'name': 'joe',
 'sex': 'male'}
user.update_one({'name': 'joe'},{'$push': {'emails': 'joe@example.com'}})
user.update_one({'name': 'joe'},{'$push': {'emails': 'joe@gmail.com'}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
 'age': 30,
 'emails': ['joe@example.com', 'joe@gmail.com'],
 'location': 'Wisconsin',
 'name': 'joe',
 'sex': 'male'}
# 使用 $addToSet 再添加一项 joe@example.com 的记录,因为存在重复,数据不会被重复添加
user.update_one({'name': 'joe'}, {'$addToSet': {'emails': 'joe@example.com'}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
 'age': 30,
 'emails': ['joe@example.com', 'joe@gmail.com'],
 'location': 'Wisconsin',
 'name': 'joe',
 'sex': 'male'}
# 如果直接使用 $push ,记录会被重复添加
user.update_one({'name': 'joe'}, {'$push': {'emails': 'joe@example.com'}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
 'age': 30,
 'emails': ['joe@example.com', 'joe@gmail.com', 'joe@example.com'],
 'location': 'Wisconsin',
 'name': 'joe',
 'sex': 'male'}

$pull

# 删除 user 集合中 joe 文档重复的 emails 值 ($pull 会删除所有符合条件的记录)
user.update_one({'name': 'joe'}, {'$pull': {'emails': 'joe@example.com'}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
 'age': 30,
 'emails': ['joe@gmail.com'],
 'location': 'Wisconsin',
 'name': 'joe',
 'sex': 'male'}

$each

# 向 user 集合中 joe 文档的 emails 字段追加两个邮箱地址
user.update_one({'name': 'joe'}, {'$push': {'emails': {'$each': ['joe@example.com', 'joe@outlook.com']}}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
 'age': 30,
 'emails': ['joe@gmail.com', 'joe@example.com', 'joe@outlook.com'],
 'location': 'Wisconsin',
 'name': 'joe',
 'sex': 'male'}

$pop

user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
 'age': 30,
 'emails': ['joe@gmail.com', 'joe@example.com', 'joe@outlook.com'],
 'location': 'Wisconsin',
 'name': 'joe',
 'sex': 'male'}
# 删除集合 user 中 joe 文档 emails 字段的第一个邮箱地址
user.update_one({'name': 'joe'}, {'$pop': {'emails': -1}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
 'age': 30,
 'emails': ['joe@example.com', 'joe@outlook.com'],
 'location': 'Wisconsin',
 'name': 'joe',
 'sex': 'male'}
# 删除集合 user 中 joe 文档 emails 字段的最后一个邮箱地址
user.update_one({'name': 'joe'}, {'$pop': {'emails': 1}})
user.find_one()
>> {'_id': ObjectId('5ac9836829561f64220f6f9d'),
 'age': 30,
 'emails': ['joe@example.com'],
 'location': 'Wisconsin',
 'name': 'joe',
 'sex': 'male'}

基于位置的数组修改器
如果文档中存在数组记录,而且数组记录有多个值,我们可以视同位置修噶器或者定位符 $ 来修改数组其中的某些值。

# 先清除 blog 集合中的原油数据
blog.drop()
# 添加一条 posts 记录
posts = {'content': '...', 'comments': [{'comment': 'good post', 'author': 'John', 'votes': 0}, {'comment': 'i thought it was too short', 'author': 'Claire', 'votes': 3}, {'comment': 'free watches', 'auth: or': 'Alice', 'votes': -1}]}
blog.insert_one(posts)
blog.find_one()
>> {'_id': ObjectId('5aca17cf29561f64220f6fa0'),
 'comments': [{'author': 'John', 'comment': 'good post', 'votes': 0},
  {'author': 'Claire', 'comment': 'i thought it was too short', 'votes': 3},
  {'author': 'Alice', 'comment': 'free watches', 'votes': -1}],
 'content': '...'}
# 为第一条评论 comments 的 votes 字段增加 1
post_id = blog.find_one()['_id']
blog.update_one({'_id': post_id}, {'$inc': {'comments.0.votes': 1}})
blog.find_one()
>> {'_id': ObjectId('5aca17cf29561f64220f6fa0'),
 'comments': [{'author': 'John', 'comment': 'good post', 'votes': 1},
  {'author': 'Claire', 'comment': 'i thought it was too short', 'votes': 3},
  {'author': 'Alice', 'comment': 'free watches', 'votes': -1}],
 'content': '...'}
# 定位符的使用,通常在不知道具体数组位置,使用定位查询文档来匹配数组元素
blog.update_one({'comments.author': 'John'}, {'$set': {'comments.$.author': 'Jim'}})
blog.find_one()
>> {'_id': ObjectId('5aca17cf29561f64220f6fa0'),
 'comments': [{'author': 'Jim', 'comment': 'good post', 'votes': 1},
  {'author': 'Claire', 'comment': 'i thought it was too short', 'votes': 3},
  {'author': 'Alice', 'comment': 'free watches', 'votes': -1}],
 'content': '...'}

PyMongo 查询相关操作

接下来,我们继续深入了解 Pymongo 的查询操作。查询的常用操作大致可以归为三部分,分别是查询的方法,可以使用find_one() 以及 find() 来进行查询;然后在实际运用中,我们可能需要使用 $ 条件操作符来帮助我们定位数据;最后我们会了解关于游标的一些常用操作。

PyMongo 的 find_one() 和 find()

为方便演示,我们会在 test_database 数据库中创建一个 users 的集合,并向其中添加三条文档记录。

users = test_database.users
joe = {'name': 'joe', 'age': 26}
mike = {'name': 'mike', 'age': 28}
jake = {'name': 'jake', 'age': 26}
# 使用 insert_many() 可以一次添加多个文档记录
users.insert_many([joe, mike, jake])
for data in users.find():
    print(data)
{'_id': ObjectId('5acb225729561f64220f6fa1'), 'name': 'joe', 'age': 26}
{'_id': ObjectId('5acb225729561f64220f6fa2'), 'name': 'mike', 'age': 28}
{'_id': ObjectId('5acb225729561f64220f6fa3'), 'name': 'jake', 'age': 26}

find() 的使用方法

我们使用 find() 方法,如果不传入任何参数,将返回该集合中的所有数据的一个游标,然后我们可以通过 for 来遍历游标来打印查询结果。

如果我们需要查找特定的数据,比如年龄为 28 的用户,那么我们可以给 find() 方法传入一个匹配的规则。

result = users.find({'age': 28})
# find() 的返回值是一个查询游标,在 python 中的数据类型为一个迭代器,我们可以使用 count() 查看查询结果数量
result.count()
>> 1
result.next()
>> {'_id': ObjectId('5acb225729561f64220f6fa2'), 'age': 28, 'name': 'mike'}

我们还可以通过传入多个同查询条件进行查询。

result = users.find({'age': 26, 'name': 'jake'})
# 以上表达式多个条件会被解释为 AND 关系
result.count()
>> 1
result.next()
>> {'_id': ObjectId('5acb225729561f64220f6fa3'), 'age': 26, 'name': 'jake'}

find_one() 的使用方法

上面我们演示了 find() 的用法,我们接下来将会演示 find_one() 的用法。 find_one() 的用法与 find() 的使用方法差别不大,他们的区别是使用 find_one() 最多只会返回一条文档记录,而 find() 则返回查询游标。以下是代码演示:

user.find_one()
>> {'_id': ObjectId('5acb225729561f64220f6fa1'), 'age': 26, 'name': 'joe'}
# find_one() 如果没有查询条件,会返回第一条记录

type(users.find_one({'name': 'kate'}))
>> NoneType
# 如果传入查询条件,没有查询结果,则会返回一个 NoneType

users.find_one({'age': 26})
>> '_id': ObjectId('5acb225729561f64220f6fa1'), 'age': 26, 'name': 'joe'}
# 如果查询匹配多个结果,find_one() 只会返回第一条匹配记录

指定返回字段

我们在查询的时候,可能并不需要文档中的所有字段,这是我们可以在查询条件之后再传入一个参数来指定返回的字段。

# 不要 _id 字段
users.find_one({}, {'_id': 0})
>> {'age': 26, 'name': 'joe'}
# 只输出 _id 字段
users.find_one({}, {'_id': 1})
>> {'_id': ObjectId('5acb225729561f64220f6fa1')}

Pymongo 条件查询操作

比较操作符
在查询中,我们会经常用到比较字段值得大小来查询数据,实现这一功能我们会用到比较操作符,Pymongo 常用的比较操作符有以下几个:

  • $lt 小于
  • $let 小于等于
  • $ge 大于
  • $gte 大于等于
  • $ne 不等于

下面我们将继续使用上面的 users 集合来演示上面几个比较操作符。

# 先遍历打印 users 集合中的数据
for data in users.find():
    print(data)
>> {'_id': ObjectId('5acb225729561f64220f6fa1'), 'name': 'joe', 'age': 26}
{'_id': ObjectId('5acb225729561f64220f6fa2'), 'name': 'mike', 'age': 28}
{'_id': ObjectId('5acb225729561f64220f6fa3'), 'name': 'jake', 'age': 26}
# 查询大于 26 岁的用户
for data in user.find({'age': {'$gt': 26}}):
    print(data)
>> {'_id': ObjectId('5acb225729561f64220f6fa2'), 'name': 'mike', 'age': 28}
# 查询大于等于 26 岁的用户
for data in user.find({'age': {'$gte': 26}}):
    print(data)
>> {'_id': ObjectId('5acb225729561f64220f6fa1'), 'name': 'joe', 'age': 26}
{'_id': ObjectId('5acb225729561f64220f6fa2'), 'name': 'mike', 'age': 28}
{'_id': ObjectId('5acb225729561f64220f6fa3'), 'name': 'jake', 'age': 26}
# 查询小于 28 岁的用户
for data in user.find({'age': {'$lt': 28}}):
    print(data)
>> {'_id': ObjectId('5acb225729561f64220f6fa1'), 'name': 'joe', 'age': 26}
{'_id': ObjectId('5acb225729561f64220f6fa3'), 'name': 'jake', 'age': 26}
# 查询小于等于 28 岁的用户
for data in user.find({'age': {'$lte': 28}}):
    print(data)
>> {'_id': ObjectId('5acb225729561f64220f6fa1'), 'name': 'joe', 'age': 26}
{'_id': ObjectId('5acb225729561f64220f6fa2'), 'name': 'mike', 'age': 28}
{'_id': ObjectId('5acb225729561f64220f6fa3'), 'name': 'jake', 'age': 26}
# 查询不等于 28 岁的用户
for data in user.find({'name': {'$ne': 'mike'}}):
    print(data)
>> {'_id': ObjectId('5acb225729561f64220f6fa1'), 'name': 'joe', 'age': 26}
{'_id': ObjectId('5acb225729561f64220f6fa3'), 'name': 'jake', 'age': 26}

$in 和 $nin 的用法
我们可以使用 $in$nin 操作符来匹配一个键的多个值,具体用法示例如下:

# 匹配 users 集合中 用户名为 joe 和 mike 的文档记录
for data in users.find({'name': {'$in': ['joe', 'mike']}}):
    print(data)
>> {'_id': ObjectId('5acb225729561f64220f6fa1'), 'name': 'joe', 'age': 26}
{'_id': ObjectId('5acb225729561f64220f6fa2'), 'name': 'mike', 'age': 28}
# 匹配用户名不是 mike 的用户 注意: $in 和 $nin 条件必须是一个数组
for data in users.find({'name': {'$nin': ['mike']}}):
    print(data)
>> {'_id': ObjectId('5acb225729561f64220f6fa1'), 'name': 'joe', 'age': 26}
{'_id': ObjectId('5acb225729561f64220f6fa3'), 'name': 'jake', 'age': 26}

$or 的用法
如果需要查询两个条件中其中一个为真的查询结果,可以使用 $or 操作符。具体示例如下:

# 为方便演示,我们先插入多一条文档记录
kate = {'name': 'kate', 'age': 30}
users.insert_one(kate)
for data in users.find({'$or': [{'name': 'mike'}, {'age': 30}]}):
    print(data)
{'_id': ObjectId('5acb225729561f64220f6fa2'), 'name': 'mike', 'age': 28}
{'_id': ObjectId('5acb6cfc29561f64220f6fa4'), 'name': 'kate', 'age': 30}

null 值查询和 $exists 条件判定
在 Python 中,mongodb 中的 null 值以 None 表示。但在查询 null 值中,会出现比较奇怪的情况,以下为演示案例:

# 为方便演示,创建一个 c 集合,并向里面添加 3 条记录
c = test_database.c
c.insert_many({'y': None}, {'y': 1}, {'y': 2})
for data in c.find():
    print(data)
>> {'_id': ObjectId('5acb738029561f64220f6fa5'), 'y': None}
{'_id': ObjectId('5acb741029561f64220f6fa6'), 'y': 1}
{'_id': ObjectId('5acb741029561f64220f6fa7'), 'y': 2}
# 查询 null 值
for data in c.find({'y': None}):
    print(data)
>> {'_id': ObjectId('5acb738029561f64220f6fa5'), 'y': None}
# 查询一个不存在的键,查询条件为 null
for data in c.find({'z': None}):
    print(data)
>> {'_id': ObjectId('5acb738029561f64220f6fa5'), 'y': None}
{'_id': ObjectId('5acb741029561f64220f6fa6'), 'y': 1}
{'_id': ObjectId('5acb741029561f64220f6fa7'), 'y': 2}

可以看到,当我们查找 {'z': None} 的时候,会把所有不包含这个条件的文档都查询出来,这样明显和我们的意图不一致,因此我们需要增加一个限定,具体代码如下:

for data in c.find({'z': {'$in': [None], '$exists': 1}}):
    print(data)
>> 

通过加上 $exists 的限定,我们可以看到代码执行完之后并没有查询结果输出,符合我们的查询意图。

查询数组
在实际使用当中,我们还可能会存在文档中有数组形式的字段值,因此我们需要一些特定的操作来查询匹配这些数组,同样的,MongoDb 提供了相关的操作符可以使用,常用的数组操作符有以下几个:

  • $all 匹配多个元素数组
  • $size 匹配特定长度的数组
  • $slice 返回匹配数组的一个子集

下面将用代码演示以上三个操作符的用法。为方便演示,我们会先创建一个 food 的集合用来存放水果的文档记录。

food = test_database.food
food.insert_one({'_id': 1, 'fruit': ['apple', 'banana', 'peach']})
food.insert_one({'_id': 2, 'fruit': ['apple', 'kumquat', 'orange']})
food.insert_one({'_id': 3, 'fruit': ['cherry', 'banana', 'apple']})
for data in food.find():
    print(data)
>> {'_id': 1, 'fruit': ['apple', 'banana', 'peach']}
{'_id': 2, 'fruit': ['apple', 'kumquat', 'orange']}
{'_id': 3, 'fruit': ['cherry', 'banana', 'apple']}

$all 的用法

result = food.find({'fruit': {'$all': ['apple', 'banana']}})
for data in result:
    print(data)
>> {'_id': 1, 'fruit': ['apple', 'banana', 'peach']}
{'_id': 3, 'fruit': ['cherry', 'banana', 'apple']}
# 也可以使用位置定位匹配
result = food.find({'fruit.1': 'banana'})
for data in result:
    print(data)
>> {'_id': 1, 'fruit': ['apple', 'banana', 'peach']}
{'_id': 3, 'fruit': ['cherry', 'banana', 'apple']}

$size 的用法
为方便演示,我们会向 food 集合中的第二个文档添加多一个水果。

food.update_one({'_id': 2}, {'$push': {'fruit': 'strawbreey'}})
for data in food.find():
    print(data)
>> {'_id': 1, 'fruit': ['apple', 'banana', 'peach']}
{'_id': 2, 'fruit': ['apple', 'kumquat', 'orange', 'strawbreey']}
{'_id': 3, 'fruit': ['cherry', 'banana', 'apple']}
# 查找数组size为3的结果
result = food.find({'fruit': {'$size': 3}})
for data in result:
    print(data)
>> {'_id': 1, 'fruit': ['apple', 'banana', 'peach']}
{'_id': 3, 'fruit': ['cherry', 'banana', 'apple']}

$slice 的用法
$slice 可以返回某个键匹配的数组的一个子集,我们将会用 blog 集合来演示使用 $slice 操作符获取特定数量的评论记录。

blog.find_one()
>> {'_id': ObjectId('5aca17cf29561f64220f6fa0'),
 'comments': [{'author': 'Jim', 'comment': 'good post', 'votes': 1},
  {'author': 'Claire', 'comment': 'i thought it was too short', 'votes': 3},
  {'author': 'Alice', 'comment': 'free watches', 'votes': -1}],
 'content': '...'}
# 获取前两条评论记录
blog.find_one({}, {'comments': {'$slice': 2}})
>> {'_id': ObjectId('5aca17cf29561f64220f6fa0'),
 'comments': [{'author': 'Jim', 'comment': 'good post', 'votes': 1},
  {'author': 'Claire', 'comment': 'i thought it was too short', 'votes': 3}],
 'content': '...'}
# 获取最后一条评论记录
blog.find_one({}, {'comments': {'$slice': -1}})
>> {'_id': ObjectId('5aca17cf29561f64220f6fa0'),
 'comments': [{'author': 'Alice', 'comment': 'free watches', 'votes': -1}],
 'content': '...'}

min() 和 max() 的使用
如果我们以某个区间值作为查询条件,我们可以使用比较操作符来实现,但是,如果文档中存在值,以及值组成的数组时,查询结果往往与我们的意图不一致,这是我们就需要用到 $elemMatch 来匹配非数组元素,或者使用 min() 和 max() 方法。

# 我们先清空 c 集合,并加入新的文档记录
c.drop()
c.count()
>> 0
c.insert_many([{'x':  5}, {'x': 15}, {'x': 25}, {'x': [5, 25]}])
c.count()
>> 4
for data in c.find():
    print(data)
>> {'_id': ObjectId('5acc31c729561f64220f6fa8'), 'x': 5}
{'_id': ObjectId('5acc31c729561f64220f6fa9'), 'x': 15}
{'_id': ObjectId('5acc31c729561f64220f6faa'), 'x': 25}
{'_id': ObjectId('5acc31c729561f64220f6fab'), 'x': [5, 25]}
# 假设我们需要查询 [10, 20] 区间内的记录
result = c.find({'x': {'$gt': 10. '$lt': 20}})
for data in result:
    print(data)
>> {'_id': ObjectId('5acc31c729561f64220f6fa9'), 'x': 15}
{'_id': ObjectId('5acc31c729561f64220f6fab'), 'x': [5, 25]}
# 这里看到 [5, 25] 这一条记录其实是不符合我们的查询预期的

# 我们可以使用 $elemMatch 来不匹配非数组元素
result = c.find({'x': {'$elemMatch': {'$gt': 10, '$lt': 20}}})
for data in result:
    print(data)
>> // 没有输出结果
# 通过添加 $elemMatch 可以剔除 [5, 25] 这一记录,但正确的查询结果 {'x': 15} 却不能匹配
# 我们将使用 min() 以及 max() 方法
# 为使用者两个方法,我们需要先给 c 集合的 x 字段建立索引
c.create_index('x')
>> 'x_1'
result = c.find({'x': {'$gt': 10, '$lt': 20}}).min([('x', 10)]).max([('x', 20)])
for data in result:
    print(data)
>> {'_id': ObjectId('5acc31c729561f64220f6fa9'), 'x': 15}

关于 min()max() 两个方法,有两点需要注意:

  • 使用这两个方法前,必须先要为需要查询的字段建立索引,否则会报错
  • 这两个方法和在 mongodb 中的写法有一些不一样,在 mongodb 中,同样操作写作: min({'x': 10}) 但在 Pymongo 中,应写成 min([(‘x’, 10)]) 注意区别,否则同样会报错。

$where 查询
针对一些比较复杂的查询,我们可以使用 $where 。然而,由于 $where 可以在查询中执行任意的 Javascript,因此可能会产生出一些不安全的操作,因此,在实际生产环境中,竟可能的不用或者禁用 $where。以下代码演示 $where 的基本用法:

# 为方便演示,我们会再次用到 foo 集合,我们先清空以下这一个集合
foo.drop()
# 向 foo 集合中添加两条文档记录
foo.insert_one({'apple': 1, 'banana': 6, 'peach': 3})
foo.insert_one({'apple': 8, 'spinach': 4, 'watermelon': 4})

for data in foo:
    print(data)
>> {'_id': ObjectId('5acdbf4729561f64220f6fac'), 'apple': 1, 'banana': 6, 'peach': 3}
{'_id': ObjectId('5acdbf6a29561f64220f6fad'), 'apple': 8, 'spinach': 4, 'watermelon': 4}

# 接下来,我们需要查找存在两个水果数量相等的文档
result = foo.find({'$where':
"""
function() {
     for (var current in this) {
         for (var other in this) {
             if (current != other && this[current] == this[other]){
                 return true;
             }
        }
    }
return false;
}
"""})

for data in result:
    print(data)
>> {'_id': ObjectId('5acdbf6a29561f64220f6fad'), 'apple': 8, 'spinach': 4, 'watermelon': 4}

游标

当我们进行查询操作的时候,程序执行查询指令后,返回的不是我们的查询结果,而是一个游标,在 Pymongo 库中,指定 find() 方法后,返回的是一个 pymongo.cursor.Cursor 的对象,这一个对象我们可以简单的理解成一个迭代器,因此我们就可以像上文一样,使用 for 循环来遍历所有查询结果。

type(foo.find())
>> pymongo.cursor.Cursor

以下再次演示使用 for 遍历输出查询结果,为方便演示,我们再次清空 foo 集合。

foo.drop()
# 我们创建一系列文档
import random
for i in range(20):
     foo.insert_one({'x': random.randint(0, 20)})

for data in foo.find():
    print(data)
>> {'_id': ObjectId('5acdc47d29561f64220f6fc2'), 'x': 15}
{'_id': ObjectId('5acdc47d29561f64220f6fc3'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fc4'), 'x': 0}
{'_id': ObjectId('5acdc47d29561f64220f6fc5'), 'x': 10}
{'_id': ObjectId('5acdc47d29561f64220f6fc6'), 'x': 7}
{'_id': ObjectId('5acdc47d29561f64220f6fc7'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fc8'), 'x': 8}
{'_id': ObjectId('5acdc47d29561f64220f6fc9'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fca'), 'x': 13}
{'_id': ObjectId('5acdc47d29561f64220f6fcb'), 'x': 6}
{'_id': ObjectId('5acdc47d29561f64220f6fcc'), 'x': 2}
{'_id': ObjectId('5acdc47d29561f64220f6fcd'), 'x': 2}
{'_id': ObjectId('5acdc47d29561f64220f6fce'), 'x': 14}
{'_id': ObjectId('5acdc47d29561f64220f6fcf'), 'x': 18}
{'_id': ObjectId('5acdc47d29561f64220f6fd0'), 'x': 5}
{'_id': ObjectId('5acdc47d29561f64220f6fd1'), 'x': 1}
{'_id': ObjectId('5acdc47d29561f64220f6fd2'), 'x': 0}
{'_id': ObjectId('5acdc47d29561f64220f6fd3'), 'x': 6}
{'_id': ObjectId('5acdc47d29561f64220f6fd4'), 'x': 4}
{'_id': ObjectId('5acdc47d29561f64220f6fd5'), 'x': 5}

sort() 排序的用法
我们在基于上面的集合数据之下,对 x 进行一个排序操作,可以使用 sort() 方法,具体操作如下:

import pymongo
result = foo.find()
# 升序
result.sort([('x', pymongo.ASCENDING)])
for data in result:
    print(data)
>> {'_id': ObjectId('5acdc47d29561f64220f6fc4'), 'x': 0}
{'_id': ObjectId('5acdc47d29561f64220f6fd2'), 'x': 0}
{'_id': ObjectId('5acdc47d29561f64220f6fd1'), 'x': 1}
{'_id': ObjectId('5acdc47d29561f64220f6fcc'), 'x': 2}
{'_id': ObjectId('5acdc47d29561f64220f6fcd'), 'x': 2}
{'_id': ObjectId('5acdc47d29561f64220f6fd4'), 'x': 4}
{'_id': ObjectId('5acdc47d29561f64220f6fd0'), 'x': 5}
{'_id': ObjectId('5acdc47d29561f64220f6fd5'), 'x': 5}
{'_id': ObjectId('5acdc47d29561f64220f6fcb'), 'x': 6}
{'_id': ObjectId('5acdc47d29561f64220f6fd3'), 'x': 6}
{'_id': ObjectId('5acdc47d29561f64220f6fc6'), 'x': 7}
{'_id': ObjectId('5acdc47d29561f64220f6fc8'), 'x': 8}
{'_id': ObjectId('5acdc47d29561f64220f6fc5'), 'x': 10}
{'_id': ObjectId('5acdc47d29561f64220f6fca'), 'x': 13}
{'_id': ObjectId('5acdc47d29561f64220f6fce'), 'x': 14}
{'_id': ObjectId('5acdc47d29561f64220f6fc2'), 'x': 15}
{'_id': ObjectId('5acdc47d29561f64220f6fc3'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fc7'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fc9'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fcf'), 'x': 18}
# 降序
for data in foo.find().sort([('x', pymongo.DESCENDING)]):
     print(data)
>> {'_id': ObjectId('5acdc47d29561f64220f6fcf'), 'x': 18}
{'_id': ObjectId('5acdc47d29561f64220f6fc3'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fc7'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fc9'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fc2'), 'x': 15}
{'_id': ObjectId('5acdc47d29561f64220f6fce'), 'x': 14}
{'_id': ObjectId('5acdc47d29561f64220f6fca'), 'x': 13}
{'_id': ObjectId('5acdc47d29561f64220f6fc5'), 'x': 10}
{'_id': ObjectId('5acdc47d29561f64220f6fc8'), 'x': 8}
{'_id': ObjectId('5acdc47d29561f64220f6fc6'), 'x': 7}
{'_id': ObjectId('5acdc47d29561f64220f6fcb'), 'x': 6}
{'_id': ObjectId('5acdc47d29561f64220f6fd3'), 'x': 6}
{'_id': ObjectId('5acdc47d29561f64220f6fd0'), 'x': 5}
{'_id': ObjectId('5acdc47d29561f64220f6fd5'), 'x': 5}
{'_id': ObjectId('5acdc47d29561f64220f6fd4'), 'x': 4}
{'_id': ObjectId('5acdc47d29561f64220f6fcc'), 'x': 2}
{'_id': ObjectId('5acdc47d29561f64220f6fcd'), 'x': 2}
{'_id': ObjectId('5acdc47d29561f64220f6fd1'), 'x': 1}
{'_id': ObjectId('5acdc47d29561f64220f6fc4'), 'x': 0}
{'_id': ObjectId('5acdc47d29561f64220f6fd2'), 'x': 0}

limit() 的使用
上面我们的 foo 文档中有 20 条文档记录,假如我们不需要一次获取所有文档,我们可以使用 limit() 方法来限制查询结果数量,具体操作如下:

for data in foo.find().limit(5):
     print(data)
>> {'_id': ObjectId('5acdc47d29561f64220f6fc2'), 'x': 15}
{'_id': ObjectId('5acdc47d29561f64220f6fc3'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fc4'), 'x': 0}
{'_id': ObjectId('5acdc47d29561f64220f6fc5'), 'x': 10}
{'_id': ObjectId('5acdc47d29561f64220f6fc6'), 'x': 7}

skip() 的使用
我们可以使用 skip() 来跳过一定数量的文档,以下为代码演示:

for data in foo.find().skip(5).limit(5):
     print(data)
>> {'_id': ObjectId('5acdc47d29561f64220f6fc7'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fc8'), 'x': 8}
{'_id': ObjectId('5acdc47d29561f64220f6fc9'), 'x': 17}
{'_id': ObjectId('5acdc47d29561f64220f6fca'), 'x': 13}
{'_id': ObjectId('5acdc47d29561f64220f6fcb'), 'x': 6}
# 我们可以看到,输出的结果和上面使用 limit(5) 的数据不一样,这里是跳过前 5 条记录的后 5条记录

小结

通过以上的讲解以及代码演示,我们已经了解基本的 Pymongo 操作,我们可以自己完成一些小的项目,将以上的技能融会贯通的使用吸收,以便日后在开发中更得心应手。

参考资料

《Mongo权威指南》
MongoDB 官方文档
Pymongo 官方文档

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容