2018年4月29日 星期日

2018 年第 17 周記事

周末看了 MOD 兩部電影, 週六中午看驚悚的韓片 "殺人者的記憶法", 懸疑的劇情與真實虛幻來回穿梭的拍攝手法讓人不斷質疑到底誰是兇手. 晚上回到鄉下, 飯後在曬穀場散步時看了國片 "順雲", 劇情好緩慢好沉悶, 但又很想看看會發生甚麼事. 劇中那個被媽媽帶來給阿公顧白天的小女孩婷婷, 她常在陽台聽順雲母女吵架, 阿公卻一直叫她進來, 我不明白編劇想表達甚麼, 後來看人家解說才了解. 唉.

高樹阿姨告別式訂在五一勞動節那天, 剛好已經跟仲仔等老同學約好要去走瑞奮古道同一天, 我問過爸的意思, 只好跟仲仔說我不能去, 由建興遞補. 周一下班就要回鄉下過一晚, 因早上 7 點時辰很早, 從高雄回去太趕.

2018年4月28日 星期六

Python 學習筆記 : 資料庫存取測試 (一) SQLite

SQLite 是一個小巧簡便的輕量型關聯式資料庫 (DBMS), 係 D. Richard Hipp 於 2000 年任職 Gerneral Dynamics 公司執行美國海軍一個委託案時所設計. SQLite 由一組 C 函式庫組成, 實作了大部分的 SQL-92 標準, 可以使用 SQL 語言進行資料庫操作, 例如 CRUD (新增, 查詢, 更新, 移除) 等作業, 非常適合用在小型應用程式或原型開發 (Proto-typing). 參考 :

https://www.sqlite.org/index.html

與一般的資料庫如 MySQL 等不同的是, SQLite 並非 Client-Server 架構, 因此沒有獨立之伺服器程序, 而是嵌入整合在用戶程式中. SQLite 的資料庫與微軟 ACCESS 一樣是單一檔案型態 (即使用單一文件儲存整個資料庫, 副檔名為 .sqlite 或 .db), 不需要進行任何組態設定, 只要連接資料庫檔案就可以直接使用. SQLite 的特性整理如下 :
  1. SQLite 屬於羽量級基於磁碟之資料庫管理系統
  2. 不需要安裝與設定伺服器
  3. 支援大部分 SQL91 標準, 但不支援外鍵限制
  4. 最大支援 140TB 單一資料庫檔案, 備份方便簡易
  5. 資料以 B+ 樹狀結構儲存
Python 自 2.5 版後即內建名稱為 sqlite3 的 SQLite 模組, 說明文件參考 :

12.6. sqlite3 — DB-API 2.0 interface for SQLite databases
https://www.tutorialspoint.com/sqlite/index.htm
https://www.quackit.com/sqlite/tutorial/about_sqlite.cfm
How to Create, Open, Backup Database in SQLite

可以從 SQLite 官網下載 "Precompiled Binaries for Windows" 安裝檔, 安裝後可由命令列輸入 sqlite3 進入 SQLite3 介面進行資料庫操作 :

https://www.sqlite.org/download.html

sqlite3 模組定義了 sqlite3.Connection 與 sqlite3.Cursor 這兩個類別來連線與操作資料庫. SQLite 資料庫的 CRUD 操作主要是利用 Cursor 物件之 execute() 方法來執行 SQL 指令. 一般資料庫操作之程序如下 :
  1. 呼叫 sqlite3.connect() 連接資料庫
  2. 呼叫 conn.cursor() 建立 Cursor 物件
  3. 呼叫 cursor.execute() 執行 CRUD 操作
  4. 呼叫 conn.close() 關閉資料庫
為了使用方便, Connection 物件也實作了 execute() 方法來執行 SQL 指令 (實際上 Connection 物件也是在背後隱密地產生一個 Cursor 物件來操作資料庫), 因此上面程序 2 與 3 可以合併為如下三步驟 :
  1. 呼叫 sqlite3.connect() 連接資料庫
  2. 呼叫 conn..execute() 執行 CRUD 操作
  3. 呼叫 conn.close() 關閉資料庫
Connection 與 Cursor 物件的常用方法如下表 :

 sqlite3 方法 說明
 conn=sqlite3.connect("db.sqlite") 連接資料庫檔案 db.sqlite
 conn.commit() 將之前的操作變更至資料庫中
 conn.rollback() 取消最近之 commit() 變更, 回復至之前狀態
 conn.close() 關閉資料庫連線
 cursor=conn.execute(SQL) 執行 SQL 指令 (字串), 傳回 Cursor 物件 
 cursor=conn.cursor() 傳回 Cursor 物件
 cursor.execute(SQL) 執行 SQL 指令 (字串)
 cursor.fetchall() 讀取全部剩餘之紀錄以串列傳回, 若無紀錄傳回空串列
 cursor.fetchone() 讀取目前 Cursor 物件所指之下一筆紀錄, 若無傳回 None

SQLite 資料庫操作的 SQL 語法參考官網或  TutorialsPoint :

SQL As Understood By SQLite
# TutorialsPoint : SQLite Tutorial
Python Library Reference

SQLite 的 Schema (表格結構) 相當簡潔, 其資料欄位只有如下 5 種資料型態 :

 sqlite3 資料類型 說明
 NULL 無值或值為 NULL
 INTEGER 有號整數
 REAL 8 Bytes 的 IEEE 浮點數
 TEXT UTF-8, UTF-16BE 或 UTF-16LE 編碼之字串
 BLOB 原始輸入類型

SQLite 並無布林型態欄位, 但可用 INTEGER 的 0 (False) 與 1 (True) 代用. 另外 SQLite 也沒有 DATE/TIME 型態欄位, 但可以透過 Date And Time Functions 日期時間函數的協助利用 REAL, INTEGER, 或 TEXT 型態來儲存日期時間資料. 參考 :

Datatypes In SQLite Version 3

除了資料型態外, 欄位還可以加上一些修飾詞, 例如欄位必須有值要用 NOT NULL; 值不可重複用 UNIQUE; 主鍵欄位使用 PRIMARY KEY, 整數自動增量 AUTOINCREMENT, 此常用來作為當作紀錄的索引. 可自動增量的 id 欄位, 其欄位型態可定義為 :

INTEGER PRIMARY KEY AUTOINCREMENT

注意, AUTOINCREMENT 只能用在 INTEGER, 而且若與 PRIMARY KEY 同時存在時必須放在 PRIMARY KEY 後面.

在測試之前還要安裝一個 FireFox 瀏覽器上好用的 SQLite 圖形化資料庫管理工具, 它是 FireFox 的一個附加元件, 必須經過安裝才能使用. 首先開啟 FireFox, 搜尋 "SQLite Manager", 點擊超連結 :

https://addons.mozilla.org/zh-TW/firefox/addon/sqlite-manager-webext/?src=search





按 "+ 新增至 FireFox" 鈕再按彈出視窗之 "安裝" 鈕, 完成後點 FireFox 右上角之設定鈕, 在彈出視窗中按 "+自訂" 鈕開啟 "其他工具與功能" 頁面 :





將其中的 "SQLite Manager" 拖曳到設定視窗中, 按 "結束自訂模式" 即完成設定 :




再次按設定頁籤開啟 SQLite Manager 頁面, 介面如下 :




以下是 sqlite3 模組之 CRUD 操作測試紀錄, 本測試參考了下列書籍 :
  1.  Python 程式設計實務 (博碩, 何敏煌)
  2.  Python 初學特訓班 (碁峰, 文淵閣工作室)
  3.  Python 入門邁向高手之路-王者歸來 (深石, 洪錦魁)
  4.  Python Pocket Reference (Oreilly, Mark Lutz)
  5.  Python Cookbook(Oreilly, David Beazly)
  6.  Learning Python (Oreilly, Mark Lutz) 
本系列之前的測試紀錄參考 :

Python 學習筆記 : 安裝執行環境與 IDLE 基本操作
Python 學習筆記 : 檔案處理
Python 學習筆記 : 日誌 (logging) 模組測試


1. 連接資料庫 :

使用 import sqlite3 匯入模組後即可呼叫 sqlite3.connect() 方法連接資料庫檔案, 它會傳回一個 sqlite3.Connection 物件, 呼叫 Connection 物件之 cursor() 則會傳回一個 sqlite3.Cursor 物件, 這個 Cursor 物件便是操作資料庫的主要工具 :

D:\Python\test>python
Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 18:41:36) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import sqlite3                                        #匯入 sqlite3 模組
>>> conn=sqlite3.connect("db.sqlite")       #連接資料庫檔案
>>> type(conn)                                              #傳回 Connection 物件
<class 'sqlite3.Connection'> 
>>> cursor=conn.cursor()                            #建立 Cursor 物件
>>> type(cursor) 
<class 'sqlite3.Cursor'> 

注意, 若檔案不存在, SQLite 會自動建立空白的資料庫檔案. 關於 Cursor 物件, 參考 :

