本篇文档旨在介绍快速使用Python程序使用PyMongo驱动程序操作MongoDB数据库。
一、前提
1.MongoDB的服务已成功启动
在开始前, 请确保MongoDB的实例已经成功运行,为了简便起见,本人使用的是本机的MongoDB,用的是默认的localhost与端口27017。
在Ubuntu系统上启动MongoDB的实例:
# 查看MongoDB状态
$ sudo systemctl status mongodb
# 如果没有启动,可以使用下面命令启动
$ sudo systemctl start mongodb
# 设置开机启动
$ sudo systemctl enable mongodb
1.2.确保PyMongo成功安装
在开始前,请确保PyMongo已经被安装,可以在shell窗口输入下面命令安装PyMongo:
$ pip install pymongo
当你在Python shell中输入下面命令不报错时,说明pymongo已安装完好:
>>> import pymongo
二、通过PyMongo的MongoClient创建连接
使用PyMongo操作MongoDB的第一步是创建MongoClient的实例去连接运行的mongod实例:
from pymongo import MongoClient
# 下面命令用于连接默认的host和port的mongod实例
client = MongoClient()
# 也可以指定host和port
client = MongoClient(host='localhost', port=27017)
# 也可以通过MongoDB URI格式创建连接
client = MongoClient('mongodb://localhost:27017/')
三、获取数据库 (Databases)
一个MongoDB的实例支持运行多个独立的数据库。当使用PyMongo访问MongoDB时,可以在MongoClient实例上使用属性样式访问数据库:
db = client.testdb # testdb为数据库名称
如果MongoDB的数据库名通过属性风格无法访问(比如:test-db), 那么可以使用字典风格的访问方式:
db = client['test-db']
四、获取集合(Collections)
集合是存储在MongoDB中的一组文档,可以看作和关系数据库中表的等价。在PyMongo中获取集合与获取数据库的工作原理相同:
collection = db.test_collection
使用字典风格访问:
collection = db['test_collection']
关于MongoDB中集合(和数据库)的一个重要注意事项是: 它们是延迟创建的——上面的命令都没有在MongoDB服务器上实际执行任何操作。在向集合和数据库中插入第一个文档时,将创建这些集合和数据库。
五、文档(Documents)
MongoDB中的数据使用类JSON样式的文档表示(并存储)。在PyMongo中,我们使用字典来表示文档。例如,可以使用以下字典来表示博客文章:
import datetime
post = {
'author':'Mike',
'text': 'My first blog post! The post is mostly introducing how to use PyMongo to interact with MongoDB',
'tag':['mongodb','python','pymongo'],
'date': datetime.datetime.utcnow()
}
注意,文档可以包含基本Python类型(比如日期时间。日期时间实例),它将自动与相应的BSON类型进行转换。
六、插入文档
将一个文档插入到集合中,我们可以使用insert_one()方法:
In [9]: posts = db.posts
In [10]: post_id = posts.insert_one(post).inserted_id
In [11]: post_id
Out[11]: ObjectId('5f282900d28216c1cebfacd8')
在向集合中插入文档时如果没有指定"_id"键,会默认自动添加一个"_id"。"_id"必须在这个集合中是唯一的。
在插入第一个文档之后,posts集合实际上已经在服务器上创建了。我们可以通过列出数据库中的所有集合来验证这一点:
In [12]: db.list_collection_names()
Out[12]: ['posts']
七、使用find_one()获取一个文档
MongoDB中可以执行的最基本的查询类型是find_one()。此方法返回与查询匹配的单个文档(如果没有匹配项,则返回无)。当您知道只有一个匹配的文档,或者只对第一个匹配的文档感兴趣时,它非常有用。这里我们使用find_one()从posts集合获取第一个文档:
In [13]: db.posts.find_one()
Out[13]:
{'_id': ObjectId('5f282900d28216c1cebfacd8'),
'author': 'Mike',
'text': 'My first blog post! The post is mostly introducing how to use PyMongo to interact with MongoDB',
'tag': ['mongodb', 'python', 'pymongo'],
'date': datetime.datetime(2020, 8, 3, 15, 10, 1, 40000)}
如果我们查询一个不存在的author,比如“Eliot”,我们不会得到任何结果:
In [17]: db.posts.find_one({"author":"Eliot"})
In [18]:
八、通过ObjectId查询
可以通过"_id"查询一个post:
In [20]: import pprint
In [21]: post_id
Out[21]: ObjectId('5f282900d28216c1cebfacd8')
In [22]: pprint.pprint(posts.find_one({"_id":post_id}))
{'_id': ObjectId('5f282900d28216c1cebfacd8'),
'author': 'Mike',
'date': datetime.datetime(2020, 8, 3, 15, 10, 1, 40000),
'tag': ['mongodb', 'python', 'pymongo'],
'text': 'My first blog post! The post is mostly introducing how to use '
'PyMongo to interact with MongoDB'}
请注意,ObjectId与其字符串表示形式不同:
In [23]: type(post_id)
Out[23]: bson.objectid.ObjectId
In [24]: post_id_as_str = str(post_id)
In [26]: posts.find_one({"_id":post_id_as_str}) # 没有结果返回
...:
web应用程序中的一个常见任务是从请求URL获取ObjectId并找到匹配的文档。在这种情况下,有必要在调用find_one()方法时传递字符串前将其转换成ObjectId:
In [29]: from bson.objectid import ObjectId
...:
...: # web应用从URL中获取post_id并以字符串形式传递
...: def get(post_id):
...: # 从字符串转换成ObjectId
...: document = client.posts.find_one({'_id':ObjectId(post_id)})
关于Unicode字符串
您可能注意到,从服务器检索时,我们先前存储的常规Python字符串看起来不同(例如,u'Mike'而不是'Mike')。一个简短的解释是合理的。
MongoDB以BSON格式存储数据。BSON字符串是UTF-8编码的,因此PyMongo必须确保它存储的任何字符串只包含有效的UTF-8数据。常规字符串(<type'str'>)将被验证并存储为原样。Unicode字符串(<type'Unicode'>)首先编码为UTF-8。我们的示例字符串在python shell中表示为u'Mike'而不是'Mike',原因是PyMongo将每个BSON字符串解码为Python unicode字符串,而不是常规str。
九、批量插入
为了使查询更加有趣,让我们再插入一些文档。除了插入单个文档外,我们还可以执行大容量插入操作,方法是将列表作为第一个参数传递给insert_many()。这将插入列表中的每个文档,只向服务器发送一个命令:
In [29]: new_posts = [{"author": "Mike",
...: "text": "Another post!",
...: "tags": ["bulk", "insert"],
...: "date": datetime.datetime(2009, 11, 12, 11, 14)},
...: {"author": "Eliot",
...: "title": "MongoDB is fun",
...: "text": "and pretty easy too!",
...: "date": datetime.datetime(2009, 11, 10, 10, 45)}]
?
In [30]: result = db.posts.insert_many(new_posts)
?
In [31]: result.inserted_ids
Out[31]: [ObjectId('5f283257d28216c1cebfacd9'), ObjectId('5f283257d28216c1cebfacda')]
?
关于这个例子,有几个有趣的事情需要注意:
- insert_many()的结果现在返回两个ObjectId实例,每个插入的文档一个。
- posts[1]的结构与其它的posts不同(其它posts没有tags),我们添加了一个新字段“title”。这就是我们所说的MongoDB是无模式的。
十、查询多个文档
要获取多个文档作为查询的结果,我们使用find()方法。find()返回一个游标实例,它允许我们迭代所有匹配的文档。例如,我们可以迭代posts集合中的每个文档:
In [32]: for post in db.posts.find():
...: pprint.pprint(post)
...:
...:
{'_id': ObjectId('5f282900d28216c1cebfacd8'),
'author': 'Mike',
'date': datetime.datetime(2020, 8, 3, 15, 10, 1, 40000),
'tag': ['mongodb', 'python', 'pymongo'],
'text': 'My first blog post! The post is mostly introducing how to use '
'PyMongo to interact with MongoDB'}
{'_id': ObjectId('5f283257d28216c1cebfacd9'),
'author': 'Mike',
'date': datetime.datetime(2009, 11, 12, 11, 14),
'tags': ['bulk', 'insert'],
'text': 'Another post!'}
{'_id': ObjectId('5f283257d28216c1cebfacda'),
'author': 'Eliot',
'date': datetime.datetime(2009, 11, 10, 10, 45),
'text': 'and pretty easy too!',
'title': 'MongoDB is fun'}
就像find_one()一样,我们可以向find()传递一个文档来限制返回的结果。这里,我们只得到那些作者是“Mike”的文档:
In [33]: for post in db.posts.find({'author':'Mike'}):
...: pprint.pprint(post)
...:
{'_id': ObjectId('5f282900d28216c1cebfacd8'),
'author': 'Mike',
'date': datetime.datetime(2020, 8, 3, 15, 10, 1, 40000),
'tag': ['mongodb', 'python', 'pymongo'],
'text': 'My first blog post! The post is mostly introducing how to use '
'PyMongo to interact with MongoDB'}
{'_id': ObjectId('5f283257d28216c1cebfacd9'),
'author': 'Mike',
'date': datetime.datetime(2009, 11, 12, 11, 14),
'tags': ['bulk', 'insert'],
'text': 'Another post!'}
十一、文档计数
如果我们只想知道有多少文档与一个查询匹配,我们可以执行estimated_document_count()操作,而不是执行完整的查询。我们可以获得集合中所有文档的计数:
In [38]: db.posts.estimated_document_count()
Out[38]: 3
# 或者, 注:{}不能省略
In [41]: db.posts.count_documents({})
Out[41]: 3
如果是有条件的查询:
In [42]: db.posts.count_documents({'author':'Mike'})
Out[42]: 2
注:老版本通过db.collection.count(),现在在新版本中已过时,建议使用上面两种方法。