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

非关系型数据库细分如下:
键值存储数据库:代表有Redis,Voldemort和Oracle BDB等。
列存储数据库:代表有Cassandra,HBase和Riak等。
文档型数据库:代表有CouchDB和MongoDB等。
图形数据库:代表有:Neo4J,InfoGrid和Infinite Graph等。

爬取一条数据可能存在某些字段提取失败而缺失的情况,而且数据可能随时调整。另外,数据之间还存在嵌套关系。如果用关系型数据库存储,一是需要提前建表,二是如果存在数据嵌套关系的话,需要进行序列化操作才可以存储,这非常不方便。用非关系型数据库,可以避免一些麻烦,更简单高效。下面重点介绍MongoDB和Redis数据存储操作。

(一) MongoDB存储
MongoDB是由C++编写的非关系型数据库,是一个基于分布式文件存储的开源数据库系统,其内容存储形式类似JSON对象,它的字段值可以包含其他文档、数组及文档数组,非常灵活。

在使用之前需安装好MongoDB并启动其服务,并且要安装Python的PyMongo库。
安装PyMongo库:pip3 install pymongo
PyMongo库下载:https://pypi.python.org/pypi/pymongo
MongoDB源码安装方式参考:https://zhuanlan.zhihu.com/p/50240932
MongoDB配置文件db目录设置参考:https://www.jianshu.com/p/938539b2a9d8

1 连接MongoDB
使用pymongo库的MongoClient()方法连接MongoDB,第一个参数是IP地址Host,第二个参数是端口Port,端口默认是27107。例如下面所示:
import pymongo
client = pymongo.MongoClient(host='localhost', port=27107)
运行上面代码就创建MongoDB连接对象。其中host参数还可以传入连接字符串,以mongodb开头,例如:
client = pymongo.MongoClient('mongodb://michael:michael123@localhost:27017/')

2 指定数据库
MongoDB可以创建多个数据库,在使用时要指定数据库,这里以test数据库为例:
db = client.test
上面代码是调用client的test属性可返回数据库,还可以使用下面这种方式:
db = client['test']

3 指定集合
每个数据库包含很多集合(collection),类似关系型数据库中的表。
collection = db.students
collection = db['students']

4 插入数据
在students这个集合中插入数据,数据以字典形式表示:
student = {
'id': '20170101',
'name': 'Jordan',
'age': 20,
'gender': 'mail',
}
直接调用collection.insert()方法插入数据,如下所示:
result = collection.insert(student)
print(result) # 插入成功则输出ID值:
5c6fb47bf3721d3445bd09bf

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

插入数据时可以同时插入多条数据,参数以列表形式传递,如下所示:
from pymongo import MongoClient
host = 'mongodb://michael:michael123@localhost:27017/'
client = MongoClient(host) # 连接Mongodb
db = client.test # 指定数据库
collection = db.students # 指定集合
student1 = {
'id': '20170101',
'name': 'Jordan',
'age': 20,
'gender': 'male',
}
student2 = {
'id': '20170102',
'name': 'Mike',
'age': 21,
'gender': 'male',
}
result = collection.insert([student1, student2]) # 插入多条数据,以列表形式指定参数
print(result)

输出信息如下,可以看出返回结果是对应的_id的集合:
[ObjectId('5c734026f3721d0f4f0be288'), ObjectId('5c734026f3721d0f4f0be289')]

在PyMongo 3.x版本中,不建议使用insert()方法,面是推荐使用insert_one()和insert_many()方法来分别插入单条和多条记录,如下所示:
from pymongo import MongoClient
host = 'mongodb://michael:michael123@localhost:27017/'
client = MongoClient(host)
db = client.test
collection = db.students
result = collection.insert_one({"name":"michael", "age":20})
print(result) # 输出结果是InsertOneResult对象
print(result.inserted_id)

输出如下所示:
<pymongo.results.InsertOneResult object at 0x7f96bb317448>
5c7346baf3721d117c1a132f