https://docs.python.org/2.5/lib/sqlite3-Cursor-Objects.html


2. 新增資料表 : 

新增資料表使用 CREATE TABLE 指令, 若要避免重複新增相同名稱資料表造成錯誤, 可用 CREATE TABLE IF NOT EXISTS, 這樣當同名資料表已經存在時就不會執行此 CREATE TABLE 指令了. SQLite 說明文件參考 :

https://www.sqlite.org/lang_createtable.html

在下面的測試中, 我參考了之前測試 Java 連接 ACCESS 資料庫的範例 :

Java 資料庫存取 : 使用 ACCESS

改寫如下 :

>>> import sqlite3 
>>> conn=sqlite3.connect("db.sqlite") 
>>> SQL='CREATE TABLE IF NOT EXISTS users(id INTEGER \ 
... PRIMARY KEY AUTOINCREMENT NOT NULL,user_name TEXT, \   
... age NUMBER, gender TEXT,email TEXT, password TEXT)' 
>>> cursor=conn.execute(SQL)     //傳回 Cursor 物件
>>> type(cursor)   
<class 'sqlite3.Cursor'>   

這樣便在資料庫 db.sqlite 中建立了一個 users 資料表, 裡面含有 id, user_name, age, gender, email, password 六個欄位. 注意, 有加上 NOT NULL 限制的欄位在新增紀錄時必須要給值, 否則會出現 "NOT NULL constraint failed" 錯誤訊息. 欄位 id 因為有 AUTOINCREMENT 屬性會自動給值, 因此加 NOT NULL 是多此一舉,

在 FireFox 的 SQLite manager 中執行 Database/Connect Database, 點選目前目錄下的 db.sqlite 檔案開啟資料庫, 再打開 users 資料表即可看到其欄位結構 :




在已建立 users 資料表情況下, 若將 IF NOT EXISTS 拿掉, 再次執行 CREATE TABLE 的話就會出現 "table users already exists" 的錯誤訊息 :

>>> SQL='CREATE TABLE users(id INTEGER \ 
...      PRIMARY KEY AUTOINCREMENT NOT NULL,user_name TEXT, \ 
...      age NUMBER, gender TEXT,email TEXT,password TEXT)'
>>> cursor=conn.execute(SQL)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
sqlite3.OperationalError: table users already exists     #資料表已存在

所以在使用 CREATE TABLE 時最好伴隨 IF NOT EXISTS.

另外, SQL 指令中的自訂名稱, 例如資料表名稱與欄位名稱亦可用引號括起來, 但要注意雙引號與單引號交錯出現原則, 即若名稱用雙引號, 則 SQL 語句就用單引號, 例如 :

SQL='CREATE TABLE IF NOT EXISTS "users" ("id" INTEGER \
     PRIMARY KEY AUTOINCREMENT NOT NULL, "user_name" TEXT, "age" NUMBER, \
     "gender" TEXT, "email" TEXT, "password" TEXT)'

若名稱用單引號, 則 SQL 語句就用雙引號, 例如 :

SQL="CREATE TABLE IF NOT EXISTS 'users' ('id' INTEGER \
     PRIMARY KEY AUTOINCREMENT NOT NULL, 'user_name' TEXT, 'age' NUMBER, \
     'gender' TEXT, 'email' TEXT, 'password' TEXT)"


3. 新增與查詢紀錄 : 

新增紀錄之 SQL 指令格式 :

INSERT INTO table_name(field1,field2,...,fieldn) VALUES(val1,val2,...,valn)   

注意, 欄位名稱必須與 VALUES 中列舉之值一一對應, 數目若不一致將導致執行錯誤. 這裡的欄位名稱不需列舉該資料表之全部欄位, 缺漏的欄位將被填入 NULL 值, 但若缺漏的欄位型態為 NOT NULL 將產生執行錯誤. 說明文件參考 :

https://www.tutorialspoint.com/sqlite/sqlite_insert_query.htm

查詢紀錄之 SQL 指令格式 :

SELECT * FROM table [WHERE field='value' [AND field='value']] 

利用 cursor.execute() 或 conn.execute() 執行 SELECT 語句後會傳回 Cursor 物件, 此物件有兩個擷取符合查詢條件之紀錄的方法 :

 Cursor 物件方法 說明
 fetchone() 以 tuple 傳回符合查詢條件之下一筆紀錄, 若無傳回 None
 fetchall() 以 list 傳回符合查詢條件之全部紀錄, 若無傳回 None

其中 fetchone() 傳回的是表示紀錄的 tuple; 而 fetchall() 傳回的是 tuple 組成之 list (表示多筆紀錄), fetchall() 即使只查詢到一筆紀錄也是傳回 list.

說明文件參考 :

https://www.tutorialspoint.com/sqlite/sqlite_select_query.htm

首先新增第一筆紀錄到上面建立的 users 資料表內, 然後用 SELECT 指令查詢單筆紀錄 :

>>> SQL="INSERT INTO users(user_name,age,gender,email,password) \ 
...      VALUES('愛咪','12','女','amy@gmail.com','123')"   
>>> conn.execute(SQL)                             #新增第一筆紀錄
>>> SQL="SELECT * FROM users"     #查詢所有紀錄
>>> cursor=conn.execute(SQL) 
>>> print(cursor.fetchone())                     #fetchone() 傳回 tuple
(1, '愛咪', 12, '女', 'amy@gmail.com', '123') 
>>> SQL="SELECT * FROM users"     #查詢所有紀錄
>>> cursor=conn.execute(SQL) 
>>> print(cursor.fetchall())                        #fetchall() 傳回 list
[(1, '愛咪', 12, '女', 'amy@gmail.com', '123')] 

