比Python,JavaScript才是更适合写爬虫的语言。原因有如下三个方面:
一、任务:爬取用户在Github上的repo信息
通过实例的方式学习爬虫是最好的方法,先定一个小目标:爬取github repo信息。入口URL如下,我们只需要一直点击next按钮就能够遍历到用户的所有repo。
https://github.com/{{username}}?tab=repositories
获取repo之后,可以做什么?
二、爬虫双股剑:axios和jQuery
axios是JavaScript中很常用的异步网络请求库,相比jQuery,它更轻量、更专业。既能够用于浏览器端,也可以用于Node。它的语法风格是promise形式的。在本任务中,只需要了解如下用法就足够了:
axios.get(url).then((resp) => { 请求成功,处理resp.data中的html数据 }).catch((err) => { 请求失败,错误处理 })
请求之后需要处理回复结果,处理回复结果的库当然是用jQuery。实际上,我们有更好的选择:cheerio。
在node下,使用jQuery,需要使用jsdom库模拟一个window对象,这种方法效率较低,四个字形容就是:笨重稳妥。
如下代码使用jQuery解析haha.html文件
fs = require("fs") jquery=require('jquery') jsdom=require('jsdom') //fs.readFileSync()返回结果是一个buffer,相当于byte[] html = fs.readFileSync('haha.html').toString('utf8') dom= new jsdom.JSDOM(html) $=jquery(dom.window) console.log($('h1'))
cheerio只实现了jQuery中的DOM部分,相当于jQuery的一个子集。cheerio的语法和jQuery完全一致,在使用cheerio时,几乎感觉不到它和jQuery的差异。在解析HTML方面,毫无疑问,cheerio是更好的选择。如下代码使用cheerio解析haha.html文件。
cheerio=require('cheerio') html=require('fs').readFileSync("haha.html").toString('utf8') $=cheerio.load(html) console.log($('h1'))
只需20余行,便可实现简单的github爬虫,此爬虫只爬取了一页repo列表。
var axios = require("axios") var cheerio = require("cheerio") axios.get("https://github.com/weiyinfu?tab=repositories").then(resp => { var $ = cheerio.load(resp.data) var lis = $("#user-repositories-list li") var repos = [] for (var i = 0; i < lis.length; i++) { var li = lis.eq(i) var repo = { repoName: li.find("h3").text().trim(), repoUrl: li.find("h3 a").attr("href").trim(), repoDesc: li.find("p").text().trim(), language: li.find("[itemprop=programmingLanguage]").text().trim(), star: li.find(".muted-link.mr-3").eq(0).text().trim(), fork: li.find(".muted-link.mr-3").eq(1).text().trim(), forkedFrom: li.find(".f6.text-gray.mb-1 a").text().trim() } repos.push(repo) } console.log(repos) })
三、更丰富的功能
爬虫不是目的,而是达成目的的一种手段。获取数据也不是目的,从数据中提取统计信息并呈现给人才是最终目的。
在github爬虫的基础上,我们可以扩展出更加丰富的功能:使用echarts等图表展示结果。
要想让更多人使用此爬虫工具获取自己的github统计信息,就需要将做成一个网站的形式,通过搜索页面输入用户名,启动爬虫立即爬取github信息,然后使用echarts进行统计展示。网站肯定也要用js作为后端,这样才能和js爬虫无缝衔接,不然还要考虑跨语言调用。js后端有两大web框架express和koa,二者API非常相似,并无优劣之分,但express更加流行。
如上设计有一处用户体验不佳的地方:当启动爬虫爬取github信息时,用户可能需要等待好几秒,这个过程不能让用户干等着。一种解决思路是:让用户看到爬虫爬取的进度或者爬取过程。可以通过websocket向用户推送爬取过程信息并在前端进行展示。展示时,使用类似控制台的界面进行展示。
如何存储爬取到的数据呢?使用MongoDB或者文件都可以,最好实现两种存储方式,让系统的存储方式变得可配置。使用MongoDB时,用到js中的连接池框架generic-pool。
整个项目用到的库包括:
试用地址:
https://weiyinfu.cn/githubstatistic/search.html
案例地址:https://github.com/weiyinfu/GithubStatistic
原文链接:https://zhuanlan.zhihu.com/p/53763115
oSQL,全称 Not Only SQL,意为不仅仅是 SQL,泛指非关系型数据库。NoSQL 是基于键值对的,而且不需要经过 SQL 层的解析,数据之间没有耦合性,性能非常高。
非关系型数据库又可细分如下:
对于爬虫的数据存储来说,一条数据可能存在某些字段提取失败而缺失的情况,而且数据可能随时调整。另外,数据之间还存在嵌套关系。如果使用关系型数据库存储,一是需要提前建表,二是如果存在数据嵌套关系的话,需要进行序列化操作才可以存储,这非常不方便。如果用了非关系型数据库,就可以避免一些麻烦,更简单、高效。
本节中,我们主要介绍 MongoDB 存储操作。
MongoDB 是由 C++ 语言编写的非关系型数据库,是一个基于分布式文件存储的开源数据库系统,其内容存储形式类似 JSON 对象,它的字段值可以包含其他文档、数组及文档数组,非常灵活。在这一节中,我们就来看看 Python 3 下 MongoDB 的存储操作。
在开始之前,请确保已经安装好了 MongoDB 并启动了其服务,安装方式可以参考:https://setup.scrape.center/mongodb。
除了安装好 MongoDB 数据库,我们还需要安装好 Python 的 PyMongo 库,如尚未安装,可以使用 pip3 来安装:
pip3 install pymongo
更详细的安装说明可以参考:https://setup.scrape.center/pymongo。
安装好 MongoDB 数据库和 PyMongo 库之后,我们便可以开始本节的学习了。
连接 MongoDB 时,我们需要使用 PyMongo 库里面的 MongoClient。一般来说,传入 MongoDB 的 IP 及端口即可,其中第一个参数为地址 host,第二个参数为端口 port(如果不给它传递参数,默认是 27017):
import pymongo
client = pymongo.MongoClient(host='localhost', port=27017)
这样就可以创建 MongoDB 的连接对象了。
另外,MongoClient 的第一个参数 host 还可以直接传入 MongoDB 的连接字符串,它以 mongodb 开头,例如:
client = MongoClient('mongodb://localhost:27017/')
这也可以达到同样的连接效果。
在 MongoDB 中,可以建立多个数据库,接下来我们需要指定操作哪个数据库。这里我们以 test 数据库为例来说明,下一步需要在程序中指定要使用的数据库:
db = client.test
这里调用 client 的 test 属性即可返回 test 数据库。当然,我们也可以这样指定:
db = client['test']
这两种方式是等价的。
MongoDB 的每个数据库又包含许多集合(collection),它们类似于关系型数据库中的表。
下一步需要指定要操作的集合,这里指定一个集合名称为 students。与指定数据库类似,指定集合也有两种方式:
collection = db.students
collection = db['students']
这样我们便声明了一个集合对象。
接下来,便可以插入数据了。对于 students 这个集合,新建一条学生数据,这条数据以字典形式表示:
student = {
'id': '20170101',
'name': 'Jordan',
'age': 20,
'gender': 'male'
}
这里指定了学生的学号、姓名、年龄和性别。接下来,直接调用 collection 的 insert 方法即可插入数据,代码如下:
result = collection.insert(student)
print(result)
在 MongoDB 中,每条数据其实都有一个 _id 属性来唯一标识。如果没有显式指明该属性,MongoDB 会自动产生一个 ObjectId 类型的 _id 属性。insert 方法会在执行后返回 _id 值。
运行结果如下:
5932a68615c2606814c91f3d
当然,我们也可以同时插入多条数据,只需要以列表形式传递即可,示例如下:
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 的集合:
[ObjectId('5932a80115c2606a59e8a048'), ObjectId('5932a80115c2606a59e8a049')]
实际上,在 PyMongo 3.x 版本中,官方已经不推荐使用 insert 方法了。当然,继续使用也没有什么问题。官方推荐使用 insert_one 和 insert_many 方法来分别插入单条记录和多条记录,示例如下:
student = {
'id': '20170101',
'name': 'Jordan',
'age': 20,
'gender': 'male'
}
result = collection.insert_one(student)
print(result)
print(result.inserted_id)
运行结果如下:
<pymongo.results.InsertOneResult object at 0x10d68b558>
5932ab0f15c2606f0c1cf6c5
与 insert 方法不同,这次返回的是 InsertOneResult 对象,我们可以调用其 inserted_id 属性获取 _id。
对于 insert_many 方法,我们可以将数据以列表形式传递,示例如下:
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)
运行结果如下:
<pymongo.results.InsertManyResult object at 0x101dea558>
[ObjectId('5932abf415c2607083d3b2ac'), ObjectId('5932abf415c2607083d3b2ad')]
该方法返回的是 InsertManyResult 类型的对象,调用 inserted_ids 属性可以获取插入数据的 _id 列表。
插入数据后,我们可以利用 find_one 或 find 方法进行查询,其中 find_one 查询得到的是单个结果,find 则返回一个生成器对象。示例如下:
result = collection.find_one({'name': 'Mike'})
print(type(result))
print(result)
这里我们查询 name 为 Mike 的数据,它的返回结果是字典类型,运行结果如下:
<class 'dict'>
{'_id': ObjectId('5932a80115c2606a59e8a049'), 'id': '20170202', 'name': 'Mike', 'age': 21, 'gender': 'male'}
可以发现,它多了 _id 属性,这就是 MongoDB 在插入过程中自动添加的。
此外,我们也可以根据 ObjectId 来查询,此时需要使用 bson 库里面的 objectid:
from bson.objectid import ObjectId
result = collection.find_one({'_id': ObjectId('593278c115c2602667ec6bae')})
print(result)
其查询结果依然是字典类型,具体如下:
{'_id': ObjectId('593278c115c2602667ec6bae'), 'id': '20170101', 'name': 'Jordan', 'age': 20, 'gender': 'male'}
当然,如果查询结果不存在,则会返回 None。
对于多条数据的查询,我们可以使用 find 方法。例如,这里查找年龄为 20 的数据,示例如下:
results = collection.find({'age': 20})
print(results)
for result in results:
print(result)
运行结果如下:
<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 的数据,则写法如下:
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 开头的学生数据,示例如下:
results = collection.find({'name': {'$regex': '^M.*'}})
这里使用 $regex 来指定正则匹配,^M.* 代表以 M 开头的正则表达式。
这里将一些功能符号再归类为下表。
符 号 | 含 义 | 示 例 | 示例含义 |
$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/。
要统计查询结果有多少条数据,可以调用 count 方法。比如,统计所有数据条数:
count = collection.find().count()
print(count)
或者统计符合某个条件的数据:
count = collection.find({'age': 20}).count()
print(count)
运行结果是一个数值,即符合条件的数据条数。
排序时,直接调用 sort 方法,并在其中传入排序的字段及升降序标志即可。示例如下:
results = collection.find().sort('name', pymongo.ASCENDING)
print([result['name'] for result in results])
运行结果如下:
['Harden', 'Jordan', 'Kevin', 'Mark', 'Mike']
这里我们调用 pymongo.ASCENDING 指定升序。如果要降序排列,可以传入 pymongo.DESCENDING。
在某些情况下,我们可能想只取某几个元素,这时可以利用 skip 方法偏移几个位置,比如偏移 2,就忽略前两个元素,得到第三个及以后的元素:
results = collection.find().sort('name', pymongo.ASCENDING).skip(2)
print([result['name'] for result in results])
运行结果如下:
['Kevin', 'Mark', 'Mike']
另外,还可以用 limit 方法指定要取的结果个数,示例如下:
results = collection.find().sort('name', pymongo.ASCENDING).skip(2).limit(2)
print([result['name'] for result in results])
运行结果如下:
['Kevin', 'Mark']
如果不使用 limit 方法,原本会返回三个结果,加了限制后,会截取两个结果返回。
值得注意的是,在数据库数量非常庞大的时候,如千万、亿级别,最好不要使用大的偏移量来查询数据,因为这样很可能导致内存溢出。此时可以使用类似如下操作来查询:
from bson.objectid import ObjectId
collection.find({'_id': {'$gt': ObjectId('593278c815c2602678bb2b8d')}})
这时需要记录好上次查询的 _id。
对于数据更新,我们可以使用 update 方法,指定更新的条件和更新后的数据即可。例如:
condition = {'name': 'Kevin'}
student = collection.find_one(condition)
student['age'] = 25
result = collection.update(condition, student)
print(result)
这里我们要更新 name 为 Kevin 的数据的年龄:首先指定查询条件,然后将数据查询出来,修改年龄后调用 update 方法将原条件和修改后的数据传入。
运行结果如下:
{'ok': 1, 'nModified': 1, 'n': 1, 'updatedExisting': True}
返回结果是字典形式,ok 代表执行成功,nModified 代表影响的数据条数。
另外,我们也可以使用 $set 操作符对数据进行更新,代码如下:
result = collection.update(condition, {'$set': student})
这样可以只更新 student 字典内存在的字段。如果原先还有其他字段,则不会更新,也不会删除。而如果不用 $set 的话,则会把之前的数据全部用 student 字典替换;如果原本存在其他字段,则会被删除。
另外,update 方法其实也是官方不推荐使用的方法。这里也分为 update_one 方法和 update_many 方法,用法更加严格,它们的第二个参数需要使用 $ 类型操作符作为字典的键名,示例如下:
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 属性,获得匹配的数据条数和影响的数据条数。
运行结果如下:
<pymongo.results.UpdateResult object at 0x10d17b678>
1 0
我们再看一个例子:
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。
运行结果如下:
<pymongo.results.UpdateResult object at 0x10b8874c8>
1 1
可以看到匹配条数为 1 条,影响条数也为 1 条。
如果调用 update_many 方法,则会将所有符合条件的数据都更新,示例如下:
condition = {'age': {'$gt': 20}}
result = collection.update_many(condition, {'$inc': {'age': 1}})
print(result)
print(result.matched_count, result.modified_count)
这时匹配条数就不再为 1 条了,运行结果如下:
<pymongo.results.UpdateResult object at 0x10c6384c8>
3 3
可以看到,这时所有匹配到的数据都会被更新。
删除操作比较简单,直接调用 remove 方法指定删除的条件即可,此时符合条件的所有数据均会被删除。示例如下:
result = collection.remove({'name': 'Kevin'})
print(result)
运行结果如下:
{'ok': 1, 'n': 1}
另外,这里依然存在两个新的推荐方法 —— delete_one 和 delete_many。示例如下:
result = collection.delete_one({'name': 'Kevin'})
print(result)
print(result.deleted_count)
result = collection.delete_many({'age': {'$lt': 25}})
print(result.deleted_count)
运行结果如下:
<pymongo.results.DeleteResult object at 0x10e6ba4c8>
1
4
delete_one 即删除第一条符合条件的数据,delete_many 即删除所有符合条件的数据。它们的返回结果都是 DeleteResult 类型,可以调用 deleted_count 属性获取删除的数据条数。
另外,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 进行数据增删改查的方法,后面我们会在实战案例中应用这些操作进行数据存储。
本节代码:https://github.com/Python3WebSpider/MongoDBTest。
MongoDB(来自于英文单词“Humongous”,中文含义为“庞大”)是可以应用于各种规模的企业、各个行业以及各类应用程序的开源数据库。作为一个适用于敏捷开发的数据库,MongoDB 的数据模式可以随着应用程序的发展而灵活地更新。与此同时,它也为开发人员 提供了传统数据库的功能:二级索引,完整的查询系统以及严格一致性等等。MongoDB 能够使企业更加具有敏捷性和可扩展性,各种规模的企业都可以通过使用 MongoDB 来创建新的应用,提高与客户之间的工作效率,加快产品上市时间,以及降低企业成本。
MongoDB 是专为可扩展性,高性能和高可用性而设计的数据库。它可以从单服务器部署扩展到大型、复杂的多数据中心架构。利用内存计算的优势,MongoDB 能够提供高性能的数据读写操作。MongoDB 的本地复制和自动故障转移功能使您的应用程序具有企业级的可靠性和操作灵活性。
以上内容摘自官网:
简而言之,MongoDB是一个免费开源跨平台的 NoSQL 数据库,与关系型数据库不同,MongoDB 的数据以类似于 JSON 格式的二进制文档存储:
{
name: "jack",
age: 22,
}
文档型的数据存储方式有几个重要好处:
可以使用我们熟悉的 MySQL 数据库来加以对比:
MySQL 基础概念MongoDB 对应概念数据库(database)容器(database)表(table)集合(collection)行(row)文档(document)列(column)域(filed)索引(index)索引(index)
也借用一下菜鸟教程)的图来更加形象生动的说明一下:
这很容易理解,但是问题在于:我们为什么要引入新的概念呢?(也就是为什么我们要把“表”替换成“集合”,“行”替换成“文档”,“列”替换成“域”呢?)原因在于,其实在 MySQL 这样的典型关系型数据中,我们是在定义表的时候定义列的,但是由于上述文档型数据库的特点,它允许文档的数据类型可以对应到语言的数据类型,所以我们是在定义文档的时候才会定义域的。
也就是说,集合中的每个文档都可以有独立的域。因此,虽说集合相对于表来说是一个简化了的容器,而文档则包含了比行要多得多的信息。
怎么样都好,搭建好环境就行,这里以 OS 环境为例,你可以使用 OSX 的 brew 安装 mongodb:
brew install mongodb
在运行之前我们需要创建一个数据库存储目录 /data/db:
sudo mkdir -p /data/db
然后启动 mongodb,默认数据库目录即为 /data/db(如果不是,可以使用 --dbpath 指令来指定):
sudo mongd
过一会儿你就能看到你的 mongodb 运行起来的提示:
具体的搭建过程可以参考菜鸟的教程:http://www.runoob.com/mongodb/mongodb-window-install.html
通过上面的步骤我们在系统里运行了一个 mongodb 实例,接下来通过 mongo 命令来连接它:
mongo [options] [db address] [file names]
由于上面运行的 mongodb 运行在 27017 端口,并且灭有启动安全模式,所以我们也不需要输入用户名和密码就可以直接连接:
mongo 127.0.0.1:27017
或者通过 --host 和 --port 选项指定主机和端口。一切顺利的话,就进入了 mongoDB shell,shell 会报出一连串权限警告,不过不用担心,这并不会影响之后的操作。在添加授权用户和开启认证后,这些警告会自动消失。
在进行增删改查操作之前,我们需要先了解一下常用的 shell 命令:
mongoDB 预设有两个数据库,admin 和 local,admin 用来存放系统数据,local 用来存放该实例数据,在副本集中,一个实例的 local 数据库对于其它实例是不可见的。使用 use 命令切换数据库:
> use admin
> use local
> use newDatabase
可以 use 一个不存在的数据库,当你存入新数据时,mongoDB 会创建这个数据库:
> use newDatabase
> db.newCollection.insert({x:1})
WriteResult({ "nInserted" : 1 })
以上命令向数据库中插入一个文档,返回 1 表示插入成功,mongoDB 自动创建 newCollection 集合和数据库 newDatabase。下面将对增查改删操作进行一个简单的演示。
MongoDB 提供 insert 方法创建新文档:
我们接着在刚才新创建的 newDatabase 下面新增数据吧:
db.newCollection.insert({name:"wmyskxz",age:22})
根据以往经验应该会觉得蛮奇怪的,因为之前在这个集合中插入的数据格式是 {x:1} 的,而这里新增的数据格式确是 {name:"wmyskxz",age:22} 这个样子的。还记得吗,文档型数据库的与传统型的关系型数据的区别就是在这里!
并且要注意,age:22 和 age:"22" 是不一样的哦,前者插入的是一个数值,而后者是字符串,我们可以通过 db.newCollection.find() 命令查看到刚刚插入的文档:
> db.newCollection.find()
{ "_id" : ObjectId("5cc1026533907ae66490e46c"), "x" : 1 }
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 22 }
这里有一个神奇的返回,那就是多了一个叫做 _id 的东西,这是 MongoDB 为你自动添加的字段,你也可以自己生成。大部分情况下还是会让 MongoDB 为我们生成,而且默认情况下,该字段是被加上了索引的。
MongoDB 提供 find 方法查找文档,第一个参数为查询条件:
> db.newCollection.find() # 查找所有文档
{ "_id" : ObjectId("5cc1026533907ae66490e46c"), "x" : 1 }
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 22 }
> db.newCollection.find({name:"wmyskxz"}) # 查找 name 为 wmyskxz 的文档
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 22 }
> db.newCollection.find({age:{$gt:20}}) # 查找 age 大于 20 的文档
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 22 }
上述代码中的$gt对应于大于号>的转义。
第二个参数可以传入投影文档映射数据:
> db.newCollection.find({age:{$gt:20}},{name:1})
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz" }
上述命令将查找 age 大于 20 的文档,返回 name 字段,排除其他字段。投影文档中字段为 1 或其他真值表示包含,0 或假值表示排除,可以设置多个字段位为 1 或 0,但不能混合使用。
为了测试,我们为这个集合弄了一些奇奇怪怪的数据:
> db.newCollection.find()
{ "_id" : ObjectId("5cc1026533907ae66490e46c"), "x" : 1 }
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 22 }
{ "_id" : ObjectId("5cc108fb33907ae66490e46e"), "name" : "wmyskxz-test", "age" : 22, "x" : 1, "y" : 30 }
然后再来测试:
> db.newCollection.find({age:{$gt:20}},{name:1,x:1})
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz" }
{ "_id" : ObjectId("5cc108fb33907ae66490e46e"), "name" : "wmyskxz-test", "x" : 1 }
> db.newCollection.find({age:{$gt:20}},{name:0,x:0})
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "age" : 22 }
{ "_id" : ObjectId("5cc108fb33907ae66490e46e"), "age" : 22, "y" : 30 }
> db.newCollection.find({age:{$gt:20}},{name:0,x:1})
Error: error: {
"ok" : 0,
"errmsg" : "Projection cannot have a mix of inclusion and exclusion.",
"code" : 2,
"codeName" : "BadValue"
}
从上面的命令我们就可以把我们的一些想法和上面的结论得以验证,perfect!
除此之外,还可以通过 count、skip、limit 等指针(Cursor)方法,改变文档查询的执行方式:
> db.newCollection.find().count()
3
> db.newCollection.find().skip(1).limit(10).sort({age:1})
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 22 }
{ "_id" : ObjectId("5cc108fb33907ae66490e46e"), "name" : "wmyskxz-test", "age" : 22, "x" : 1, "y" : 30 }
上述查找命令跳过 1 个文档,限制输出 10 个,以 age 子段正序排序(大于 0 为正序,小于 0 位反序)输出结果。最后,可以使用 Cursor 方法中的 pretty 方法,提升查询文档的易读性,特别是在查看嵌套的文档和配置文件的时候:
> db.newCollection.find().pretty()
{ "_id" : ObjectId("5cc1026533907ae66490e46c"), "x" : 1 }
{
"_id" : ObjectId("5cc102fb33907ae66490e46d"),
"name" : "wmyskxz",
"age" : 22
}
{
"_id" : ObjectId("5cc108fb33907ae66490e46e"),
"name" : "wmyskxz-test",
"age" : 22,
"x" : 1,
"y" : 30
}
MongoDB 提供 update 方法更新文档:
以 update() 方法为例。其格式:
> db.collection.update(
<query>,
<update>,
{
upsert: <boolean>,
multi: <boolean>
}
)
各参数意义:
下面我们测试把 name 字段为 wmyskxz 的文档更新一下试试:
> db.newCollection.update({name:"wmyskxz"},{name:"wmyskxz",age:30})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
要注意的是,如果更新文档只传入 age 字段,那么文档会被更新为{age: 30},而不是{name:"wmyskxz", age:30}。要避免文档被覆盖,需要用到 $set 指令,$set 仅替换或添加指定字段:
> db.newCollection.update({name:"wmyskxz"},{$set:{age:30}})
如果要在查询的文档不存在的时候插入文档,要把 upsert 参数设置真值:
> db.newCollection.update({name:"wmyskxz11"},{$set:{age:30}},{upsert:true})
update 方法默认情况只更新一个文档,如果要更新符合条件的所有文档,要把 multi 设为真值,并使用 $set 指令:
> db.newCollection.update({age:{$gt:20}},{$set:{test:"A"}},{multi:true})
WriteResult({ "nMatched" : 3, "nUpserted" : 0, "nModified" : 3 })
> db.newCollection.find()
{ "_id" : ObjectId("5cc1026533907ae66490e46c"), "x" : 1 }
{ "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 30, "test" : "A" }
{ "_id" : ObjectId("5cc108fb33907ae66490e46e"), "name" : "wmyskxz-test", "age" : 22, "x" : 1, "y" : 30, "test" : "A" }
{ "_id" : ObjectId("5cc110148d0a578f03d43e81"), "name" : "wmyskxz11", "age" : 30, "test" : "A" }
MongoDB 提供了 delete 方法删除文档:
以 remove 方法为例:
> db.newCollection.remove({name:"wmyskxz11"})
> db.newCollection.remove({age:{$gt:20}},{justOne:true})
> db.newCollection.find()
{ "_id" : ObjectId("5cc1026533907ae66490e46c"), "x" : 1 }
{ "_id" : ObjectId("5cc108fb33907ae66490e46e"), "name" : "wmyskxz-test", "age" : 22, "x" : 1, "y" : 30, "test" : "A" }
MongoDB 提供了 drop 方法删除集合,返回 true 表面删除集合成功:
> db.newCollection.drop()
相比传统关系型数据库,MongoDB 的 CURD 操作更像是编写程序,更符合开发人员的直觉,不过 MongoDB 同样也支持 SQL 语言。MongoDB 的 CURD 引擎配合索引技术、数据聚合技术和 JavaScript 引擎,赋予 MongoDB 用户更强大的操纵数据的能力。
参考文章:简明 MongoDB 入门教程 -
https://segmentfault.com/a/1190000010556670
前置申明:这一部分基于以下链接整理 https://github.com/justinyhuang/the-little-mongodb-book-cn/blob/master/mongodb.md#%E8%AE%B8%E5%8F%AF%E8%AF%81
这是一个抽象的话题,与大多数NoSQL方案相比,在建模方面,面向文档的数据库算是和关系数据库相差最小的。这些差别是很小,但是并不是说不重要。
您要接受的第一个也是最基本的一个差别,就是 MongoDB 没有连接(join)。我不知道MongoDB不支持某些类型连接句法的具体原因,但是我知道一般而言人们认为连接是不可扩展的。也就是说,一旦开始横向分割数据,最终不可避免的就是在客户端(应用程序服务器)使用连接。且不论MongoDB为什么不支持连接,事实是数据是有关系的,可是MongoDB不支持连接。(译者:这里的关系指的是不同的数据之间是有关联的,对于没有关系的数据,就完全不需要连接。)
为了在没有连接的MongoDB中生存下去,在没有其他帮助的情况下,我们必须在自己的应用程序中实现连接。
基本上我们需要用第二次查询去找到相关的数据。找到并组织这些数据相当于在关系数据库中声明一个外来的键。现在先别管什么独角兽了,我们来看看我们的员工。首先我们创建一个员工的数据(这次我告诉您具体的_id值,这样我们的例子就是一样的了):
db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d730"), name: 'Leto'})
然后我们再加入几个员工并把 Leto 设成他们的老板:
db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d731"), name: 'Duncan', manager: ObjectId("4d85c7039ab0fd70a117d730")});
db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d732"), name: 'Moneo', manager: ObjectId("4d85c7039ab0fd70a117d730")});
(有必要再强调一下,_id可以是任何的唯一的值。在实际工作中你很可能会用到ObjectId, 所以我们在这里也使用它)
显然,要找到Leto的所有员工,只要执行:
db.employees.find({manager: ObjectId("4d85c7039ab0fd70a117d730")})
没什么了不起的。在最糟糕的情况下,为弥补连接的缺失需要做的只是再多查询一次而已,该查询很可能是经过索引了的。
MongoDB 没有连接并不意味着它没有其他的优势。还记得我们曾说过 MongoDB 支持数组并把它当成文档中的一级对象吗?当处理多对一或是多对多关系的时候,这一特性就显得非常好用了。用一个简单的例子来说明,如果一个员工有两个经理,我们可以把这个关系储存在一个数组当中:
({name: 'Siona', manager: [ObjectId("4d85c7039ab0fd70a117d730"), ObjectId("4d85c7039ab0fd70a117d732")] })
需要注意的是,在这种情况下,有些文档中的 manager 可能是一个向量,而其他的却是数组。在两种情况下,前面的 find 还是一样可以工作:
db.employees.find({manager: ObjectId("4d85c7039ab0fd70a117d730")})
很快您就会发现数组中的值比起多对多的连接表(join-table)来说要更容易处理。
除了数组,MongoDB 还支持嵌入文档。尝试插入含有内嵌文档的文档,像这样:
db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d734"), name: 'Ghanima', family: {mother: 'Chani', father: 'Paul', brother: ObjectId("4d85c7039ab0fd70a117d730")}})
也许您会这样想,确实也可以这样做:嵌入文档可以用‘.’符号来查询:
db.employees.find({'family.mother': 'Chani'})
就这样,我们简要地介绍了嵌入文档适用的场合以及您应该怎样使用它。
MongoDB 支持一个叫做 DBRef 的功能,许多 MongoDB 的驱动都提供对这一功能的支持。当驱动遇到一个 DBRef 时它会把当中引用的文档读取出来。DBRef 包含了所引用的文档的 ID 和所在的集合。它通常专门用于这样的场合:相同集合中的文档需要引用另外一个集合中的不同文档。例如,文档 1 的 DBRef 可能指向 managers 中的文档,而文档 2 中的 DBRef 可能指向 employees 中的文档。
代替连接的另一种方法就是反规范化数据。在过去,反规范化是为性能敏感代码所设,或者是需要数据快照(例如审计日志)的时候才应用的。然而,随着NoSQL的日渐普及,有许多这样的数据库并不提供连接操作,于是作为规范建模的一部分,反规范化就越来越常见了。这样说并不是说您就需要为每个文档中的每一条信息创建副本。与此相反,与其在设计的时候被复制数据的担忧牵着走,还不如按照不同的信息应该归属于相应的文档这一思路来对数据建模。
比如说,假设您在编写一个论坛的应用程序。把一个 user 和一篇 post 关联起来的传统方法是在 posts 中加入一个 userid 的列。这样的模型中,如果要显示 posts 就不得不读取(连接)users。一种简单可行的替代方案就是直接把 name 和 userid 存储在 post中。您甚至可以用嵌入文档来实现,比如说 user: {id: ObjectId('Something'), name: 'Leto'}。当然,如果允许用户更改他们的用户名,那么每当有用户名修改的时候,您就需要去更新所有的文档了(这需要一个额外的查询)。
对一些人来说改用这种方法并非易事。甚至在一些情况下根本行不通。不过别不敢去尝试这种方法:有时候它不仅可行,而且就是正确的方法。
当处理一对多或是多对多问题的时候,采用id数组往往都是正确的策略。可以这么说,DBRef并不是那么常用,虽然您完全可以试着采用这项技术。这使得新手们在面临选择嵌入文档还是手工引用(manual reference)时犹豫不决。
首先,要知道目前一个单独的文档的大小限制是 4MB,虽然已经比较大了。了解了这个限制可以为如何使用文档提供一些思路。目前看来多数的开发者还是大量地依赖手工引用来维护数据的关系。嵌入文档经常被使用,but mostly for small pieces of data which we want to always pull with the parent document。一个真实的例子,我把 accounts 文档嵌入存储在用户的文档中,就像这样:
db.users.insert({name: 'leto', email: 'leto@dune.gov', account: {allowed_gholas: 5, spice_ration: 10}})
这不是说您就应该低估嵌入文档的作用,也不是说应该把它当成是鲜少用到的工具并直接忽略。将数据模型直接映射到目标对象上可以使问题变得更加简单,也往往因此而不再需要连接操作。当您知道 MongoDB 允许对嵌入文档的域进行查询并做索引后,这个说法就尤其显得正确了。
既然集合不强制使用模式,那么就完全有可能用一个单一的集合以及一个不匹配的文档构建一个系统。以我所见过的情况,大部分的 MongoDB 系统都像您在关系数据库中所见到的那样布局。换句话说,如果在关系数据库中会用表,那么很有可能在 MongoDB 中就要用集合(多对多连接表在这里是一个不可忽视的例外)
当把嵌入文档引进来的时候,讨论就会变得更加有意思了。最常见的例子就是博客系统。是应该分别维护 posts 和 comments 两个集合,还是在每个 post 中嵌入一个 comments 数组?暂且不考虑那个 4MB 的限制(哈姆雷特所有的评论也不超过200KB,谁的博客会比他更受欢迎?),大多数的开发者还是倾向于把数据划分开。因为这样既简洁又明确。
没有什么硬性的规定(呃,除了 4MB 的限制)。做了不同的尝试之后您就可以凭感觉知道怎样做是对的了。
至此已经对 MongoDB 有了一个基本的了解和入门,但是要运用在实际的项目中仍然有许多实践需要自己去完成
- END -
*请认真填写需求信息,我们会在24小时内与您取得联系。