这次的返回结果是InsertOneResult对象,与insert()方法返回的结果是不同的。可以调用inserted_id属性获取_id值。

对于insert_many()方法将数据以列表形式传递,获取_id值是调用属性inserted_ids,如下所示:
from pymongo import MongoClient
host = 'mongodb://michael:michael123@localhost:27017/'
client = MongoClient(host) # 连接Mongodb
db = client.test # 指定数据库
collection = db.students # 指定集合
student1 = {
'id': '20170101',
'name': 'Jordan',
'age': 20,
'gender': 'male',
}
student2 = {
'id': '20170102',
'name': 'Mike',
'age': 21,
'gender': 'male',
}
result = collection.insert_many([student1, student2]) # 用insert_many方法插入多条数据
print(result) # 输出结果是:InsertManyResult
print(result.inserted_ids) # 输出是列表

输出如下所示:
<pymongo.results.InsertManyResult object at 0x7f556f3bafc8>
[ObjectId('5c7347a3f3721d11d768a561'), ObjectId('5c7347a3f3721d11d768a562')]

5 查询
利用find_one()或find()方法进行查询,在进行查询前也需要先连接MongoDB,指定数据库,指定集合。find_one()方法查询的是单个结果,find()则返回一个生成器对象。如下所示:
from pymongo import MongoClient
host = 'mongodb://michael:michael123@localhost:27017/'
client = MongoClient(host) # 连接
db = client.test # 指定数据库
collection = db.students # 指定数据表(集合)
result = collection.find_one({'name':'Mike'}) # 利用find_one()方法查询
print(type(result)) # 字典类型
print(result) # 在结果中有_id属性

查询到的数据是字典类型,输出如下所示:
<class 'dict'>
{'_id': ObjectId('5c734026f3721d0f4f0be289'), 'id': '20170102', 'name': 'Mike', 'age': 21, 'gender': 'male'}

在输出结果中,多了_id属性,这是MongoDB在插入过程中自动添加的。也可以使用ObjectID进行查询,需要借助于bson库里面的objectid:
from pymongo import MongoClient
from bson.objectid import ObjectId
host = 'mongodb://michael:michael123@localhost:27017/'
client = MongoClient(host) # 连接
db = client.test # 指定数据库
collection = db.students # 指定数据表(集合)
result = collection.find_one({'_id':ObjectId('5c734026f3721d0f4f0be289')}) # 利用find_one()方法查询
print(result) # 在结果中有_id属性
此时输出结果与前面的一样。如果结果不存在,则返回None。

要查询多条数据,可使用find()方法,例如查找age为20为数据,示例如下:
from pymongo import MongoClient
from bson.objectid import ObjectId
host = 'mongodb://michael:michael123@localhost:27017/'
client = MongoClient(host) # 连接
db = client.test # 指定数据库
collection = db.students # 指定数据表(集合)
results = collection.find({'age': 20}) # 使用find()方法查找多条数据
print(results) # 类型是Cursor类型
for result in results:
print(result)

输出如下所示:
<pymongo.cursor.Cursor object at 0x7f3b7d4c44a8>
{'_id': ObjectId('5c733e96f3721d0ec2ec3357'), 'name': 'michael', 'age': 20}
{'_id': ObjectId('5c734026f3721d0f4f0be288'), 'id': '20170101', 'name': 'Jordan', 'age': 20, 'gender': 'male'}

输出可以看出,结果是Cursor类型,相当于一个生成器,需要遍历获取每个结果,其中的每个结果都是字典类型。如果要查询年龄大于20的数据,find()方法的写法如下所示:
results = collection.find({'age': {'$gt':20}})
查询条件的键值也不是单纯的数字,而是一个字典,其键名是比较符号$gt,其意是键值大于20。

比较符号归纳为下面的表5-3(表5-3 比较符号)
第五部分(三)     数据存储(非关系型数据库存储:MongoDB存储、Redis存储)

除了用比较符号查询外,还可以用正则匹配查询。例如,查询name字段以M开头的数据,find()方法可改为:
results = collection.find({'name': {'$regex': '^M.*'}})
这里使用$regex指定正则匹配,^M.*代表以M开头的正则表达式。