可見 fetchone() 傳回的是一個表示紀錄的 tuple; 而 fetchall() 傳回的則是可表示多筆紀錄的 list (事實上是 tuple's list), 即使只有一筆紀錄也是傳回 list. 注意, 雖然可查詢到這筆紀錄, 但事實上它還放在記憶體中並未寫回資料庫裡, 因為還沒有呼叫 conn.commit(). 這時到 FireFox 的 SQLite Manager 裡面是看不到這筆紀錄的.

接著寫入第二筆與第三筆紀錄 :

>>> SQL="INSERT INTO users(user_name,age,gender,email,password) \   
...      VALUES('彼得',14,'男','peter@gmail.com','456')" 
>>> conn.execute(SQL)                            #新增第二筆紀錄
>>> SQL="INSERT INTO users(user_name,age,gender,email,password) \ 
...      VALUES('凱莉',16,'女','kelly@gmail.com','789')" 
>>> conn.execute(SQL)                            #新增第三筆紀錄     
>>> SQL="SELECT * FROM users"    #查詢所有紀錄
>>> cursor=conn.execute(SQL) 
>>> print(cursor.fetchone())                     #擷取下一筆
(1, '愛咪', 12, '女', 'amy@gmail.com', '123') 
>>> print(cursor.fetchone())                     #擷取下一筆
(2, '彼得', 14, '男', 'peter@gmail.com', '456') 
>>> print(cursor.fetchone()) 
(3, '凱莉', 16, '女', 'kelly@gmail.com', '789')              #擷取下一筆
>>> print(cursor.fetchone()) 
None 
>>> print(cursor.fetchall())                        #擷取全部
[] 

可見 Cursor 物件的作用如同指向查詢所得紀錄集的指標, 呼叫 fetchone() 就由頭指向下一筆紀錄, 到尾時就傳回 None, 此時呼叫 fetchall() 就傳回空串列了.

以上新增的紀錄每一筆都有完整的欄位資料, 事實上新增時可以只填入部分欄位資料 (但欄位定義中有 NOT NULL 者必須填入), 未填欄位會被填入 None, 例如下面填入第四筆紀錄時只填入 user_name 欄位, 其餘欄位未填, 最後並呼叫 conn.commit() 將目前已寫入記憶體中的紀錄寫回資料庫檔案中 :

>>> SQL="INSERT INTO users(user_name) VALUES('東尼')" 
>>> cursor=conn.execute(SQL)                 #新增第四筆紀錄
>>> SQL="SELECT * FROM users" 
>>> cursor=conn.execute(SQL) 
>>> print(cursor.fetchall())                         #擷取全部
[(1, '愛咪', 12, '女', 'amy@gmail.com', '123'), (2, '彼得', 14, '男', 'peter@gmail.com', '456'), (3, '凱莉', 16, '女', 'kelly@gmail.com', '789'), (4, '東尼', None, None, None, None)]
>>> conn.commit()                                       #寫回資料庫

可見 fetchall() 傳回含有四個 tuple 的串列. 呼叫 commit() 之後再去 SQLite Manager 按 Refresh 鈕即可看到這四筆紀錄 :




可見值為 None 的欄位在 SQLite Manager 中顯示為空白.


4. 更新與刪除紀錄 : 

更新紀錄的 SQL 指令格式如下 :

UPDATE table SET field1='value1' [, field2='value2', ... fieldn='valuen'] [WHERE field=value] 

以上面新增的第四筆紀錄為例, 由於只填入了 user_name 欄位, 此處可用 UPDATE 指令來補足其餘欄位資料, 例如 :

>>> SQL="UPDATE users SET age='48',gender='男',email='tony@gmail.co', \ 
...      password='abc' WHERE user_name='東尼'" 
>>> cursor=conn.execute(SQL)                     #更新紀錄
>>> SQL="SELECT * FROM users"          #查詢全部紀錄
>>> cursor=conn.execute(SQL)                   
>>> print(cursor.fetchall())                             #擷取全部紀錄集
[(1, '愛咪', 12, '女', 'amy@gmail.com', '123'), (2, '彼得', 14, '男', 'peter@gmail.com', '456'), (3, '凱莉', 16, '女', 'kelly@gmail.com', '789'), (4, '東尼', 48, '男', 'tony@gmail.co', 'abc')
>>> conn.commit()                                           #寫回資料庫

可見執行 UPDATE 指令後, 第四筆紀錄缺漏的欄位資料都補足了. 寫回資料庫後將 SQLite Manager 按 Refresh 鈕更新即可看到更新的結果. 注意, 如果沒有設定 WHERE 限制條件, 則每一筆紀錄都會被 UPDATE 操作, 使得被更新的欄位值都變成相同.

SQL 指令也可以用格式化指令 format() 搭配 {} 運算子來對應填值, 上面的 UPDATE 指令可用下列指令取代 :

SQL="UPDATE users SET age='{}',gender='{}',email='{}', \
     password='{}' WHERE user_name='東尼'".format('48',\
     '男','tony@gmail.co','abc')

刪除紀錄的 SQL 指令格式如下 :

DELETE FROM table [WHERE field='value']

注意, 刪除紀錄若沒有加上 WHERE 條件的話會刪除整個資料表內的全部記錄.

以刪除上面第四筆紀錄為例 :

>>> SQL="DELETE FROM users WHERE id='4'"   #刪除第四筆資料
>>> cursor=conn.execute(SQL)                                   
>>> SQL="SELECT * FROM users"                            #查詢全部紀錄
>>> cursor=conn.execute(SQL)                                     
>>> print(cursor.fetchall())                                               #擷取全部紀錄集
[(1, '愛咪', 12, '女', 'amy@gmail.com', '123'), (2, '彼得', 14, '男', 'peter@gmail.com', '456'), (3, '凱莉', 16, '女', 'kelly@gmail.com', '789')]
>>> conn.commit()                                                             #寫回資料庫

可見只剩下三筆資料, 第四筆已經被刪除. 注意, 此處 id 用 4 或 '4' 均可, SQLite 會自動轉態. WHERE 限制條件可以使用 AND 或 OR 來組合多重條件, 例如若要刪除女性紀錄, 但年紀小於 15 歲者, 其 SQL 指令如下 :

>>> SQL="DELETE FROM users WHERE gender='女' AND age < 15" 
>>> cursor=conn.execute(SQL)               #刪除第一筆紀錄
>>> SQL="SELECT * FROM users" 
>>> cursor=conn.execute(SQL) 
>>> print(cursor.fetchall()) 
[(2, '彼得', 14, '男', 'peter@gmail.com', '456'), (3, '凱莉', 16, '女', 'kelly@gmail.com', '789')] 

可見符合女性且年齡小於 15 者只有第一筆紀錄被刪除. 下面測試


5. 更改資料欄位 : 

更改資料欄位的  SQL 指令為 ALTER TABLE, 能用的只有新增欄位以及變更資料表名稱兩個功能, 新增欄位指令格式如下 :

ALTER TABLE table ADD field type

SQLite 一次只能新增一個欄位, 且資料格式不能用上面的簡約格式如 TEXT, INTEGER 等, 而是要用如 CHAR(), VARCHAR(), INT(20) 等詳盡格式, 例如 :

>>> SQL="ALTER TABLE users ADD telephone CHAR(20)"   #新增 telephone 欄位
>>> cursor=conn.execute(SQL)
>>> SQL="ALTER TABLE users ADD city CHAR(20)"             #新增 city 欄位
>>> cursor=conn.execute(SQL) 
>>> conn.commit() 
>>> SQL="INSERT INTO users(user_name) VALUES('潔西卡')"   
>>> cursor=conn.execute(SQL)   
>>> SQL="SELECT * FROM users" 
>>> cursor=conn.execute(SQL) 
>>> print(cursor.fetchall()) 
[(2, '彼得', 14, '男', 'peter@gmail.com', '456', None, None), (3, '凱莉', 16, ' 女', 'kelly@gmail.com', '789', None, None), (5, '潔西卡', None, None, None, None, None, None)]
>>> conn.commit() 

可見每一筆紀錄後面都新增了兩個欄位, 更新 SQLite Manager 後檢視資料表結構可知已加入 telephone 與 city 兩欄位 :




變更資料表名稱指令格式如下 :

ALTER TABLE table RENAME TO  new_table 

其中 new_table 為資料表的新名稱 :

>>> SQL="ALTER TABLE users RENAME TO users_new"
>>> cursor=conn.execute(SQL)
>>> SQL="SELECT * FROM users"
>>> cursor=conn.execute(SQL)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
sqlite3.OperationalError: no such table: users

可見更名為 users_new 之後再去查詢 users 就會出現 "no such table" 的錯誤, 應該改為查詢 users_new 資料表才對 :

>>> SQL="SELECT * FROM users_new"     
>>> cursor=conn.execute(SQL)   
>>> print(cursor.fetchall())   
[(2, '彼得', 14, '男', 'peter@gmail.com', '456', None, None), (3, '凱莉', 16, ' 女', 'kelly@gmail.com', '789', None, None), (5, '潔西卡', None, None, None, None, None, None)]


6. 刪除資料表 : 

刪除資料表指令格式 :

DROP TABLE IF EXISTS table

要將上面建立的 users 資料表刪除之指令為 DROP TABLE users, 但在刪除之前可先用 SQLite Manager 將資料表匯出儲存, 以便刪除後若後悔還可從匯出檔再匯入 :

點選左邊欄位之資料表 users, 按右欄中的 "EXPORT" 鈕 :




勾選 "Include CREATE TABLE statement" :




按底下的 "OK" 鈕即可將資料表 users 存為 users.sql 檔, 內容如下 :

DROP TABLE IF EXISTS "users";
CREATE TABLE users(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,user_name TEXT,age NUMBER, gender TEXT,email TEXT NOT NULL,password TEXT);
INSERT INTO "users" VALUES(1,'愛咪','12','女','amy@gmail.com','123');
INSERT INTO "users" VALUES(2,'彼得',14,'男','peter@gmail.com','456');
INSERT INTO "users" VALUES(3,'凱莉',16,'女','kelly@gmail.com','789');
INSERT INTO "users" VALUES(4,'東尼',48,'男','tony@gmail.co','789');

以後可利用此備份檔重建資料表.

這樣就可以放心刪除資料表 users 了 :

>>> SQL="DROP TABLE IF EXISTS users"   
>>> cursor=conn.execute(SQL) 
>>> SQL="SELECT * FROM users" 
>>> cursor=conn.execute(SQL) 
Traceback (most recent call last):   
  File "<stdin>", line 1, in <module> 
sqlite3.OperationalError: no such table: users   

可見 users 資料表已被刪除, 查詢紀錄顯示 "no such table" 錯誤.

2018年4月24日 星期二

Python 學習筆記 : 日誌 (logging) 模組測試

通常在開發 Python 程式時會用 print() 在疑似出錯的地方輸出變數值來進行除錯, 但這種方式有個問題, 就是當程式開發完成後必須將這些除錯用的 print() 刪除, 對小程式而言還不會形成負擔, 但對中大型軟體來說可是個麻煩的大工程.

Python 內建的 logging 模組可取代 print() 的除錯功能, 開發完成後只要用 logging.disable(50) 指令取消日誌輸出功能即可, 不需刪除日誌指令. 此外, 日誌除了輸出到螢幕顯示外, 還可以輸出到檔案保存, 這是 print() 做不到的. 總之, 使用 logging 功能可以輕易地在程式中輸出自訂訊息, 若執行時部分訊息沒有輸出的話, 表示相關的部分程式碼段被跳過沒有執行.

以下測試參考了下列書籍 :
  1.  Python 3 技術手冊 (碁峰, 黃書逸) - 第 11-28 節
  2.  Python 自動化的樂趣 (碁峰, AL Sweigart) - 第 10 章
  3.  Python 網路爬蟲實戰 (松崗, 胡松濤) -第 4-2 節
  4.  Python 3 程式庫參考手冊 (碁峰, 陳建勳) - 第 9-10 節
  5.  Python 入門邁向高手之路-王者歸來 (深石, 洪錦魁) - 第 15-7 節
本系列之前之測試紀錄參考 :

Python 學習筆記 : 安裝執行環境與 IDLE 基本操作
Python 學習筆記 : 檔案處理

logging 模組是一個非常有彈性的日誌系統, 可將程式執行中發生的事件, 錯誤, 或警告依據安全等級予以過濾後輸出至螢幕 (標準輸出) 或檔案中保存. logging 模組為內建, 使用前只要 import 即可.

D:\Python\test>python
Python 3.6.4 (v3.6.4:d48eceb, Dec 19 2017, 06:54:40) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import logging

說明文件參考 :

https://docs.python.org/3/library/logging.html

logging 模組預先定義了如下 6 種安全等級常數以及對應之日誌輸出函數 (但 logging.NOTSET 無對應之輸出函數), 其中 DEBUG 等級最低也最不重要, 用來輸出除錯訊息; INFO 用來記錄一般事件; WARNING 用來輸出目前無礙程式執行, 但未可能使程式無法執行之潛在警訊; ERROR 用來記錄已經發生之程式錯誤; CRITICAL 等級最高, 用來記錄會使程式停止或當掉的致命性錯誤 :


 安全等級 數值 說明 輸出函數
 logging.NOTSET 0 未設定 x
 logging.DEBUG 10 除錯等級 logging.debug()
 logging.INFO 20 訊息等級 logging.info()
 logging.WARNING 30 警告等級 logging.warning()
 logging.ERROR 40 錯誤等級 logging.error()
 logging.CRITICAL 50 嚴重錯誤等級 logging.critical()


查詢 logging 之安全等級如下, 傳回值為代表等級高低之數值 :

>>> logging.NOTSET 
0
>>> logging.DEBUG 
10
>>> logging.INFO 
20
>>> logging.WARNING 
30
>>> logging.ERROR 
40
>>> logging.CRITICAL   
50

這 6 種安全等級是 logging 模組預設的等級, 基本上足敷一般使用需求. 如果要自訂等級也可以呼叫 logging.addLevelName(level, levelName) 來新增自己的安全等級, 其中 level 為與嚴重性成正比之正整數, 而 levelName 則為其對應之等級名稱. 呼叫 logging.getLevelName(level) 則會傳回等級值 level 之等級名稱, 例如 :

>>> logging.addLevelName(35, 'MINOR')   #新增自訂之等級名稱 'MINOR'
>>> logging.getLevelName(35)                       #傳回等級名稱
'MINOR'
>>> logging.getLevelName(0)
'NOTSET'
>>> logging.getLevelName(10)
'DEBUG'
>>> logging.getLevelName(20)
'INFO'
>>> logging.getLevelName(30)
'WARNING'
>>> logging.getLevelName(40)
'ERROR'
>>> logging.getLevelName(50)
'CRITICAL'

呼叫日誌輸出函數 (須傳入自訂訊息) 會依據日誌之安全等級設定來自動決定是否要紀錄或輸出該訊息, logging 模組預設安全等級為 WARNING, 大於或等於 WARNING 等級之訊息才會被記錄, 例如 :

>>> logging.debug("debug message")             #小於安全層級 : 不輸出
>>> logging.info("info message")                    #小於安全層級 : 不輸出
>>> logging.warning("warning message")     #大於等於安全層級 : 輸出
WARNING:root:warning message
>>> logging.error("error message")                #大於等於安全層級 : 輸出
ERROR:root:error message
>>> logging.critical("critical message")          #大於等於安全層級 : 輸出
CRITICAL:root:critical message

可見預設情況下, 只有等級大於或等於 WARNING 的才會被記錄.

其中安全等級為 DEBUG, INFO, ... CRITICAL 等, 使用者為電腦之使用者帳號, 最後才是自訂訊息. 利用 logging 模組的 basicConfig() 函數可以設定日誌之安全層級, 只要將安全等級以 level 參數傳進去即可 (亦可傳入數值), 例如 :

>>> logging.basicConfig(level=logging.DEBUG)   #設定日誌安全等級為 DEBUG
>>> logging.debug("debug message") 
DEBUG:root:debug message
>>> logging.info("info message")
INFO:root:info message
>>> logging.warning("warning message") 
WARNING:root:warning message
>>> logging.error("error message") 
ERROR:root:error message
>>> logging.critical("critical message") 
CRITICAL:root:critical message

可見將日誌安全等級降到最低的 DEBUG 後, 所有比 DEBUG 等級還要高的訊息都會被記錄. 如果只對 ERROR 層級以上的訊息有興趣, 則只要將 level 設為 40 (logging.ERROR) 即可, 但一個訊息之重要性屬於哪一個安全等級還是需要寫程式的人自行判定要呼叫哪一個輸出函數.

logging.basicConfig() 有 8 個參數如下表所示 :


 參數 說明
 level 日誌之安全等級 (0, 10, 20, 40, 50)
 format 控制輸出訊息的格式化字串
 filename 用來儲存輸出訊息的日誌檔案名稱
 filemode 開啟日誌檔案之模式, 如 'a' (預設), 'w' 等
 datefmt 輸出日期時間 asctime 之格式字串, 與 time.strftime()
 style 格式化字串的標示字元, 有三種 : % (預設), {, 或 $
 handlers 加入至根日誌之處理器, 不可與 stream, filename 同時存在
 stream 標準輸出之串流


其中 filename 參數是當要將日誌紀錄於日誌檔案時用來指定檔案名稱的. 而 datefmt 則是用來設定 format 參數的格式化字串中 asctime 之日期時間格式, 與 time.strftime() 函數一樣, 常用格式如下 :


 參數 說明
 %Y 長年份格式, 例如 2018
 %y 短年份格式, 例如 18
 %m 月份 01~12
 %d 日期 01~31
 %H 小時 00~23
 %w 星期 0~6 (0=星期日)
 %M 分鐘 00~59
 %S 秒 00~59


關於 basicConfig() 參考 :

https://docs.python.org/3/library/logging.html#logging.basicConfig

輸出的訊息除了自訂之字串外, 還可以加上其他相關資訊, 例如事件發生時間, 安全等級名稱, 執行此程式之使用者帳號等, 這可在傳入參數 format 中以如下特定之格式化字串來設定 :


 格式化字串 說明
 %(asctime)s 日期時間, 格式為 YYYY-MM-DD HH:mm:SS,ms (毫秒)
 %(message)s 使用者自訂訊息
 %(levelname)s 日誌安全等級
 %(levelno)s 日誌安全等級之數值
 %(name)s 使用者名稱 (帳號) 
 %(lineno)d 日誌輸出函數呼叫於程式中所在之列數
 %(filename)s 日誌輸出函數之模組的檔名
 %(module)s 日誌輸出函數之模組名稱
 %(pathname)s 日誌輸出函數之模組之完整路徑
 %(funcName)s 日誌輸出函數之名稱
 %(threrad)d 執行緒 ID
 %(threradName)s 執行緒名稱
 %(process)d 程序 ID
 %(created)f 以 UNIX 標準表示之現在時間 (浮點數)


如果沒有傳入 format 參數, 則預設之輸出訊息格式為 :

安全等級:使用者:自訂訊息

即格式化字串預設為 format='%(levelname)s:%(name)s:%(message)s'

例如 :

>>> import logging
>>> logging.warning("Warning message")     #預設安全層級 : WARNING
WARNING:root:Warning message
>>> logging.basicConfig(level=logging.DEBUG,format='%(asctime)s - %(levelname)s : %(message)s')
>>> logging.debug('Debug message')
2018-04-24 18:18:19,848 - DEBUG : Debug message
>>> logging.Warning('Warning message') 
2018-04-24 18:22:24,796 - WARNING : Warning message
>>> logging.info('Info message') 
2018-04-24 18:27:01,729 - INFO : Info message
>>> logging.error('Error message') 
2018-04-24 18:27:31,980 - ERROR : Error message
>>> logging.critical('Critical message') 
2018-04-24 18:28:02,600 - CRITICAL : Critical message

注意, asctime 的預設格式為 '%Y-%m-%d %H:%M:%S,', 毫秒部分另外用 '%s,%03d' 格式 (參考 formatTime). 這可用 datefmt 參數來自訂格式, 例如 :

>>> import logging
>>> logging.basicConfig(level=logging.DEBUG,
...                     format='%(asctime)s - %(levelname)s : %(message)s',
...                     datefmt='%Y%m%dT%H%M%S')
>>> logging.info('Info message')
20180424T193715 - INFO : Info message


可見時間格式已經改為自訂格式了. 我在 Windows 下測試發現, 連續用 logging.basicConfig() 更改設定有時候並無效果, 這時只要用 exit() 跳出 IDLE 重新啟動再執行 logging.basicConfig() 即可.

如果要將日誌輸出到檔案, 則須傳入 filename 參數, 傳入值為可含路徑之檔案名稱字串, 例如 :

>>> import logging 
>>> logging.basicConfig(level=logging.DEBUG
...                     format='%(asctime)s - %(levelname)s : %(message)s', 
...                     filename='mylog.txt') 
>>> logging.critical("critical message") 
>>> logging.debug("debug message") 
>>> logging.info("info message") 
>>> logging.warning("warning message") 
>>> logging.error("error message") 
>>> logging.critical("critical message") 

此處是將訊息輸出到目前目錄下的 mylog.txt 日誌檔裡. 可見使用 filename 參數將訊息輸出至日誌檔案後, 就不會輸出至標準輸出 (螢幕) 了. 開啟 mylog.txt 果然訊息都記錄在輸出檔 :




日誌檔的預設寫入模式為 'a' (append), 亦即新的輸出訊息會被記錄在日誌檔尾端. 如果要改為其他寫入模式例如 'w', 則須傳入參數 filemode='w'.

Python 的 logging 模組可以同時紀錄多個日誌, 上面的範例中直接使用 logging 模組輸出日誌訊息使用的是預設之根日誌 (Root Logger), 因此預設之輸出訊息中的 %(name) 顯示使用者為 'root'. 不傳入參數直接呼叫 logging.getLogger() 會傳回根日誌 RootLogger 物件, 它是 Logger 物件的一個特例, 提供如下幾個常用方法 :


 Logger 物件方法 說明
 setLevel(level) 設定日誌安全等級
 getEffectiveLevel() 傳回目前設定之安全等級
 isEnabledFor(level) 檢查傳入之 level 是否會被處理 (依據目前等級), 傳回 True/False
 debug(msg) 輸出 debug 等級之訊息
 info(msg) 輸出 info 等級之訊息
 warning(msg) 輸出 warning 等級之訊息
 error(msg) 輸出 error 等級之訊息
 critical(msg) 輸出 critical 等級之訊息
 log(level, msg) 輸出指定等級 level 之訊息
 addHandler(handler) 加入處理器 handler


說明文件參考 :

https://docs.python.org/3/library/logging.html#logging.Logger
https://docs.python.org/2.4/lib/minimal-example.html

例如 :

>>> import logging
>>> rootlogger=logging.getLogger()      #傳回根日誌 RootLogger 物件
>>> type(rootlogger)                                #型態為 RootLogger
<class 'logging.RootLogger'> 
>>> rootlogger.getEffectiveLevel()         #傳回目前根日誌安全等級=WARNING
30
>>> rootlogger.setLevel(logging.ERROR)       #設定根日誌安全等級=ERROR
>>> rootlogger.getEffectiveLevel()                   #傳回目前根日誌安全等級=ERROR
40
>>> rootlogger.isEnabledFor(logging.DEBUG)        #DEBUG 低於 ERROR
False
>>> rootlogger.isEnabledFor(logging.WARNING)   #WARNING 低於 ERROR
False
>>> rootlogger.isEnabledFor(logging.ERROR)         #等於 ERROR : True
True
>>> rootlogger.isEnabledFor(logging.CRITICAL)    #大於 ERROR : False
True
>>> rootlogger.debug("debug message")            #低於 ERROR : 不輸出
>>> rootlogger.warning("warning message")    #低於 ERROR : 不輸出
>>> rootlogger.error("error message")               #等於 ERROR : 輸出
error message
>>> rootlogger.critical("critical message")         #大於 ERROR : 輸出
critical message
>>> rootlogger.log(logging.CRITICAL,"critical message")   #通用之日誌輸出函數
critical message

如果要使用自訂的日誌而非根日誌, 可在呼叫 logging.getLogger() 函數時傳入日誌名稱字串, 傳回值是一個 Logger 物件, 呼叫 Logger 物件的 debug(), info(), warning(), error(), critical() 或 log() 方法即可將訊息輸出至該自訂日誌而非根日誌, 例如 :

>>> import logging
>>> tonylogger=logging.getLogger('Tony')    #設定名為 Tony 之自訂日誌
>>> type(tonylogger)                                         #傳回 Logger 物件
<class 'logging.Logger'> 
>>> tonylogger.getEffectiveLevel()                  #傳回 Tony 日誌安全等級=WARNIMG
30
>>> tonylogger.setLevel(40)                              #設定 Tony 日誌安全等級=ERROR
>>> tonylogger.getEffectiveLevel()                   #傳回 Tony 日誌安全等級=ERROR
40
>>> tonylogger.debug("debug message")            #低於 ERROR : 不輸出
>>> tonylogger.warning("warning message")     #低於 ERROR : 不輸出
>>> tonylogger.error("error message")               #等於 ERROR : 輸出
error message 
>>> tonylogger.critical("critical message")          #大於 ERROR : 輸出
critical message

可見不論使用 Logger 或 RootLogger 物件輸出日誌訊息, 預設格式只有訊息本身, 如果要同時記錄使用者, 安全等級與日期時間等訊息, 須依下列順序設定 :
  1. 呼叫 logging.Formatter() 建立 Formatter 格式化物件
  2. 呼叫 logging.StreamHandler() 建立 StreamHandler 輸出串流處理物件
  3. 呼叫 Handler 物件之 setFormatter() 方法設定處理器之輸出格式
  4. 呼叫 Logger 物件之 addHandler() 加入處理器物件
以上面建立的 Tony 日誌為例說明如下 :

>>> formatter=logging.Formatter('%(levelname)s:%(name)s:%(message)s')  #格式化
>>> type(formatter)                                              #傳回 Formatter 物件
<class 'logging.Formatter'>   
>>> streamhandler=logging.StreamHandler()   #傳回 StreamHandler 物件
>>> type(streamhandler) 
<class 'logging.StreamHandler'>
>>> streamhandler.setFormatter(formatter)      #為處理器指定格式化物件
>>> tonylogger.addHandler(streamhandler)      #為自訂日誌添加處理器
>>> tonylogger.log(logging.WARNING,"warning message")   #低於 ERROR : 不輸出
>>> tonylogger.log(logging.ERROR,"error message")   
ERROR:Tony:error message   
>>> tonylogger.log(logging.CRITICAL,"critical message")   
CRITICAL:Tony:critical message

可見使用 StreamHandler 與 Formatter 後日誌訊息已能依照格式輸出了. 上面範例是輸出到標準輸出 (螢幕), 需使用 StreamHandler, 若要輸出到日誌檔, 則需使用 FileHandler 物件, 如下所示 :

>>> format='%(asctime)s:%(levelname)s:%(name)s:%(message)s'
>>> formatter=logging.Formatter(format)                       #傳回 Formatter 物件
>>> type(formatter) 
<class 'logging.Formatter'>
>>> logfilehandler=logging.FileHandler('./tonylog.txt')   #傳回 FileHandler 物件
>>> type(logfilehandler) 
<class 'logging.FileHandler'>
>>> filehandler.setFormatter(formatter)      #為處理器指定格式化物件
>>> tonylogger.addHandler(filehandler)      #為自訂日誌添加處理器
>>> tonylogger.log(logging.WARNING,"warning message")   #低於 ERROR : 不輸出
>>> tonylogger.log(logging.ERROR,"error message") 
ERROR:Tony:error message
>>> tonylogger.log(logging.CRITICAL,"critical message")   
CRITICAL:Tony:critical message

開啟日誌檔案 tonylog.txt 內容如下 :

2018-04-25 14:59:56,069:ERROR:Tony:error message
2018-04-25 15:00:04,043:CRITICAL:Tony:critical message




可見與上面使用根日誌時不同之處在於, 日誌訊息同時輸出到螢幕與日誌檔, 這是因為我們同時在自訂日誌 tonylogger 中開啟了 StreamHandler (給螢幕) 與 FileHandler (給檔案) 之故. 使用自訂日誌雖然較麻煩, 但彈性比較大.

上面我們是武斷地為自訂日誌取名 ('Tony'), 事實上若以目前登入者帳號為名可能較實用, 這可以用 Python 內建 getpass 模組之 getuser() 來取得使用者名稱, 例如 :

>>> import getpass 
>>> user=getpass.getuser()            #傳回登入者帳號 (字串)
>>> user 
'user'
>>> type(user)                                 #getuser() 傳回值為字串
<class 'str'>
>>> import logging
>>> logger=logging.getLogger(user)    #建立以使用者為名之日誌物件
>>> type(logger)                                   
<class 'logging.Logger'>
>>> logger.getEffectiveLevel()     #查詢目前安全等級之值
30                                                                    #30=logging.WARNING


為了使用日誌方便, 可將所需日誌格式寫成一個類別, 在 "Python 網路爬蟲實戰" 的 4-2-2 節有介紹一個自訂日誌類別寫法, 我將其改寫如下 :

#myLog.py
import logging
import getpass

class MyLog(object):
    def __init__(self):
        user=getpass.getuser()
        self.logger=logging.getLogger(user)
        self.logger.setLevel(logging.DEBUG)
        format='%(asctime)s - %(levelname)s -%(name)s : %(message)s'
        formatter=logging.Formatter(format)
        streamhandler=logging.StreamHandler()
        streamhandler.setFormatter(formatter)
        self.logger.addHandler(streamhandler)
        logfile='./' + user + '.log'
        filehandler=logging.FileHandler(logfile)
        filehandler.setFormatter(formatter)
        self.logger.addHandler(filehandler)
    def debug(self, msg):
        self.logger.debug(msg)
    def info(self, msg):
        self.logger.info(msg)
    def warning(self, msg):
        self.logger.warning(msg)
    def error(self, msg):
        self.logger.error(msg)
    def critical(self, msg):
        self.logger.critical(msg)
    def log(self, level, msg):
        self.logger.log(level, msg)
    def setLevel(self, level):
        self.logger.setLevel(level)
    def disable(self):
        logging.disable(50)

將此類別存成 myLog.py 檔案, 用 from myLog import MyLog 匯入 MyLog 類別後呼叫 MyLog() 建構式建立物件, 然後就可以呼叫此物件之 debug(), info(), warning(), error(), critical(), 以及 log() 等方法來記錄日誌訊息於螢幕與日誌檔 "使用者名稱.log" 了, 例如 :

>>> from myLog import MyLog 
>>> mylog=MyLog() 
>>> mylog.error("error message")      #預設安全等級=DEBUG
2018-04-25 18:17:07,364 - ERROR -Tony : error message 
>>> mylog.debug("debug message") 
2018-04-25 18:25:03,276 - DEBUG -Tony : debug message
>>> mylog.info("info message") 
2018-04-25 18:30:28,747 - INFO -Tony : info message
>>> mylog.warning("warning message") 
2018-04-25 18:30:41,814 - WARNING -Tony : warning message
>>> mylog.error("error message") 
2018-04-25 18:31:08,937 - ERROR -Tony : error message
>>> mylog.critical("critical message") 
2018-04-25 18:31:27,330 - CRITICAL -Tony : critical message
>>> mylog.log(40, "error message") 
2018-04-25 18:40:15,587 - ERROR -Tony : error message

可見使用這個 MyLog 類別讓紀錄日誌簡單多了. 此類別預設安全等級為 DEBUG (根日誌預設是 WARNING), 因此每一種等級之資訊都會輸出. 日誌同時會記錄到以使用者為名之 .log 檔案, 此處使用者為 Tony, 因此開啟目前目錄下之 Tony.log 會發現與螢幕輸出完全一樣之內容 :

2018-04-25 18:25:03,276 - DEBUG -Tony : debug message
2018-04-25 18:30:28,747 - INFO -Tony : info message
2018-04-25 18:30:41,814 - WARNING -Tony : warning message
2018-04-25 18:31:08,937 - ERROR -Tony : error message
2018-04-25 18:31:27,330 - CRITICAL -Tony : critical message
2018-04-25 18:40:15,587 - ERROR -Tony : error message

此類別預設安全等級為 10 (DEBUD), 因此每一種訊息都會輸出紀錄, 呼叫 setLevel() 方法傳入 10, 20, 30, 40, 50 可以更改安全等級設定 (不要用 logging.DEBUG 等, 會出現錯誤) :

>>> mylog.setLevel(30)                   #安全等級由預設之 DEBUG 改為 WARNING
>>> mylog.info("info message")    #低於 WARNING : 不輸出
>>> mylog.warning("warning message")    #等於 WARNING : 輸出
2018-04-25 18:44:20,290 - WARNING -Tony : warning message

此處將安全等級改為 30 (WARNING) 後, INFO 等級以下的就不會輸出了, 要等於或大於 WARNING 的才會輸出.

最後, 程式開發除錯結束後, 在根日誌可以呼叫 logging.disable(50) 或 logging.disable(logging.CRITICAL) 來關閉日誌輸出功能, 不需要將程式中所有日誌輸出指令刪除, 例如 :

>>> logging.disable(50)    #關閉日誌輸出功能
>>> logging.debug("debug message")      #無輸出
>>> logging.critical("critical message")    #無輸出

可見關閉後即使是 CRITICAL 的訊息也不會輸出了.

使用上面的自訂日誌類別的話, 只要呼叫 mylog.disable() 就可以了 :

>>> from myLog import MyLog 
>>> mylog=MyLog() 
>>> mylog.warning("warning message")   
2018-04-25 19:16:43,426 - WARNING -Tony : warning message
>>> mylog.disable()                                      #關閉日誌功能
>>> mylog.warning("warning message")   
>>> mylog.critical("critical message")   
>>>

可見日誌功能被關閉後, 全部日誌都不會被記錄.

參考 :

6.29.2 Basic example
https://docs.python.org/3/howto/logging-cookbook.html
[Python] logging 教學

2018年4月23日 星期一

2018 年第 16 周記事

上週訂製的臥室窗簾週四晚上來安裝了, 換新之後果然氣象為之一新.

昨晚自鄉下回來高雄, 晚一些又載水某去鳳山娘家, 因明日她要去首爾開會, 剛好岳母今日早上要去東京旅遊, 所以岳父一起載他們去機場, 我就不用請假跑一趟小港了.

高樹阿姨周一轉至長庚後, 醫生已經開始進行免疫療法, 醫生說以前沒收治過這種病例, 成功率可能三成而已, 但值得一試, 總比在義大都無作為好吧! 週四晚上 8:00 我前往探視, 阿姨眼睛仍炯炯有神, 但兩手浮腫, 大姊說醫生有在評估洗腎必要性, 因洗腎可能會影響療效.

今日 (4/23) 中午阿姨的女兒 Line 我說她媽媽不行了, 要帶回高樹, 我直到下午上班才看到訊息, 她又補充說醫生來看了之後表示再觀察看看. 但到了下班前夕急轉直下, 輸血後血壓拉不上來, 醫院通知可帶回去了. 我先發訊息提醒她女兒到家後的助念事宜, 回到家吃過晚飯後偕菁菁回鄉下載爸一起過去高樹上香. 在屋前跟阿姨的女兒們談這四個多月來的醫療過程, 她們都頗感自責, 因為轉至長庚時, 加護病房的醫生責怪她們怎麼這麼晚才轉來, 主治也說如果不開刀, 或許機會大一些. 我只好要她們別太自責, 生病了只能相信醫生的判斷, 這大概就是台灣俗話所謂的 "先生緣" 吧! 唉, 母親 2014 年仙遊, 三年半後她情同姊妹的高樹阿姨也作古, 真是世事難料, 人生無常. 三年來端午節都包粽子送我們的高樹阿姨, 今年不會再包了.

2018年4月22日 星期日

創見隨身碟無法格式化問題

之前在燦坤買的創見 16GB 隨身碟這兩天拿來用時, 發現一插入電腦就顯示隨身碟有問題須進行修復, 不理它也沒關係, 但檢視隨身碟內容確實部分資料夾無法存取, 顯然檔案系統真的有問題了.

早上將裡面可存取之檔案資料夾複製到鄉下的 Win7 電腦內備份後將其格式化, 結果做到一半就顯示 "磁碟有防寫保護" 與 "Windows 無法格式化" :






上網查詢此問題, 原來要用創見提供的軟體才行, 參考 :

創見USB隨身碟「磁碟有防寫保護」解決方式教學

我照其說明到創見官網下載 JetFlash Online Recovery 程式 :

https://tw.transcend-info.com/Support/Software-3/




執行此程式, 選擇正確之 USB 碟後按確定即開始格式化 :





完成後移除 USB 重新插拔隨身碟即可 :




哈哈! 格式化後再插入就正常可用了. 如果這樣還是失敗, 隨身碟應該 GG 了, 有終身保固就拿去創見換一個.

2018年4月21日 星期六

小咪走失記

下午菁菁的理化家教老師來了之後, 我就開始埋首研讀 Python 網頁擷取書籍, 4 點 40 分下樓倒完垃圾回來, 覺得怎麼好像整個下午都沒看到小咪呢? 房間, 客廳, 陽台到處找遍了都沒看到, 問上課中的菁菁也說不知道, 老師則說他進來時看到她在陽台, 該不會趁二哥牽腳踏車出去還是老師進來的空檔溜出去?

我馬上搭電梯到頂樓找, 沒有, 然後走樓梯下到一樓也沒有, 上回偷跑出去就是在 2 樓樓梯間被我找到, 她好像是找不到家就沿著樓梯上下跑, 只要一樓電梯外的大門沒開應該不會出去. 我想該不會經由頂樓陽台跑到別棟樓梯吧? 我把 A, B, C, D 四棟安全梯由上到下都走遍, 甚至 B1, B2 都去找也無蹤跡. 到管理室問組長有沒有人看到一隻黑貓, 她也說沒看到, 剛下課的家教老師還沒回去, 也在一樓警衛室外走道來回尋找, 他說小咪走失菁菁會難過, 他人真好, 但我叫他不用找了, 我騎腳踏車在社區周圍逛一圈如果沒有看到也只好放棄.

突然覺得小咪不見了好不習慣, 大家都出去樓梯間尋找, 二哥本來已出門要去補習又跑回來找. 我把 A 棟逃生梯樓梯間從頂樓往下再找一遍還是沒有, 看看時間已近六點, 必須啟程返回鄉下, 剛從地下室將車開上來就接到二哥電話, 說小咪悶不吭聲躲在椅子底下, 根本沒出去啦! 聽到這消息讓我鬆了一口氣, 否則我整個周末心情都不會好.

可惡的臭小咪! 如果走失你就變野貓啦! 你會很可憐, 而我會很寂寞.

2018年4月20日 星期五

買書 : Python入門邁向高手之路

這幾天突然對 Python web scraping 又有了興趣, 今日在博客來找到這本書, 裡面有談到網頁擷取方法, 向明儀詢問發現有現貨, 就請他們放在櫃檯, 今天去河堤還書時順便去看看.

Python入門邁向高手之路王者歸來(附光碟)

Source : 博客來

此書非常厚, 篇幅有 34 章之多, 翻閱時發現有一章是介紹如何利用 Twilio 傳送免費簡訊, 剛好前幾天我直接在 Twilio 網站測試失敗, 一個簡訊都沒收到, 或許可從此書獲得解方, 當即決定買下來好好研究一番.

好書 : Data Science from Scratch-用 Python 學資料科學

跟市圖預約這本書好久了 (約半年), 最近才拿到書, 看書名有 from scratch 原以為是寫給初學者看, 手把手從零開始講的入門書, 看了前兩章發覺此書寫得簡約不囉嗦, 換句話說就是內容壓得很踏實, 並不是可以輕鬆閱讀的書, 畢竟 25 章的內容要用 309 頁篇幅來鋪陳, 沒有省話一哥的本事是寫不出來的. 此書翻譯者藍子軒譯筆流暢, 之前買過幾本他翻的書, 評價都不錯.



Source : 博客來


此書感覺有點像資料科學概論, 幾乎每一個主題都有觸及, 例如 4~7 章介紹資料科學要用到的線性代數, 機率, 與統計學, 第 9 章提到網頁擷取等, 但沒有辦法每一個都深入介紹, 所以每章尾巴都有一節 "進一步探索" 介紹該章提及但無法納入的內容之參考資料.


關於真武湯

今天跟同事大熊閒聊談到他冬天偶而會服科學中藥真武湯, 因其中含大熱之附子可對治冬天手腳冰冷云云. 我查了一下網路資料, 原來真武湯是經方有名的方子, 組成如下 :

炮附子(一枚)  白朮(三兩炒)  茯苓、白芍(三兩炒)  生薑(三兩)

傷寒論云 : "太陽病發汗,汗出不解,其人仍發熱,心下悸、頭眩、身瞤動、振振欲擗地者,真武湯主之". 真武主要作用是強心利尿改善血液循環, 取名真武係因真武大帝乃道教北方之神, 而北方屬水之故. 真武大帝即玄天上帝, 因避宋朝傳說先祖趙玄朗諱而改為真武. 參考 :

https://zh.wikipedia.org/wiki/玄天上帝

其中 "身瞤動" 指的是像帕金森氏症那樣的抖動症狀嗎? 另外有些資料寫真武湯對高血壓也有效, 但炮附子含量要高些, 太低無效. 我查詢莊松榮的真武湯炮附子用量為 3g. 參考 :

真武湯配方
大口仔功課簿 : 真武湯 (香港)
【真武湯】筆記(部分)

2018年4月19日 星期四

賈揚清談 Caffe 的誕生

我在搜尋 Caffe 的資料時, 在壹讀看到 Caffe 作者賈揚清說明他 2016 年離開 Google 加入 Facebook 的原因是為了個人學習與職涯發展, 但主要是談他開發 Caffe 的心路歷程, 原來指導老師的一席話, 讓他在趕博士論文與撰寫一個大家都會用的框架之間分出主次, 毅然全力投入 Caffe 的撰寫, 然後再回頭搞定自己的論文.

Caffe 作者賈揚清:我為什麼離開 Google,加入 Facebook?

賈揚清是中國浙江紹興人, 北京清大碩士畢業後赴美求學, 2013 年取得加州大學柏克萊分校計算機科學博士學位後進入 Google 實習, 而後加入 Google Brain 團隊, 參與了谷歌 TensorFlow 框架研發. 2016 年轉職至 Facebook 擔任先進 AI 技術研究科學家, 推出的 Caffe2Go 框架讓機器學習運算可以應用在行動裝置中. 關於 Caffe 參考 :

# Wiki :Caffe (software)
http://caffe.berkeleyvision.org
https://github.com/BVLC/caffe
https://caffe2.ai
https://github.com/caffe2/caffe2

目前機器學習已快速地滲透到我們的生活中, 業界對人才需求孔急, 現在的問題已非要不要學習 AI 技術, 而是要用多快的速度去學習 AI 技術的問題了. TensorFlow 與 Caffe 等開源機器學習框架的出現, 讓入門者可以快速地建構出 Prototype, 大大降低進入門檻, 開源實在是推動文明前進的最大動力. 如同物種的演化一樣, 當理論與技術到達某種臨界點之後, 其進程將會是爆炸性的成長!

參考 :

厉害!这位绍兴人是世界顶级人工智能科学家,春晖中学毕业!
臉書AI科學家賈揚清:科技巨頭為何願將深度學習框架開源?
挑戰 TensorFlow 霸主地位!Facebook 聯手微軟整合三大 AI 運算框架,讓 AI 協同工作再也不難
邊緣運算關鍵技術AI,讓瑞典工具機大廠異常預警速度加快20倍


2018年4月17日 星期二

無作用的 Twilio SMS 服務

以前在物聯網書上看過 Twilio 的簡訊服務, 申請了帳號後卻一直沒有使用, 今天因為檢查智邦生活館的 PP 一元簡訊帳號餘額 (還有 311 元), 順便就測試看看 Twilio SMS 服務.

登入 Twilio 後點選 "Programmable SMS" 功能, 按 "Get Started" :




按 "Get a number" 取得一個 Twilio 電話號碼 :




左上角顯示配發之號碼, 如果滿意就按右下角的 "Choose this number", 否則就按右上角的 "Search a different nember" :




按 "Done" 完成設定, 回到 SMS 頁面 :




在 "Body" 輸入欲傳送之簡訊, 按 "Make request" 傳送簡訊 :





但我傳送了好幾次都沒收到簡訊, 奇怪, 明明在 SMS Coverage 網頁台灣有列在涵蓋範圍內啊!




看起來並沒有其他還需要設定之處啊!

2018年4月16日 星期一

使用 Keras 卷積神經網路 (CNN) 辨識 Cifar-10 圖片 (二)

上一篇 Keras 機器學習測試中, 利用 CNN+MLP 結構將 Cifar-10 資料集的圖片預測準確度從單用 MLP 的 0.45 提升到 0.74, 足見 CNN 的學習效果非常顯著. 如果將此學習架構深度化, 多加一層卷積 + 池化層, 隱藏層也增加一層, 這樣應該可使準確度再拉高, 當然也將耗費更多運算時間.

本系列之前的測試紀錄參考 :

Windows 安裝深度學習框架 TensorFlow 與 Keras
使用 Keras 測試 MNIST 手寫數字辨識資料集
使用 Keras 多層感知器 MLP 辨識手寫數字 (一)
使用 Keras 多層感知器 MLP 辨識手寫數字 (二)

以下根據林大貴寫的 "TensorFlow+Keras 深度學習人工智慧實務應用" 一書第 10 章進行測試並紀錄測試結果. 為簡略計不再於 IDLE 介面執行, 而是修改上一篇測試所用之程式直接在命令列執行, 主要是加入第三個卷積層, 每一個卷積重複兩次, 同時 MLP 添加第二個隱藏層, 模型之結構如下圖所示 :




完整之測試程式如下 :

#show_cifar10_deep_cnn3_predict.py
#載入資料集
import numpy as np
np.random.seed(10)
from keras.datasets import cifar10
(x_train_image, y_train_label), (x_test_image, y_test_label)=cifar10.load_data()

#資料預處理
x_train_normalize=x_train_image.astype('float32')/255.0
x_test_normalize=x_test_image.astype('float32')/255.0
from keras.utils import np_utils
y_train_onehot=np_utils.to_categorical(y_train_label)
y_test_onehot=np_utils.to_categorical(y_test_label)

#建立模型
#建立三層卷積 (丟棄 30% 神經元) + 池化層, 每個卷積做兩次
from keras.models import Sequential
from keras.layers import Dense,Dropout,Flatten,Conv2D,MaxPooling2D
from keras.layers import ZeroPadding2D,Activation
model=Sequential()
#卷積層1+池化層1
model.add(Conv2D(filters=32,
                 kernel_size=(3,3),
                 padding='same',
                 input_shape=(32,32,3),
                 activation='relu'))
model.add(Dropout(0.3))
model.add(Conv2D(filters=32,
                 kernel_size=(3,3),
                 padding='same',
                 activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
#卷積層2+池化層2
model.add(Conv2D(filters=64,
                 kernel_size=(3,3),
                 padding='same',
                 activation='relu'))
model.add(Dropout(0.3))
model.add(Conv2D(filters=64,
                 kernel_size=(3,3),
                 padding='same',
                 activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
#卷積層3+池化層3
model.add(Conv2D(filters=128,
                 kernel_size=(3,3),
                 padding='same',
                 activation='relu'))
model.add(Dropout(0.3))
model.add(Conv2D(filters=128,
                 kernel_size=(3,3),
                 padding='same',
                 activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
#建立分類模型 MLP
model.add(Flatten())                      #平坦層
model.add(Dropout(0.3))
model.add(Dense(2500,activation='relu'))  #隱藏層1
model.add(Dropout(0.3))
model.add(Dense(1500,activation='relu'))  #隱藏層2
model.add(Dropout(0.3))
model.add(Dense(10,activation='softmax')) #輸出層
model.summary()

#訓練模型
import time
start=time.time()
model.compile(loss='categorical_crossentropy',optimizer='adam',metrics=['accuracy'])
train_history=model.fit(x=x_train_normalize, y=y_train_onehot, validation_split=0.2, epochs=10, batch_size=128,verbose=2)
elapsed=time.time()-start
print("Training time=" + str(elapsed) + " Seconds")
#繪製訓練結果
def show_train_history(train_history):
    fig=plt.gcf()
    fig.set_size_inches(16, 6)
    plt.subplot(121)
    plt.plot(train_history.history["acc"])
    plt.plot(train_history.history["val_acc"])
    plt.title("Train History")
    plt.xlabel("Epoch")
    plt.ylabel("Accuracy")
    plt.legend(["train", "validation"], loc="upper left")
    plt.subplot(122)
    plt.plot(train_history.history["loss"])
    plt.plot(train_history.history["val_loss"])
    plt.title("Train History")
    plt.xlabel("Epoch")
    plt.ylabel("Loss")
    plt.legend(["train", "validation"], loc="upper left")
    plt.show()
import matplotlib.pyplot as plt
show_train_history(train_history)

#評估預測準確率
scores=model.evaluate(x_test_normalize, y_test_onehot)
print("Accuracy=", scores[1])

#預測測試集圖片
prediction=model.predict_classes(x_test_normalize)
print(prediction)


注意, 程式中我使用 time 模組的 time() 函數來計算模型訓練所需時間.

程式執行結果如下 :

D:\Python\test>python show_cifar10_cnn3_predict.py
Using TensorFlow backend.
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
conv2d_1 (Conv2D)            (None, 32, 32, 32)        896
_________________________________________________________________
dropout_1 (Dropout)          (None, 32, 32, 32)        0
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 32, 32, 32)        9248
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 16, 16, 32)        0
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 16, 16, 64)        18496
_________________________________________________________________
dropout_2 (Dropout)          (None, 16, 16, 64)        0
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 16, 16, 64)        36928
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 8, 8, 64)          0
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 8, 8, 128)         73856
_________________________________________________________________
dropout_3 (Dropout)          (None, 8, 8, 128)         0
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 8, 8, 128)         147584
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 4, 4, 128)         0
_________________________________________________________________
flatten_1 (Flatten)          (None, 2048)              0
_________________________________________________________________
dropout_4 (Dropout)          (None, 2048)              0
_________________________________________________________________
dense_1 (Dense)              (None, 2500)              5122500
_________________________________________________________________
dropout_5 (Dropout)          (None, 2500)              0
_________________________________________________________________
dense_2 (Dense)              (None, 1500)              3751500
_________________________________________________________________
dropout_6 (Dropout)          (None, 1500)              0
_________________________________________________________________
dense_3 (Dense)              (None, 10)                15010
=================================================================
Total params: 9,176,018
Trainable params: 9,176,018
Non-trainable params: 0
_________________________________________________________________
Train on 40000 samples, validate on 10000 samples
Epoch 1/10
2018-04-16 09:58:59.432800: I C:\tf_jenkins\workspace\rel-win\M\windows\PY\36\tensorflow\core\platfo
rm\cpu_feature_guard.cc:137] Your CPU supports instructions that this TensorFlow binary was not comp
iled to use: AVX AVX2
 - 412s - loss: 1.8321 - acc: 0.3131 - val_loss: 1.6129 - val_acc: 0.4149
