通過《網絡數據采集和解析》一文,我們已經知道了如何從指定的頁面中抓取數據,以及如何保存抓取的結果,但是我們沒有考慮過這麽一種情況,就是我們可能需要從已經抓取過的頁面中提取出更多的數據,重新去下載這些頁面對於規模不大的網站倒是問題也不大,但是如果能夠把這些頁面緩存起來,對應用的性能會有明顯的改善。
Redis是REmote DIctionary Server的縮寫,它是一個用ANSI C編寫的高性能的key-value存儲係統,與其他的key-value存儲係統相比,Redis有以下一些特點(也是優點):
- Redis的讀寫性能極高,並且有豐富的特性(發布/訂閱、事務、通知等)。
- Redis支持數據的持久化(RDB和AOF兩種方式),可以將內存中的數據保存在磁盤中,重啓的時候可以再次加載進行使用。
- Redis不僅僅支持簡單的key-value類型的數據,同時還提供hash、list、set,zset、hyperloglog、geo等數據類型。
- Redis支持主從複制(實現讀寫分析)以及哨兵模式(監控master是否宕機並調整配置)。
可以使用Linux係統的包管理工具(如yum)來安裝Redis,也可以通過在Redis的官方網站下載Redis的源代碼解壓縮解歸檔之後進行構件安裝。
# wget http://download.redis.io/releases/redis-3.2.11.tar.gz
# gunzip redis-3.2.11.tar.gz
# tar -xvf redis-3.2.11.tar
# cd redis-3.2.11
# make && make install
接下來我們將redis-3.2.11目錄下的redis.conf配置文件複制到用戶主目錄下並修改配置文件(如果你對配置文件不是很有把握就不要直接修改而是先複制一份再修改這個副本)。
# cd ..
# cp redis-3.2.11/redis.conf redis.conf
# vim redis.conf
配置將Redis服務綁定到指定的IP地址和端口。
配置底層有多少個數據庫。
配置Redis的持久化機制 - RDB。
配置Redis的持久化機制 - AOF。
配置訪問Redis服務器的驗證口令。
配置Redis的主從複制,通過主從複制可以實現讀寫分離。
配置慢查詢日志。
這樣我們就完成了Redis的基本配置,如果對上面的東西感到困惑,可以先係統的了解一下Redis,《Redis開發與運維》是一本不錯的入門讀物,而《Redis實戰》是不錯的進階讀物。
接下來啓動Redis服務器,可以將服務器放在後台去運行。
# redis-server redis.conf &
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 3.2.11 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in standalone mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
| `-._ `._ / _.-' | PID: 12345
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
接下來,我們嘗試用Redis客戶端去連接服務器。
# redis-cli -h 172.18.61.250 -p 6379
172.18.61.250:6379> auth 1qaz2wsx
OK
172.18.61.250:6379> ping
PONG
172.18.61.250:6379>
Redis有著非常豐富的數據類型,也有很多的命令來操作這些數據,具體的內容可以查看Redis命令參考,在這個網站上,除了Redis的命令參考,還有Redis的詳細文檔,其中包括了通知、事務、主從複制、持久化、哨兵、集群等內容。
172.18.61.250:6379> set username admin
OK
172.18.61.250:6379> get username
"admin"
172.18.61.250:6379> hset student1 name hao
(integer) 0
172.18.61.250:6379> hset student1 age 38
(integer) 1
172.18.61.250:6379> hset student1 gender male
(integer) 1
172.18.61.250:6379> hgetall student1
1) "name"
2) "hao"
3) "age"
4) "38"
5) "gender"
6) "male"
172.18.61.250:6379> lpush num 1 2 3 4 5
(integer) 5
172.18.61.250:6379> lrange num 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
172.18.61.250:6379> sadd fruits apple banana orange apple grape grape
(integer) 4
172.18.61.250:6379> scard fruits
(integer) 4
172.18.61.250:6379> smembers fruits
1) "grape"
2) "orange"
3) "banana"
4) "apple"
172.18.61.250:6379> zadd scores 90 zhao 78 qian 66 sun 95 lee
(integer) 4
172.18.61.250:6379> zrange scores 0 -1
1) "sun"
2) "qian"
3) "zhao"
4) "lee"
172.18.61.250:6379> zrevrange scores 0 -1
1) "lee"
2) "zhao"
3) "qian"
4) "sun"
可以使用pip安裝redis模塊。redis模塊的核心是名爲Redis的類,該類的對象代表一個Redis客戶端,通過該客戶端可以向Redis服務器發送命令並獲取執行的結果。上面我們在Redis客戶端中使用的命令基本上就是Redis對象可以接收的消息,所以如果了解了Redis的命令就可以在Python中玩轉Redis。
$ pip3 install redis
$ python3
>>> import redis
>>> client = redis.Redis(host='1.2.3.4', port=6379, password='1qaz2wsx')
>>> client.set('username', 'admin')
True
>>> client.hset('student', 'name', 'hao')
1
>>> client.hset('student', 'age', 38)
1
>>> client.keys('*')
[b'username', b'student']
>>> client.get('username')
b'admin'
>>> client.hgetall('student')
{b'name': b'hao', b'age': b'38'}
MongoDB是2009年問世的一個面向文檔的數據庫管理係統,由C++語言編寫,旨在爲Web應用提供可擴展的高性能數據存儲解決方案。雖然在劃分類別的時候後,MongoDB被認爲是NoSQL的産品,但是它更像一個介於關係數據庫和非關係數據庫之間的産品,在非關係數據庫中它功能最豐富,最像關係數據庫。
MongoDB將數據存儲爲一個文檔,一個文檔由一係列的“鍵值對”組成,其文檔類似於JSON對象,但是MongoDB對JSON進行了二進制處理(能夠更快的定位key和value),因此其文檔的存儲格式稱爲BSON。關於JSON和BSON的差別大家可以看看MongoDB官方網站的文章《JSON and BSON》。
目前,MongoDB已經提供了對Windows、MacOS、Linux、Solaris等多個平台的支持,而且也提供了多種開發語言的驅動程序,Python當然是其中之一。
可以從MongoDB的官方下載鏈接下載MongoDB,官方爲Windows係統提供了一個Installer程序,而Linux和MacOS則提供了壓縮文件。下面簡單說一下Linux係統如何安裝和配置MongoDB。
# wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-amazon-3.6.5.tgz
# gunzip mongodb-linux-x86_64-amazon-3.6.5.tgz
# mkdir mongodb-3.6.5
# tar -xvf mongodb-linux-x86_64-amazon-3.6.5.tar --strip-components 1 -C mongodb-3.6.5/
# export PATH=$PATH:~/mongodb-3.6.5/bin
# mkdir -p /data/db
# mongod --bind_ip 172.18.61.250
2018-06-03T18:03:28.232+0800 I CONTROL [initandlisten] MongoDB starting : pid=1163 port=27017 dbpath=/data/db 64-bit host=iZwz97tbgo9lkabnat2lo8Z
2018-06-03T18:03:28.232+0800 I CONTROL [initandlisten] db version v3.6.5
2018-06-03T18:03:28.232+0800 I CONTROL [initandlisten] git version: a20ecd3e3a174162052ff99913bc2ca9a839d618
2018-06-03T18:03:28.232+0800 I CONTROL [initandlisten] OpenSSL version: OpenSSL 1.0.0-fips29 Mar 2010
...
2018-06-03T18:03:28.945+0800 I NETWORK [initandlisten] waiting for connections on port 27017
說明:上面的操作中,export命令是設置PATH環境變量,這樣可以在任意路徑下執行mongod來啓動MongoDB服務器。MongoDB默認保存數據的路徑是/data/db目錄,爲此要提前創建該目錄。此外,在使用mongod啓動MongoDB服務器時,—bind_ip參數用來將服務綁定到指定的IP地址,也可以用—port參數來指定端口,默認端口爲27017。
我們通過與關係型數據庫進行對照的方式來說明MongoDB中的一些概念。
SQL | MongoDB | 解釋(SQL/MongoDB) |
---|---|---|
database | database | 數據庫/數據庫 |
table | collection | 二維表/集合 |
row | document | 記錄(行)/文檔 |
column | field | 字段(列)/域 |
index | index | 索引/索引 |
table joins | --- | 表連接/嵌套文檔 |
primary key | primary key | 主鍵/主鍵(_id 字段) |
啓動服務器後可以使用交互式環境跟服務器通信,如下所示。
# mongo --host 172.18.61.250
MongoDB shell version v3.6.5
connecting to: mongodb://172.18.61.250:27017/
...
>
-
查看、創建和刪除數據庫。
> // 顯示所有數據庫 > show dbs admin 0.000GB config 0.000GB local 0.000GB > // 創建並切換到school數據庫 > use school switched to db school > // 刪除當前數據庫 > db.dropDatabase() { "ok" : 1 } >
-
創建、刪除和查看集合。
> // 創建並切換到school數據庫 > use school switched to db school > // 創建colleges集合 > db.createCollection('colleges') { "ok" : 1 } > // 創建students集合 > db.createCollection('students') { "ok" : 1 } > // 查看所有集合 > show collections colleges students > // 刪除colleges集合 > db.colleges.drop() true >
說明:在MongoDB中插入文檔時如果集合不存在會自動創建集合,所以也可以按照下面的方式通過創建文檔來創建集合。
-
文檔的CRUD操作。
> // 向students集合插入文檔 > db.students.insert({stuid: 1001, name: '駱昊', age: 38}) WriteResult({ "nInserted" : 1 }) > // 向students集合插入文檔 > db.students.save({stuid: 1002, name: '王大錘', tel: '13012345678', gender: '男'}) WriteResult({ "nInserted" : 1 }) > // 查看所有文檔 > db.students.find() { "_id" : ObjectId("5b13c72e006ad854460ee70b"), "stuid" : 1001, "name" : "駱昊", "age" : 38 } { "_id" : ObjectId("5b13c790006ad854460ee70c"), "stuid" : 1002, "name" : "王大錘", "tel" : "13012345678", "gender" : "男" } > // 更新stuid爲1001的文檔 > db.students.update({stuid: 1001}, {'$set': {tel: '13566778899', gender: '男'}}) WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 }) > // 插入或更新stuid爲1003的文檔 > db.students.update({stuid: 1003}, {'$set': {name: '白元芳', tel: '13022223333', gender: '男'}}, upsert=true) WriteResult({ "nMatched" : 0, "nUpserted" : 1, "nModified" : 0, "_id" : ObjectId("5b13c92dd185894d7283efab") }) > // 查詢所有文檔 > db.students.find().pretty() { "_id" : ObjectId("5b13c72e006ad854460ee70b"), "stuid" : 1001, "name" : "駱昊", "age" : 38, "gender" : "男", "tel" : "13566778899" } { "_id" : ObjectId("5b13c790006ad854460ee70c"), "stuid" : 1002, "name" : "王大錘", "tel" : "13012345678", "gender" : "男" } { "_id" : ObjectId("5b13c92dd185894d7283efab"), "stuid" : 1003, "gender" : "男", "name" : "白元芳", "tel" : "13022223333" } > // 查詢stuid大於1001的文檔 > db.students.find({stuid: {'$gt': 1001}}).pretty() { "_id" : ObjectId("5b13c790006ad854460ee70c"), "stuid" : 1002, "name" : "王大錘", "tel" : "13012345678", "gender" : "男" } { "_id" : ObjectId("5b13c92dd185894d7283efab"), "stuid" : 1003, "gender" : "男", "name" : "白元芳", "tel" : "13022223333" } > // 查詢stuid大於1001的文檔只顯示name和tel字段 > db.students.find({stuid: {'$gt': 1001}}, {_id: 0, name: 1, tel: 1}).pretty() { "name" : "王大錘", "tel" : "13012345678" } { "name" : "白元芳", "tel" : "13022223333" } > // 查詢name爲“駱昊”或者tel爲“13022223333”的文檔 > db.students.find({'$or': [{name: '駱昊'}, {tel: '13022223333'}]}, {_id: 0, name: 1, tel: 1}).pretty() { "name" : "駱昊", "tel" : "13566778899" } { "name" : "白元芳", "tel" : "13022223333" } > // 查詢學生文檔跳過第1條文檔只查1條文檔 > db.students.find().skip(1).limit(1).pretty() { "_id" : ObjectId("5b13c790006ad854460ee70c"), "stuid" : 1002, "name" : "王大錘", "tel" : "13012345678", "gender" : "男" } > // 對查詢結果進行排序(1表示升序,-1表示降序) > db.students.find({}, {_id: 0, stuid: 1, name: 1}).sort({stuid: -1}) { "stuid" : 1003, "name" : "白元芳" } { "stuid" : 1002, "name" : "王大錘" } { "stuid" : 1001, "name" : "駱昊" } > // 在指定的一個或多個字段上創建索引 > db.students.ensureIndex({name: 1}) { "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 } >
使用MongoDB可以非常方便的配置數據複制,通過冗余數據來實現數據的高可用以及災難恢複,也可以通過數據分片來應對數據量迅速增長的需求。關於MongoDB更多的操作可以查閱官方文檔 ,同時推薦大家閱讀Kristina Chodorow寫的《MongoDB權威指南》。
####在Python程序中操作MongoDB
可以通過pip安裝pymongo來實現對MongoDB的操作。
$ pip3 install pymongo
$ python3
>>> from pymongo import MongoClient
>>> client = MongoClient('mongodb://120.77.222.217:27017')
>>> db = client.school
>>> for student in db.students.find():
... print('學號:', student['stuid'])
... print('姓名:', student['name'])
... print('電話:', student['tel'])
...
學號: 1001.0
姓名: 駱昊
電話: 13566778899
學號: 1002.0
姓名: 王大錘
電話: 13012345678
學號: 1003.0
姓名: 白元芳
電話: 13022223333
>>> db.students.find().count()
3
>>> db.students.remove()
{'n': 3, 'ok': 1.0}
>>> db.students.find().count()
0
>>> coll = db.students
>>> from pymongo import ASCENDING
>>> coll.create_index([('name', ASCENDING)], unique=True)
'name_1'
>>> coll.insert_one({'stuid': int(1001), 'name': '駱昊', 'gender': True})
<pymongo.results.InsertOneResult object at 0x1050cc6c8>
>>> coll.insert_many([{'stuid': int(1002), 'name': '王大錘', 'gender': False}, {'stuid': int(1003), 'name': '白元芳', 'gender': True}])
<pymongo.results.InsertManyResult object at 0x1050cc8c8>
>>> for student in coll.find({'gender': True}):
... print('學號:', student['stuid'])
... print('姓名:', student['name'])
... print('性別:', '男' if student['gender'] else '女')
...
學號: 1001
姓名: 駱昊
性別: 男
學號: 1003
姓名: 白元芳
性別: 男
>>>
關於PyMongo更多的知識可以通過它的官方文檔進行了解。
from hashlib import sha1
from urllib.parse import urljoin
import pickle
import re
import requests
import zlib
from bs4 import BeautifulSoup
from redis import Redis
def main():
# 指定種子頁面
base_url = 'https://www.zhihu.com/'
seed_url = urljoin(base_url, 'explore')
# 創建Redis客戶端
client = Redis(host='1.2.3.4', port=6379, password='1qaz2wsx')
# 設置用戶代理(否則訪問會被拒絕)
headers = {'user-agent': 'Baiduspider'}
# 通過requests模塊發送GET請求並指定用戶代理
resp = requests.get(seed_url, headers=headers)
# 創建BeautifulSoup對象並指定使用lxml作爲解析器
soup = BeautifulSoup(resp.text, 'lxml')
href_regex = re.compile(r'^/question')
# 將URL處理成SHA1摘要(長度固定更簡短)
hasher_proto = sha1()
# 查找所有href屬性以/question打頭的a標簽
for a_tag in soup.find_all('a', {'href': href_regex}):
# 獲取a標簽的href屬性值並組裝完整的URL
href = a_tag.attrs['href']
full_url = urljoin(base_url, href)
# 傳入URL生成SHA1摘要
hasher = hasher_proto.copy()
hasher.update(full_url.encode('utf-8'))
field_key = hasher.hexdigest()
# 如果Redis的鍵'zhihu'對應的hash數據類型中沒有URL的摘要就訪問頁面並緩存
if not client.hexists('zhihu', field_key):
html_page = requests.get(full_url, headers=headers).text
# 對頁面進行序列化和壓縮操作
zipped_page = zlib.compress(pickle.dumps(html_page))
# 使用hash數據類型保存URL摘要及其對應的頁面代碼
client.hset('zhihu', field_key, zipped_page)
# 顯示總共緩存了多少個頁面
print('Total %d question pages found.' % client.hlen('zhihu'))
if __name__ == '__main__':
main()