正则表达式是功能符号,功能符号归类为表5-4(表5-4 功能符号)
第五部分(三)     数据存储(非关系型数据库存储:MongoDB存储、Redis存储)

这些操作的详细用法,参考MongoDB官方文档:
https://docs.mongodb.com/manual/reference/operator/query/

6 计数(count())
统计查询结果的数据条数,调用count()方法。例如要统计所有数据条数:
count = collection.find().count()
print(count)
要统计符合某个条件的数据,在find()方法中添加指定的条件,例如:
count = collection.find({'age': 20}).count()
print(count)
上面2个count的值是整数值。

7 排序(sort())
直接调用查询结果的sort()方法,并传入排序字段及升序标志即可,如下所示:
results = collection.find().sort('name', pymongo.ASCENDING)
print([result['name'] for result in results])
输出如下所示:
['Jordan', 'Mike', 'michael']
参数pymongo.ASCENDING指定升序排序。要指定以降序排序,可传入pymongo.DESCENDING。

8 偏移
对排序的结果还可以调用偏移方法skip()该方法的参数指定要偏移几个位置。例如要偏移1个元素,得到第二个及后面的元素:
results = collection.find().sort('name', pymongo.ASCENDING).skip(1)
print([result['name'] for result in results]) # 输出如下所示:
['Mike', 'michael']

另外,也可以用limit()方法指定要取的结果个数,例如下面这样:
results = collection.find().sort('name', pymongo.ASCENDING).skip(1).limit(1)
print([result['name'] for result in results]) # 输出如下所示:
['Mike']

这里使用limit()方法会限制返回结果个数,原本返回的是2个,现只返回1个。要注意的是,在数据库数量非常大的时候,如千万、亿级别,就不要使用大的偏移量来查询数据,因为这样可能导致内存溢出。可用类似下面操作来查询:
from bson.objectid import ObjectId
collection.find({'_id': {'$gt': ObjectId('5c734026f3721d0f4f0be288')}})
要这样查询,需要记录好上次查询的_id。

9 更新
要更新数据,使用update()方法,指定更新的条件和更新后的数据即可。例如:
"""使用update()方法查询数据"""
from pymongo import MongoClient
host = "mongodb://michael:michael123@localhost:27017"
client = MongoClient(host) # 连接
db = client.test # 指定数据库
collection = db.students # 指定集合(数据表)
condition = {'name': 'michael'} # 设定查询条件
student = collection.find_one(condition) # 根据查询条件查询1条数据
student['age'] = 25 # 修改age字段的值是25
result = collection.update(condition, student) # 要指定更新条件和更新值
print(result) # 输出信息:{'n': 1, 'nModified': 1, 'ok': 1.0, 'updatedExisting': True}

上面代码中更新name为michael的数据的age,根据设定的查询条件,将数据查询出来,修改age值后调用update()方法将原条件和修改后的数据传入。返回结果是字典形式,ok表示执行成功,nModified代表影响的数据条数。还可以使用$set操作符对数据进行更新,例如下面这样:
result = collection.update(condition, {'$set': student})

使用$set操作符只更新student字典内存在的字段。如果该字典内还有其他字段,则不会更新,也不会删除。不用$set操作符,则会把之前的数据全部用student字典替换;如果原来存在其它字段,则会被删除。

官方不推荐使用update()方法,而是使用更严格的update_one()方法和update_many()方法,它们的第二个参数需要使用$类型操作符作为字典的键名,如下所示:
condition = {'name': 'Jordan'}
student = collection.find_one(condition)
student['age'] = 26
result = collection.update_one(condition, {'$set': student})
print(result)
print(result.matched_count, result.modified_count)

输出结果如下所示:
<pymongo.results.UpdateResult object at 0x7fe6c80f3288>
1 1