Epoch 2/10
 - 415s - loss: 1.3994 - acc: 0.4883 - val_loss: 1.2652 - val_acc: 0.5418
Epoch 3/10
 - 402s - loss: 1.2031 - acc: 0.5636 - val_loss: 1.1099 - val_acc: 0.6075
Epoch 4/10
 - 398s - loss: 1.0691 - acc: 0.6193 - val_loss: 0.9544 - val_acc: 0.6652
Epoch 5/10
 - 397s - loss: 0.9675 - acc: 0.6552 - val_loss: 0.9089 - val_acc: 0.6871
Epoch 6/10
 - 397s - loss: 0.8953 - acc: 0.6818 - val_loss: 0.8561 - val_acc: 0.6951
Epoch 7/10
 - 397s - loss: 0.8211 - acc: 0.7098 - val_loss: 0.8310 - val_acc: 0.7025
Epoch 8/10
 - 397s - loss: 0.7664 - acc: 0.7281 - val_loss: 0.7770 - val_acc: 0.7261
Epoch 9/10
 - 397s - loss: 0.7188 - acc: 0.7458 - val_loss: 0.7487 - val_acc: 0.7370
Epoch 10/10
 - 398s - loss: 0.6786 - acc: 0.7602 - val_loss: 0.7298 - val_acc: 0.7467
