
|-----------------------|
| 你好,我是安然无虞。 |
| 和我一起,为高质量人生而不懈奋斗。 |
文章目录

MongoDB简介
MongoDB是一个基于分布式文件存储的数据库. 由C++语言编写. 旨在为WEB应用提供可扩展的高性能数据存储解决方案.
MongoDB是一个介于 关系数据库 和 非关系数据库 之间的产品, 是非关系数据库当中功能最丰富、最像关系数据库的.
它支持的数据结构非常松散, 类似 json的bson格式, 因此可以存储比较复杂的数据类型.
MongoDB最大的特点是它支持的查询语言非常强大, 其语法有点类似于面向对象的查询语言, 几乎可以实现类似关系数据库单表查询的绝大部分功能, 而且还支持对数据建立 索引.
并且无模式, 灵活的文档类型, 无需固定的结构.
命名规则:
- 不能是空字符串.
- 不能包含空字符.
- 不能使用system前缀, 因为是系统保留的.
- 建议不包含$.
MongoDB 对比 MySQL
| mongo概念 | MySQL概念 | 说明 |
|---|---|---|
| database | database | 数据库 |
| collection | table | 集合 - 表 |
| document | row | 文档 - 数据行 |
| field | column | 字段 - 属性 |
| index | index | 索引 |
| 嵌入文档 | join | 使用嵌入式的文档来代替关联查询 |
| primary key | primary key | mongodb自动生成主键 _id |
基本指令
macOS中已通过 brew services start mongodb-community@8.2 运行`本地 MongoDB 服务.
接着使用 mongosh 指令启动并连接 MongoDB.
-
显示当前的所有数据库
shellshow dbs show databases # 比如看系统中默认的数据库 test> show dbs admin 40.00 KiB config 100.00 KiB local 64.00 KiB -
进入到指定的数据库中
shelluse 数据库名 # 比如: test> use local switched to db local local> -
当前所处的数据库
shelldb local> db local -
显示数据库中所有的集合 (表)
shellshow collections local> show collections startup_log
增删查改
增加
向数据库中插入一个文档
shell
db.{collection}.insert(document)
# 例如:
# 新建一个数据库yrx
test> use yrx
switched to db yrx
yrx> db
yrx
# 因为此时新数据库yrx还没有数据,所以暂时看不到yrx
yrx> show dbs
admin 40.00 KiB
config 108.00 KiB
local 40.00 KiB
# 向yrx中插入一个集合(表)company
yrx> db.company.insert({name: 'zhangsan', age: 18, gender: 'male'})
DeprecationWarning: Collection.insert() is deprecated. Use insertOne, insertMany, or bulkWrite.
{
acknowledged: true,
insertedIds: { '0': ObjectId('69756bcaffa7a096b3e7e146') }
}
yrx> show collections
company
yrx>
# 新数据库yrx中有集合(表)数据了,现在可以看到yrx了
yrx> show dbs
admin 40.00 KiB
config 96.00 KiB
local 64.00 KiB
yrx 40.00 KiB
上面是向数据库中插入一个文档, 当插入多个时, 可以这样:
shell
db.company.insert([{name: 'lisi', age: 20, gender: 'male'}, {name: 'lulu', age: 18, gender: 'female'}])
db.company.insert([{name: 'zhangwen', age: 23, gender: 'male'}, {name: 'liwen', age: 25, gender: 'female'}, {name: 'zhou', gender: 'female'}])
看插入结果:
shell
yrx> db.company.insert([{name: 'zhangwen', age: 23, gender: 'male'}, {name: 'liwen', age: 25, gender: 'female'}, {name: 'zhou', gender: 'female'}])
{
acknowledged: true,
insertedIds: {
'0': ObjectId('69793ef52377fa2311816ff1'),
'1': ObjectId('69793ef52377fa2311816ff2'),
'2': ObjectId('69793ef52377fa2311816ff3')
}
}
这里我们能看出 MongoDB 会自动生成主键 _id.
我们来查看一下可视化程序里面的数据:
json
{
"_id" : ObjectId("69756bcaffa7a096b3e7e146"),
"name" : "zhangsan",
"age" : NumberInt(18),
"gender" : "male"
}
{
"_id" : ObjectId("69793e7d2377fa2311816fef"),
"name" : "lisi",
"age" : NumberInt(20),
"gender" : "male"
}
{
"_id" : ObjectId("69793e7d2377fa2311816ff0"),
"name" : "lulu",
"age" : NumberInt(18),
"gender" : "female"
}
......
这里的 主键_id 是可以自己指定的, 但是自己指定的话, 要确保其唯一性.
比如:
shell
db.company.insert({_id: 123, name: 'zhang', age: 18, gender: 'male'})
除了 insert() 方法之外, 还有其他的方法, 比如:
- insertOne(): 插入一个文档
- insertMany(): 插入多个文档
上面的方法语义明确、易懂.
查询
查询文档.
shell
db.{collection}.find()
用于查询集合中的文档, 可以接收一个对象作为查询条件.
比如我们想查询前面集合中所有的文档, 可以:
shell
db.company.find({})
如果我们想查询集合中指定的字段, 比如年龄是18岁的人:
shell
db.company.find({age: 18})
我们也可以使用 findOne()来返回查询到的第一条数据.
countDocuments()方法 返回查询结果的数量.
比如:
shell
yrx> db.company.countDocuments()
7
yrx> db.company.countDocuments({age: 18})
3
查询指定字段, 在查询条件后再加一个对象, 指明你想要的字段, 不想要查询出来的字段赋值为0:
shell
yrx> db.company.find({age: 18.0}, {name: 1, _id: 0})
[ { name: 'zhangsan' }, { name: 'lulu' }, { name: 'zhang' } ]
# 如果没有指明不想要的字段, 默认会有主键------id
yrx> db.company.find({age: 18.0}, {name: 1})
[
{ _id: ObjectId('69756bcaffa7a096b3e7e146'), name: 'zhangsan' },
{ _id: ObjectId('69793e7d2377fa2311816ff0'), name: 'lulu' },
{ _id: 123, name: 'zhang' }
]
常用的比较运算符:
- $gt: 大于
- $lt: 小于
- $gte: 大于等于
- $lte: 小于等于
- $ne: 不等于
shell
db.company.find({age: {$ne: 18}})
yrx> db.company.countDocuments({age: {$ne: 18}})
4
更新
shell
db.{collection}.update(查询条件, 新对象)
旧写法: update() 默认只会更新第一个数据, 如果要更新多个数据, 需要加上 multi 参数, 并设置为true.
新写法对比:
- updateOne()
- updateMany()
| 旧写法(已弃用) | 新写法(推荐) | 说明 |
|---|---|---|
update(filter, update) |
updateOne(filter, update) |
更新第一个匹配项 |
update(filter, update, { multi: true }) |
updateMany(filter, update) |
更新所有匹配项 |
使用更新操作符: $set 修改文档中指定字段的值.
例如: 将 liwen 的名字改成 liwenwen.
shell
db.company.updateOne({name: 'liwen'}, {$set: {name: 'liwenwen'}})
使用 $unset删除文档中指定的字段.
shell
db.company.updateOne({_id: 123}, {$unset: {age: 1}})
删除
删除操作了解即可. 真实工作中尽量别用.
一般都是做逻辑删除, 比如: 新增一个字段 is_delete, 并且设置is_delete=true.
shell
db.{collection}.remove()
删除符合条件的所有数据.
删除一个justOne 传true, 则只删除一个.
必须传递参数, 不传参会报错, 如果传递一个{}, 则删除所有.
db.{collection}.deleteOne(): 删除一个.
db.{collection}.deleteMany(): 删除多个.
db.{collection}.drop(): 删除集合.
db.{collection}.dropDatabase(): 删除数据库.
排序 sort
sort()
需要传递一个参数来指定排序规则:
- 1为升序
- -1为降序
limit skip
- limit: 限制返回的结果数
- skip: 跳过结果的数量
常用的操作符
$regex
$regex: 正则表达式, 可用于模糊查询.
shell
db.company.find({name: {$regex: /.*?wen/}})
运行结果是:
shell
yrx> db.company.find({name: {$regex: /.*?wen/}})
[
{
_id: ObjectId('69793ef52377fa2311816ff1'),
name: 'zhangwen',
age: 23,
gender: 'male'
},
{
_id: ObjectId('69793ef52377fa2311816ff2'),
name: 'liwenwen',
age: 25,
gender: 'female'
}
]
$exists
$exists: 某字段是否存在.
shell
db.company.find({is_deleted: {$exists: false}})
$in
$in: 包含.
shell
db.company.find({age: {$in: [20, 25]}})
and or
-
$and: 与.
-
$or: 或.
查找年龄是23岁, 并且名字是zhangwen, 或者年龄是25岁的.
shell
db.company.find({$or: [{$and: [{age: 23}, {name: 'zhangwen'}]}, {age: 25}]})
$inc
$inc: 值增加或者减少.
shell
db.company.update({name: 'zhangwen'}, {$inc: {age: -5}})
aggregate 聚合操作
新建一个集合: sales.
shell
db.sales.insertMany([
{ "_id" : 1, "item" : "铅笔", "price" : 5, "quantity" : 2, tags: ["blank", "red"], size:[10, 2, 3]},
{ "_id" : 2, "item" : "钢笔", "price" : 20, "quantity" : 2, tags: ["red", "blank"], size:[10, 3, 3]},
{ "_id" : 3, "item" : "作业本", "price" : 5, "quantity" : 10, tags: ["red", "blank", "plain"], size:[10, 12, 3]},
{ "_id" : 4, "item" : "铅笔", "price" : 8, "quantity" : 20, tags: ["blank", "red"], size:[10, 2, 4]},
{ "_id" : 5, "item" : "作业本", "price" : 10, "quantity" : 10, tags: ["blue"], size:[20, 10, 3]}
])
shell
$group == MySQL group by
_id: 对应想要分组的字段 # 注意和集合中主键进行区分
-
按照 item 字段进行分组,统计每个 item 下面的文档个数:
shelldb.sales.aggregate([{$group: {_id: '$item', count: {$sum: 1}}}])运行结果是:
shelltest> db.sales.aggregate([{$group: {_id: '$item', count: {$sum: 1}}}]) [ { _id: '钢笔', count: 1 }, { _id: '铅笔', count: 2 }, { _id: '作业本', count: 2 } ] -
按照 item 字段进行分组,统计每个 item 下面 quantity 的总数,price的平均数
shelltest> db.sales.aggregate([{$group: {_id: 'item', count: {$sum: '$quantity'}, price_avg: {$avg: '$price'}}}]) [ { _id: 'item', count: 44, price_avg: 9.6 } ] -
按照 item 字段进行分组,统计每个 item 下面 price的最大值与最小值.
shelltest> db.sales.aggregate([{$group: {_id: '$item', max_price: {$max: '$price'}, min_price: {$min: '$price'}}}]) [ { _id: '钢笔', max_price: 20, min_price: 20 }, { _id: '铅笔', max_price: 8, min_price: 5 }, { _id: '作业本', max_price: 10, min_price: 5 } ]
数组查询
-
tags值是只包含两个元素"red","blank"并且有指定顺序.
shelltest> db.sales.find({tags: ["red", "blank"]}) [ { _id: 2, item: '钢笔', price: 20, quantity: 2, tags: [ 'red', 'blank' ], size: [ 10, 3, 3 ] } ] -
tags值是只包含两个元素"red","blank"不指定顺序, 只要有这2个即可, 不关心数组中是否有其它元素.
shelltest> db.sales.find({tags: {$all: ["red", "blank"]}}) [ { _id: 1, item: '铅笔', price: 5, quantity: 2, tags: [ 'blank', 'red' ], size: [ 10, 2, 3 ] }, { _id: 2, item: '钢笔', price: 20, quantity: 2, tags: [ 'red', 'blank' ], size: [ 10, 3, 3 ] }, { _id: 3, item: '作业本', price: 5, quantity: 10, tags: [ 'red', 'blank', 'plain' ], size: [ 10, 12, 3 ] }, { _id: 4, item: '铅笔', price: 8, quantity: 20, tags: [ 'blank', 'red' ], size: [ 10, 2, 4 ] } ] -
tags中有一个元素的值是"red"的所有文档.
shelltest> db.sales.find({tags: 'red'}) [ { _id: 1, item: '铅笔', price: 5, quantity: 2, tags: [ 'blank', 'red' ], size: [ 10, 2, 3 ] }, { _id: 2, item: '钢笔', price: 20, quantity: 2, tags: [ 'red', 'blank' ], size: [ 10, 3, 3 ] }, { _id: 3, item: '作业本', price: 5, quantity: 10, tags: [ 'red', 'blank', 'plain' ], size: [ 10, 12, 3 ] }, { _id: 4, item: '铅笔', price: 8, quantity: 20, tags: [ 'blank', 'red' ], size: [ 10, 2, 4 ] } ] -
size中至少有一个大于10的.
shelltest> db.sales.find({size: {$gt: 10}}) [ { _id: 3, item: '作业本', price: 5, quantity: 10, tags: [ 'red', 'blank', 'plain' ], size: [ 10, 12, 3 ] }, { _id: 5, item: '作业本', price: 10, quantity: 10, tags: [ 'blue' ], size: [ 20, 10, 3 ] } ] -
size中的第二个必须大于10的.
shelltest> db.sales.find({'size.1': {$gt: 10}}) [ { _id: 3, item: '作业本', price: 5, quantity: 10, tags: [ 'red', 'blank', 'plain' ], size: [ 10, 12, 3 ] } ]
Python操作MongoDB
Python 操作MongoDB很简单, 语法和前面差不多:
python
import pymongo
"""
稍微封装一下.
"""
class Config:
mongo_params_local = {
'host': '127.0.0.1',
'port': 27017
}
class MongoBase:
def __init__(self, collection_name):
# 连接MongoDB
self.mongo_local = pymongo.MongoClient(**Config.mongo_params_local)
self.collection_name = collection_name
self.collection = None
def get_collection(self):
db = self.mongo_local.yrx
collection = db[self.collection_name]
self.collection = collection
def insert_data(self, data):
"""
插入数据
:param data: 需要插入的数据.
:return: none
"""
self.collection.insert_one(data)
def find_data(self, _id):
res = self.collection.find_one({'_id': _id})
return res
def update_data(self, _id, update_field):
"""根据主键id更新数据"""
self.collection.update_one({'_id': _id}, {'$set': update_field})
def __del__(self):
self.mongo_local.close()
print("mongo连接已关闭")
if __name__ == '__main__':
mongo_base = MongoBase('company')
继承上面实现的 MongoBase类:
python
import re
import time
import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
from loguru import logger
from _mongo_db import MongoBase
class YrxSpider(MongoBase):
def __init__(self, collection_name):
super(YrxSpider, self).__init__(collection_name)
self.start_url = "http://exam.python-spider.com/spider/search"
self.page_url = "http://exam.python-spider.com/spider/list"
self.detail_url = "http://exam.python-spider.com/spider/postDetail"
self.detail = 'http://exam.python-spider.com/spider/detail?href={}'
self.executor = ThreadPoolExecutor(8)
self.get_collection()
def get_list_data(self, page):
"""获取列表页的数据"""
response = requests.post(self.page_url, data={"page": page})
results = response.json()
return results
def get_detail_data(self, item):
"""获取详情页的数据"""
assert isinstance(item, dict), 'item必须为字典.'
detail_id = item.get('_id')
response = requests.post(self.detail_url, data={"href": detail_id})
item['detail_data'] = response.content.decode("unicode_escape")
item['url'] = self.detail.format(detail_id)
return item
def diff_field(self, item, res):
"""
比对字段.
:param item: 是我们刚抓到的, 是即将要插入数据库的数据.
:param res: 是从mongo里获取到的.
:return: 需要更新的字段.
"""
update_field = {}
for key in item.keys():
if key not in res.keys() or item[key] != res[key]:
# 如果进入到这个判断, 是不是就代表这个key是需要更新的.
update_field[key] = item[key]
return update_field
def main(self):
response = requests.get(self.start_url)
pages = re.search(r'pageNum: (\d+)', response.text).group(1)
all_tasks = [self.executor.submit(self.get_list_data, page + 1) for page in range(int(pages))]
for list_result in as_completed(all_tasks):
message = list_result.result().get('message')
detail_task_list = []
for msg in message:
item = dict()
item['title'] = msg.get('title')
item['_id'] = msg.get('href')
task = self.executor.submit(self.get_detail_data, item)
detail_task_list.append(task)
for detail_result in as_completed(detail_task_list):
data = detail_result.result()
_id = data.get('_id')
res = self.find_data(_id)
if res:
# 如果数据进了这个判断, 就代表要么数据有更新, 要么就是重复数据.
# 要处理数据有更新的情况和重复数据的情况.
# data: 是我们刚抓到的, 是即将要插入数据库的数据.
# res: 是从mongo里获取到的.
update_field = self.diff_field(data, res)
if update_field:
# 如果有就更新.
update_field['update_time'] = datetime.datetime.now()
self.update_data(_id, update_field)
logger.success(f'{_id}: 数据更新成功.')
else:
logger.warning(f'{_id}: 是重复数据.')
else:
data['create_time'] = datetime.datetime.now()
data['update_time'] = datetime.datetime.now()
self.insert_data(data)
logger.success(f'正在插入{data.get("_id")}')
if __name__ == '__main__':
start = time.perf_counter()
YrxSpider('project').main() # 新建一张表并将获取的数据插入其中
print(time.perf_counter() - start)
|----------------------|
| 遇见安然遇见你,不负代码不负卿。 |
| 谢谢老铁的时间,咱们下篇再见~ |