这里调用update_one()方法,第二个参数不能再直接传入修改后的字典,需要使用{'$set': student}的形式,返回结果是UpdateResult类型。对其调用matched_count和modified_count属性,可获得匹配的数据条数和影响的数据条数。

下面来看下update_one()方法和update_many()方法的区别。update_one()方法只操作第一条符合条件的数据。update_many()方法会操作所有符合条件的数据。例如下面示例:
# 使用update_one()方法
condition = {'age': {'$gt': 20}}
result = collection.update_one(condition, {'$inc': {'age': 1}}) # $inc是满足条件对应后面的值加1
print(result)
print(result.matched_count, result.modified_count)

# 使用update_many()方法
condition = {'age': {'$gt': 20}}
result = collection.update_many(condition, {'$inc': {'age': 1}}) # $inc是满足条件对应后面的值加1
print(result)
print(result.matched_count, result.modified_count)

这里指定查询条件age为大于20,然后更新条件为{'$inc': {'age': 1}},即age的值加1,执行后将第一条符合条件的数据加1,update_one()方法运行结果如下所示:
<pymongo.results.UpdateResult object at 0x7f1b55e98908>
1 1
update_many()方法运行结果如下所示:
<pymongo.results.UpdateResult object at 0x7fc7dfa47c88>
4 4
从输出可知,只要匹配到的数据都会被更新。

10 删除
直接调用remove()方法指定删除的条件即可,此时符合条件的所有数据均会被删除。例如:
result = collection.remove({'name': 'Kevin'})
print(result) # 输出:{'n': 1, 'ok': 1.0}

remove方法不常用,推荐使用delete_one()和delete_many()方法,示例如下:
result = collection.delete_one({'name': 'Kevin'})
print(result) # 此时的类型是:DeleteResult类型
print(result.deleted_count)
result = collection.delete_many({'age': {'$lt': 25}})
print(result.deleted_count)
输出如下所示:
<pymongo.results.DeleteResult object at 0x7f5e979c4648>
1
6

从输出可知,delete_one()删除第一条符合条件的数据,delete_many()删除所有符合条件的数据。它们的结果类型都是DeleteResult类型。调用deleted_count属性可获取删除的数据条数。

除了上面的基本操作外,PyMongo还提供一些组合方法,如find_one_and_delete()、fine_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/

(二) Redis存储
Redis是基于内存的高效键值型非关系型数据库,存取效率极高,支持多种存储数据结构,使用简单。下面说下Python的Redis操作,主要以redis-py库的用法作说明。

首先要正确安装Redis及redis-py库。要做数据导入/导出操作,还要安装RedisDump。

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

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

官方推荐使用StrictRedis,接下来也用StrictRedis类的相关方法作介绍。

1 连接Redis
Redis正常运行在6379端口,无密码,下面进行Redis连接测试:
from redis import StrictRedis
redis = StrictRedis(host='localhost', port=6379, db=0)
redis.set('name', 'Bob')
print(redis.get('name')) # 输出:b'Bob'

这段连接测试代码中,StrictRedis的参数依次是:主机地址、端口、使用的数据库。如果有密码的话,后面还可以传密码参数password='foobared',这里假设密码是foobared。这4个参数默认不传时,分别默认为:localhost、6379、0和None。首先声明redis为StrictRedis对象,调用其set()方法设置键值对。运行程序未报错,则表示连接成功,同时可以执行set()和get()操作。

还可以使用ConnectionPool(连接池)来连接,如下面代码所示:
from redis import StrictRedis, ConnectionPool
pool = ConnectionPool(host='localhost', port=6379, db=0) # 有密码时也可以在这里传
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连接。当有password时可以写,没有时可以省略。使用URL连接实例如下:
url = 'redis://@localhost:6379/0'
pool = ConnectionPool.from_url(url)
redis = StrictRedis(connection_pool=pool)
先声明一个Redis连接字符串,然后调用from_url()方法创建ConnectionPool,接着将其传给StrictRedis就可完成连接。

2 键操作
Redis的键操作如图5-1所示:
第五部分(三)     数据存储(非关系型数据库存储:MongoDB存储、Redis存储)
图5-1 Reids键的一些判断和操作方法