Training time=4010.7789998054504 Seconds
10000/10000 [==============================] - 28s 3ms/step
Accuracy= 0.7403

訓練結果 train_history 繪圖如下 :




結果還是 0.74 左右, 花了 4010 秒 (約 1 小時又 7 分鐘) 運算不是白忙一場嗎? 但書中作者的預測結果卻能提升到 0.7889, Why? 

參考 :

Deep learning for complete beginners: convolutional neural networks with keras

2018-04-16 補充 :

我用另一台電腦跑出 0.7552 準確率 :

D:\Python\test>python show_cifar10_cnn3_predict.py
Using TensorFlow backend.
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
conv2d_1 (Conv2D)            (None, 32, 32, 32)        896
_________________________________________________________________
dropout_1 (Dropout)          (None, 32, 32, 32)        0
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 32, 32, 32)        9248
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 16, 16, 32)        0
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 16, 16, 64)        18496
_________________________________________________________________
dropout_2 (Dropout)          (None, 16, 16, 64)        0
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 16, 16, 64)        36928
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 8, 8, 64)          0
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 8, 8, 128)         73856
_________________________________________________________________
dropout_3 (Dropout)          (None, 8, 8, 128)         0
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 8, 8, 128)         147584
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 4, 4, 128)         0
_________________________________________________________________
flatten_1 (Flatten)          (None, 2048)              0
_________________________________________________________________
dropout_4 (Dropout)          (None, 2048)              0
_________________________________________________________________
dense_1 (Dense)              (None, 2500)              5122500
_________________________________________________________________
dropout_5 (Dropout)          (None, 2500)              0
_________________________________________________________________
dense_2 (Dense)              (None, 1500)              3751500
_________________________________________________________________
dropout_6 (Dropout)          (None, 1500)              0
_________________________________________________________________
dense_3 (Dense)              (None, 10)                15010
=================================================================
Total params: 9,176,018
Trainable params: 9,176,018
Non-trainable params: 0
_________________________________________________________________
Train on 40000 samples, validate on 10000 samples
Epoch 1/10
 - 685s - loss: 1.8330 - acc: 0.3154 - val_loss: 1.6589 - val_acc: 0.3792
