上一篇文章: Python3網絡爬蟲實戰---3一、數據存儲:文件存儲
下一篇文章: Python3網絡爬蟲實戰---3三、數據存儲:非關係型數據庫存儲:MongoDB
關係型數據庫基於關係模型的數據庫,而關係模型是經過二維表來保存的,因此它的存儲方式就是行列組成的表,每一列是一個字段,每一行是一條記錄。表能夠看做是某個實體的集合,而實體之間存在聯繫,這就須要表與表之間的關聯關係來體現,如主鍵外鍵的關聯關係,多個表組成一個數據庫,也就是關係型數據庫。mysql
關係型數據庫有多種,如 SQLite、MySQL、Oracle、SQL Server、DB2等等。sql
在本節咱們主要介紹 Python3 下 MySQL 的存儲。數據庫
在 Python2 中,鏈接 MySQL 的庫大可能是使用 MySQLDB,可是此庫官方並不支持 Python3,因此在這裏推薦使用的庫是 PyMySQL。segmentfault
本節來說解一下 PyMySQL 操做 MySQL 數據庫的方法。數組
在本節開始以前請確保已經安裝好了 MySQL 數據庫並正常運行,並且須要安裝好 PyMySQL 庫,若是沒有安裝,能夠參考第一章的安裝說明。網絡
在這裏咱們首先嚐試鏈接一下數據庫,假設當前的 MySQL運行在本地,用戶名爲 root,密碼爲 123456,運行端口爲 3306,在這裏咱們利用 PyMySQL 先鏈接一下 MySQL 而後建立一個新的數據庫,名字叫作 spiders,代碼以下:併發
import pymysql db = pymysql.connect(host='localhost',user='root', password='123456', port=3306) cursor = db.cursor() cursor.execute('SELECT VERSION()') data = cursor.fetchone() print('Database version:', data) cursor.execute("CREATE DATABASE spiders DEFAULT CHARACTER SET utf8") db.close()
運行結果:ide
Database version: ('5.6.22',)
在這裏咱們經過 PyMySQL 的 connect() 方法聲明瞭一個 MySQL 鏈接對象,須要傳入 MySQL 運行的 host 即 IP,此處因爲 MySQL 在本地運行,因此傳入的是 localhost,若是 MySQL 在遠程運行,則傳入其公網 IP 地址,而後後續的參數 user 即用戶名,password 即密碼,port 即端口默認 3306。fetch
鏈接成功以後,咱們須要再調用 cursor() 方法得到 MySQL 的操做遊標,利用遊標來執行 SQL 語句,例如在這裏咱們執行了兩句 SQL,用 execute() 方法執行相應的 SQL 語句便可,第一句 SQL 是得到 MySQL 當前版本,而後調用fetchone() 方法來得到第一條數據,也就獲得了版本號,另外咱們還執行了建立數據庫的操做,數據庫名稱叫作 spiders,默認編碼爲 utf-8,因爲該語句不是查詢語句,因此直接執行後咱們就成功建立了一個數據庫 spiders,接着咱們再利用這個數據庫進行後續的操做。編碼
通常來講上面的建立數據庫操做咱們只須要執行一次就行了,固然咱們也能夠手動來建立數據庫,之後咱們的操做都是在此數據庫上操做的,因此後文介紹的 MySQL 鏈接會直接指定當前數據庫 spiders,全部操做都是在 spiders 數據庫內執行的。
因此這裏MySQL的鏈接就須要額外指定一個參數 db。
而後接下來咱們新建立一個數據表,執行建立表的 SQL 語句便可,建立一個用戶表 students,在這裏指定三個字段,結構以下:
字段名 | 含義 | 類型 |
---|---|---|
id | 學號 | varchar |
name | 姓名 | varchar |
age | 年齡 | int |
建立表的示例代碼以下:
import pymysql db = pymysql.connect(host='localhost', user='root', password='123456', port=3306, db='spiders') cursor = db.cursor() sql = 'CREATE TABLE IF NOT EXISTS students (id VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, age INT NOT NULL, PRIMARY KEY (id))' cursor.execute(sql) db.close()
運行以後咱們便建立了一個名爲 students 的數據表,字段即爲上文列舉的三個字段。
固然在這裏做爲演示咱們指定了最簡單的幾個字段,實際在爬蟲過程當中咱們會根據爬取結果設計特定的字段。
咱們將數據解析出來後的下一步就是向數據庫中插入數據了,例如在這裏咱們爬取了一個的學生信息,學號爲 20120001,名字爲 Bob,年齡爲 20,那麼如何將該條數據插入數據庫呢,實例代碼以下:
import pymysql id = '20120001' user = 'Bob' age = 20 db = pymysql.connect(host='localhost', user='root', password='123456', port=3306, db='spiders') cursor = db.cursor() sql = 'INSERT INTO students(id, name, age) values(%s, %s, %s)' try: cursor.execute(sql, (id, user, age)) db.commit() except: db.rollback() db.close()
在這裏咱們首先構造了一個 SQL 語句,其 Value 值咱們沒有用字符串拼接的方式來構造,如:
sql = 'INSERT INTO students(id, name, age) values(' + id + ', ' + name + ', ' + age + ')'
這樣的寫法繁瑣並且不直觀,因此咱們選擇直接用格式化符 %s 來實現,有幾個 Value 寫幾個 %s,咱們只須要在 execute() 方法的第一個參數傳入該 SQL 語句,Value 值用統一的元組傳過來就行了。
這樣的寫法有既能夠避免字符串拼接的麻煩,又能夠避免引號衝突的問題。
以後值得注意的是,須要執行 db 對象的 commit() 方法纔可實現數據插入,這個方法纔是真正將語句提交到數據庫執行的方法,對於數據插入、更新、刪除操做都須要調用該方法才能生效。
接下來咱們加了一層異常處理,若是執行失敗,則調用rollback() 執行數據回滾,至關於什麼都沒有發生過同樣。
在這裏就涉及一個事務的問題,事務機制能夠確保數據的一致性,也就是這件事要麼發生了,要麼沒有發生,好比插入一條數據,不會存在插入一半的狀況,要麼所有插入,要麼整個一條都不插入,這就是事務的原子性,另外事務還有另外三個屬性,一致性、隔離性、持久性,一般成爲 ACID 特性。
概括以下:
屬性 | 解釋 |
---|---|
原子性(atomicity) | 一個事務是一個不可分割的工做單位,事務中包括的諸操做要麼都作,要麼都不作。 |
一致性(consistency) | 事務必須是使數據庫從一個一致性狀態變到另外一個一致性狀態。一致性與原子性是密切相關的。 |
隔離性(isolation) | 一個事務的執行不能被其餘事務干擾。即一個事務內部的操做及使用的數據對併發的其餘事務是隔離的,併發執行的各個事務之間不能互相干擾。 |
持久性(durability) | 持續性也稱永久性(permanence),指一個事務一旦提交,它對數據庫中數據的改變就應該是永久性的。接下來的其餘操做或故障不該該對其有任何影響。 |
插入、更新、刪除操做都是對數據庫進行更改的操做,更改操做都必須爲一個事務,因此對於這些操做的標準寫法就是:
try: cursor.execute(sql) db.commit() except: db.rollback()
這樣咱們即可以保證數據的一致性,在這裏的 commit() 和 rollback() 方法就是爲事務的實現提供了支持。
好,在上面咱們瞭解了數據插入的操做,是經過構造一個 SQL 語句來實現的,可是很明顯,這裏有一個及其不方便的地方,好比又加了一個性別 gender,假如忽然增長了一個字段,那麼咱們構造的 SQL 語句就須要改爲:
INSERT INTO students(id, name, age, gender) values(%s, %s, %s, %s)
相應的元組參數則須要改爲:
(id, name, age, gender)
這顯然不是咱們想要的,在不少狀況下,咱們要達到的效果是插入方法無需改動,作成一個通用方法,只須要傳入一個動態變化的字典給就行了。好比咱們構造這樣一個字典:
{ 'id': '20120001', 'name': 'Bob', 'age': 20 }
而後 SQL 語句會根據字典動態構造,元組也動態構造,這樣才能實現通用的插入方法。因此在這裏咱們須要將插入方法改寫一下:
data = { 'id': '20120001', 'name': 'Bob', 'age': 20 } table = 'students' keys = ', '.join(data.keys()) values = ', '.join(['%s'] * len(data)) sql = 'INSERT INTO {table}({keys}) VALUES ({values})'.format(table=table, keys=keys, values=values) try: if cursor.execute(sql, tuple(data.values())): print('Successful') db.commit() except: print('Failed') db.rollback() db.close()
在這裏咱們傳入的數據是字典的形式,定義爲 data 變量,表名也定義成變量 table。接下來咱們就須要構造一個動態的 SQL 語句了。
首先咱們須要構造插入的字段,id、name 和 age,在這裏只須要將data的鍵名拿過來,而後用逗號分隔便可。因此 ', '.join(data.keys()) 的結果就是 id, name, age,而後咱們須要構造多個 %s 看成佔位符,有幾個字段構造幾個,好比在這裏有兩個字段,就須要構造 %s, %s, %s ,因此在這裏首先定義了長度爲 1 的數組 ['%s'] ,而後用乘法將其擴充爲 ['%s', '%s', '%s'],再調用 join() 方法,最終變成 %s, %s, %s。因此咱們再利用字符串的 format() 方法將表名,字段名,佔位符構造出來,最終sql語句就被動態構形成了:
INSERT INTO students(id, name, age) VALUES (%s, %s, %s)
最後再 execute() 方法的第一個參數傳入 sql 變量,第二個參數傳入 data 的鍵值構造的元組,就能夠成功插入數據了。
如此以來,咱們便實現了傳入一個字典來插入數據的方法,不須要再去修改 SQL 語句和插入操做了。
數據更新操做實際上也是執行 SQL 語句,最簡單的方式就是構造一個 SQL 語句而後執行:
sql = 'UPDATE students SET age = %s WHERE name = %s' try: cursor.execute(sql, (25, 'Bob')) db.commit() except: db.rollback() db.close()
在這裏一樣是用佔位符的方式構造 SQL,而後執行 excute() 方法,傳入元組形式的參數,一樣執行 commit() 方法執行操做。
若是要作簡單的數據更新的話,使用此方法是徹底能夠的。
可是在實際數據抓取過程當中,在大部分狀況下是須要插入數據的,可是咱們關心的是會不會出現重複數據,若是出現了重複數據,咱們更但願的作法通常是更新數據而不是重複保存一次,另外就是像上文所說的動態構造 SQL 的問題,因此在這裏咱們在這裏從新實現一種能夠作到去重的作法,若是重複則更新數據,若是數據不存在則插入數據,另外支持靈活的字典傳值。
data = { 'id': '20120001', 'name': 'Bob', 'age': 21 } table = 'students' keys = ', '.join(data.keys()) values = ', '.join(['%s'] * len(data)) sql = 'INSERT INTO {table}({keys}) VALUES ({values}) ON DUPLICATE KEY UPDATE'.format(table=table, keys=keys, values=values) update = ','.join([" {key} = %s".format(key=key) for key in data]) sql += update try: if cursor.execute(sql, tuple(data.values())*2): print('Successful') db.commit() except: print('Failed') db.rollback() db.close()
在這裏構造的 SQL 語句實際上是插入語句,可是在後面加了 ON DUPLICATE KEY UPDATE,這個的意思是若是主鍵已經存在了,那就執行更新操做,好比在這裏咱們傳入的數據 id 仍然爲 20120001,可是年齡有所變化,由 20 變成了 21,但在這條數據不會被插入,而是將 id 爲 20120001 的數據更新。
在這裏完整的 SQL 構造出來是這樣的:
INSERT INTO students(id, name, age) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE id = %s, name = %s, age = %s
相比上面介紹的插入操做的 SQL,後面多了一部份內容,那就是更新的字段,ON DUPLICATE KEY UPDATE 使得主鍵已存在的數據進行更新,後面跟的是更新的字段內容。因此這裏就變成了 6 個 %s。因此在後面的 execute() 方法的第二個參數元組就須要乘以 2 變成原來的 2 倍。
如此一來,咱們就能夠實現主鍵不存在便插入數據,存在則更新數據的功能了。
刪除操做相對簡單,使用 DELETE 語句便可,須要指定要刪除的目標表名和刪除條件,並且仍然須要使用 db 的 commit() 方法才能生效,實例以下:
table = 'students' condition = 'age > 20' sql = 'DELETE FROM {table} WHERE {condition}'.format(table=table, condition=condition) try: cursor.execute(sql) db.commit() except: db.rollback() db.close()
在這裏咱們指定了表的名稱,刪除條件。由於刪除條件可能會有多種多樣,運算符好比有大於、小於、等於、LIKE等等,條件鏈接符好比有 AND、OR 等等,因此再也不繼續構造複雜的判斷條件,在這裏直接將條件看成字符串來傳遞,以實現刪除操做。
說完插入、修改、刪除等操做,還剩下很是重要的一個操做,那就是查詢。
在這裏查詢用到 SELECT 語句,咱們先用一個實例來感覺一下:
sql = 'SELECT * FROM students WHERE age >= 20' try: cursor.execute(sql) print('Count:', cursor.rowcount) one = cursor.fetchone() print('One:', one) results = cursor.fetchall() print('Results:', results) print('Results Type:', type(results)) for row in results: print(row) except: print('Error')
運行結果:
Count: 4 One: ('20120001', 'Bob', 25) Results: (('20120011', 'Mary', 21), ('20120012', 'Mike', 20), ('20120013', 'James', 22)) Results Type: <class 'tuple'> ('20120011', 'Mary', 21) ('20120012', 'Mike', 20) ('20120013', 'James', 22)
在這裏咱們構造了一條 SQL 語句,將年齡 20 歲及以上的學生查詢出來,而後將其傳給 execute() 方法便可,注意在這裏再也不須要 db 的 commit() 方法。而後咱們能夠調用 cursor 的 rowcount 屬性獲取查詢結果的條數,當前示例中獲取的結果條數是 4 條。
而後咱們調用了 fetchone() 方法,這個方法能夠獲取結果的第一條數據,返回結果是元組形式,元組的元素順序跟字段一一對應,也就是第一個元素就是第一個字段 id,第二個元素就是第二個字段 name,以此類推。隨後咱們又調用了fetchall() 方法,它能夠獲得結果的全部數據,而後將其結果和類型打印出來,它是二重元組,每一個元素都是一條記錄。咱們將其遍歷輸出,將其逐個輸出出來。
可是這裏注意到一個問題,顯示的是4條數據,fetall() 方法不是獲取全部數據嗎?爲何只有3條?這是由於它的內部實現是有一個偏移指針來指向查詢結果的,最開始偏移指針指向第一條數據,取一次以後,指針偏移到下一條數據,這樣再取的話就會取到下一條數據了。因此咱們最初調用了一次 fetchone() 方法,這樣結果的偏移指針就指向了下一條數據,fetchall() 方法返回的是偏移指針指向的數據一直到結束的全部數據,因此 fetchall() 方法獲取的結果就只剩 3 個了,因此在這裏要理解偏移指針的概念。
因此咱們還能夠用 while 循環加 fetchone() 的方法來獲取全部數據,而不是用 fetchall() 所有一塊兒獲取出來,fetchall() 會將結果以元組形式所有返回,若是數據量很大,那麼佔用的開銷會很是高。因此推薦使用以下的方法來逐條取數據:
sql = 'SELECT * FROM students WHERE age >= 20' try: cursor.execute(sql) print('Count:', cursor.rowcount) row = cursor.fetchone() while row: print('Row:', row) row = cursor.fetchone() except: print('Error')
這樣每循環一次,指針就會偏移一條數據,隨用隨取,簡單高效。
本節咱們介紹了 PyMySQL 操做 MySQL 數據庫以及一些SQL語句的構造方法,在後文咱們會在實戰案例中應用這些操做進行數據存儲。
上一篇文章: Python3網絡爬蟲實戰---3一、數據存儲:文件存儲
下一篇文章: Python3網絡爬蟲實戰---3三、數據存儲:非關係型數據庫存儲:MongoDB