3 字符串操作
Redis支持基本的键值对形式存储,用法如图5-2所示:
第五部分(三)     数据存储(非关系型数据库存储:MongoDB存储、Redis存储)
图5-2 Redis用法总结

4 列表操作
Redis提供了列表存储,列表内元素可以重复,可以从两端存储,用法如图5-3所示。
第五部分(三)     数据存储(非关系型数据库存储:MongoDB存储、Redis存储)
图5-3 Redis列表操作

5 集合操作
Redis提供了集合存储,集合中的元素不能重复,用法如图5-4所示。
第五部分(三)     数据存储(非关系型数据库存储:MongoDB存储、Redis存储)
图5-4 Redis集合操作

6 有序集合
有序集合比集合多了一个分数字段,利用它可以对集合中的数据进行排序,用法总结如图5-5所示。
第五部分(三)     数据存储(非关系型数据库存储:MongoDB存储、Redis存储)
图5-5 Redis有序集合操作

7 散列操作
Redis还提供了散列表的数据结构。可用name指定散列表的名称,表内存储各个键值对,用法总结如图5-6所示。
第五部分(三)     数据存储(非关系型数据库存储:MongoDB存储、Redis存储)
图5-6 Redis散列操作

8 RedisDump,Redis数据的导入和导出功能
RedisDump有强大的Redis数据的导入和导出功能。要使用该功能需要先进行安装。RedisDump基于Ruby实现,在安装前要先安装Ruby。
RedisDump的GitHub连接和官方连接如下:
GitHub: https://github.com/delano/redis-dump
Redis官方:http://delanotes.com/redis-dump
Ruby的安装参考:http://www.ruby-lang.org/zh_cn/documentation/installation
安装完Ruby后可使用gem命令安装RedisDump,命令如下:
gem install redis-dump

RedisDump提供的两个命令:redis-dump是导出数据,redis-load是导入数据

(1) redis-dump
redis-dump -h ,输入这个命令可以看所有可选项。其中 –u 代表Redis连接字符串,-d 代表数据库代号,-s 代表导出之后的休眠时间,-c 代表分块大小,默认是10000,-f 代表导出时的过滤器,-O 代表禁用运行时优化,-V 用于显示版本,-D 表示开启调试。

导出测试,有密码和无密码的使用方式如下所示:
redis-dump -u :foobared@localhost:6379 # 有密码,密码是:foobared
redis-dump -u localhost:6379 # 无密码
运行上面命令,可将本地0到15号数据库的所有数据导出来,输出如下所示:
{"db":0,"key":"name","ttl":-1,"type":"string","value":"michael","size":7}
每条数据包含6个字段,其中db即数据库代号,key即键名,ttl即该键值对的有效时间,type即键值类型,value即内容,size是占用空间。

要导出所有数据库的所有数据为JSON文件,可使用如下命令:
redis-dump -u localhost:6379 > ./redis_data.json
可使用 -d 参数指定某个数据库的导出,例如要导出10号数据库内容,命令如下:
redis-dump -u localhost:6379 -d 10 > ./py_data.json
要导出特定的内容,可使用 –f 参数过滤,并指出过滤条件,例如要导出以py开头的数据,命令如下:
redis-dump -u localhost:6379 -f py:* > ./py_data.json

(2) redis-load
redis-load -h ,使用该命令查看所有可选项。其中 –u 是Redis连接字符串,-d 是数据库代号,默认是全部,-s 是导入之后的休眠时间,-n 是不检测UTF-8编码,-V 是显示版本,-D 是开启调试。

将JSON行文件导入到Redis数据库中,命令如下:
< redis_data.json redis-load -u @localhost:6379
在linux下使用下面的命令是同样的效果:
cat redis_data.json | redis-load -u @localhost:6379

Redis可实现很多架构,如维护代理池、Cookies池、ADSL拨号代理池、Scrapy-Redis分布式架构等,对于Redis的操作需要熟练。