Epoch 2/10
 - 638s - loss: 1.3598 - acc: 0.5026 - val_loss: 1.3293 - val_acc: 0.5270
Epoch 3/10
 - 635s - loss: 1.1501 - acc: 0.5855 - val_loss: 1.0248 - val_acc: 0.6400
Epoch 4/10
 - 690s - loss: 1.0238 - acc: 0.6368 - val_loss: 0.9540 - val_acc: 0.6686
Epoch 5/10
 - 736s - loss: 0.9270 - acc: 0.6697 - val_loss: 0.8981 - val_acc: 0.6858
Epoch 6/10
 - 667s - loss: 0.8542 - acc: 0.6982 - val_loss: 0.8487 - val_acc: 0.7018
Epoch 7/10
 - 704s - loss: 0.7938 - acc: 0.7179 - val_loss: 0.7624 - val_acc: 0.7340
Epoch 8/10
 - 673s - loss: 0.7427 - acc: 0.7371 - val_loss: 0.7709 - val_acc: 0.7295
Epoch 9/10
 - 696s - loss: 0.7013 - acc: 0.7507 - val_loss: 0.7527 - val_acc: 0.7375
Epoch 10/10
 - 675s - loss: 0.6597 - acc: 0.7637 - val_loss: 0.6890 - val_acc: 0.7587
Training time=6801.2324249744415 Seconds
10000/10000 [==============================] - 51s 5ms/step
Accuracy= 0.7552




即使如此, 準確率似乎也沒有顯著提升.