python 群益API SKCOM工具

SKCOM-tool Github

python 群益API SKCOM工具 版本(API):2.13.20

檢查三格位置的SKCOM.dll版本差異與位置: 1.當前的COM元件 2.系統已註冊的COM元件 3.comtypes client所用的COM元件

並且給予相對應的建議動作。 方便查詢出目前電腦上所使用的版本。

「三個位置的SKCOM都是最新版本就沒有問題。」

若不需要GUI,執行「F.py」,查看輸出即可。


昨晚突然看到了自己有存下一個別人用python寫的SKCOM工具,仔細看了一下代碼,覺得那對我來說並不到能接受的標準,方向不是最正確的。

但是我也忘記是從哪裡找到的,不過我寫的SKCOM工具主體也不是參考它為主。

對我來說,我覺得「不清不楚」是最大的問題根源,COM版本差異可能會造成執行上的問題,所以這個SKCOM工具揭露的資訊就是為了不要不清不楚。

對於SKCOM.dll的版本檢測,最理想的狀態就是三者合一,都是最新版本的COM。

然而SKCOM.dll位置未必會相同,也未必會使用使用群益API的套件,也許換一個地方執行,換一個程式執行,也許會對某些部分就會產生不同的結果。

譬如說你有個程式A使用舊元件,程式B使用新元件,因為在官方範例寫法會把COM元件放在與py檔同一資料夾,也許你就無意之間造成了各種COM版本差異。

用這個工具看看,看到各位置的COM資訊,就能採取對應行動確保彼此間是一致的。

python 初學者速理解 yield與generator

這我很久以前碰過,但是也不清不楚,return 與 yield 都能回傳值,那為什麼有甚麼區分?

yeild用於function中回傳值,回傳的類型是產生器generator,這個產生器會產生回傳值,但本身並不是想要的回傳值。

所以很直覺地使用len()或是[:]切片在產生器上,會回報錯誤。

yeild產生器像是在函式中的斷點,可以得到當下變數的數值。意味著你可以在函式外影響函式內的值,也會直接牽連到yield。

要從產生器內取出回傳值,用 next() 或用 for迴圈 都可以,for迴圈會跑到產生器停止,無法再用next()取得值就跳出。

而generator進度是會保存下來的,因此一旦全部跑完一次,想要重頭,那就得重新產生generator一次。ex1a(Github)

yield能用在哪?從generator取值感覺並不直覺好用。

當資料量小的時候,使用時間並不會很明顯,一旦資料量變大,就能看出yield在某些時刻能起到龐大的優勢。

這裡假設一個情境:批次batch。 ex1(Github)

有一個長度10000000的串列,要切成每份大小10的串列,然後取出。(這算是一維變成二維,用numpy也可以輕易做到,萬一無法整除呢?

不過呢,當你生成一個新的(1000000,10)大小的二維串列,也代表於記憶體內有兩份一樣並不同形式的資料。

批次是為了解決一次性傳輸碰上量大問題,切小再送避免卡頓。在量大的前提,要是再生出一份對應整理好的資料,也勢必會影響執行時間。

用yield去做切割,原資料不變,也不會產生整理過的新資料,相對就會快上許多。

"""https://stackoverflow.com/questions/8290397/how-to-split-an-iterable-in-constant-size-chunks"""
def batch_yield(iterable, n=1):
    l = len(iterable)
    for ndx in range(0, l, n):
        yield iterable[ndx:min(ndx + n, l)]

如果 range(10) 要切成每份大小為 3, batch_yield(range(10), n=3)。

用 sys.getsizeof() 查看大小,使用generator是比起新產生一個整理過的資料還省下不少空間,執行速度也相對更快速。

不過「批次」這樣情形不一定得要用到yield,用[:]迴圈也可以不佔用太多記憶體空間,只是yield可以展開過程,會比較容易理解。

因為yield可以先取部分,再判斷情況去影響函式內部,讓generator提前結束或是再延長,也就是說長度可以變動,所以len()跟[:]就派不上用場。

講到一個經典情況:抽球。 程式碼:Github

今天有一個球池,裡面一開始只有紅球、黃球。每次抽球前會先攪拌打散順序,要是抽出黃球就放入黃球跟紅球;抽到紅球就放入藍球;抽到藍球就放入綠球;抽到綠球就放入白球;最後抽到白球就結束了。

進行一次 next() 抽球動作generator,會抽出 yield (產生)一個球。請問這個抽球動作長度len()多少?第5次抽球[4]是甚麼顏色?沒有產生結果之前,誰也說不準。

(球池補充,使用類別:MyBallPool(GitHub) MyBallPool2(GitHub)

如果有更多的狀況、更多的條件判斷與回饋,用上function yeild就能變成一行呼叫函式的代碼,這能使得版面看起來整潔許多。

接下來講的常見的迴圈加刪除的例子,個人認為對新手是相當難理解的。

a = list(range(10)) # a = [0,1,...,9]

for item in a:
    print(item, end = " ")
    item = 9999
    del a[0]

print("\n" + " ".join([str(item) for item in a]))   

->0 2 4 6 8
5 6 7 8 9

新手可能覺得這個for迴圈會執行10次,但是結果只執行了5次。

這是不是跟 yeild 外部影響內部非常相似?想一想,「for item in a」的 a,到底是那串資料還是另有甚麼?

其實是從 a 中取出 a.__iter__() 的回傳值:迭代器iterator。這個迭代器用 next() 依序傳值,運作跟for index in range(len(a)) a[index]是類似的。ex2(Github)

由於刪除串列中其中一個項目,但在迭代器的標籤位置不變,因此下次next()往後一格,也只是標籤位置+1,導致看起來像被跳過一格。 ex2a(Github)

若想用 for item in sequence 這種簡便的寫法循環全部項目,那麼加上[:],改用 for item in sequence[:] 也能辦到,即便中間刪了sequence的某個值,也能老老實實有十個就跑十次。

sequence 跟 sequence[:] 是不一樣的,用id()查詢也能知道,就像 sequence[:5] 明顯就不是 sequence。

因此在迴圈中刪掉 sequence 裡的項目,不影響 sequence[:].__itre__() 的迭代器所使用的資料序列,就能乖乖地跑完十次。 (補充:ex2b(Github) 在function yield中刪資料)

總結:

yield本身並不是想要的回傳值,而是一個產生器generator,能產生函式中的yield斷點的函式內變數。想要從產生器取值,需要使用next()。

因為yield本身並不是回傳值,所以能夠節省空間,像是批次處理。

yield產生的值是回傳當下的函式內變數,因此外部影響函式內部的話,yield的值也會受影響。(反之也能在function yield影響外部

也因為外部能夠影響內部,len長度跟slice切片位置可能會在過程中改變,所以這兩項並不重要。(generator內部也沒有這屬性)

for in 迴圈的運作模式,跟使用yield得到的產生器運作是相似的,因此在for in中增減序列的項目,也會影響For迴圈的執行。

而 for each in sequence,實際上是將 sequence.__iter__()回傳的 iterator 不斷 next() 直到無法取值為止,each 為 next(iterator) 的回傳值。

整篇文章的範例程式碼都收錄在 yield(Github)

個人語:

yield 可以把「for in if」濃縮起來,寫 for in if 除了那一行很占版面,也不容易一眼看出,而且[for in if]會產生新的串列,但  yield 不會。若後續要從中一一取出做運用,當然使用 yield 會是更便捷快速於[for in if]。

如果有一筆A資料,提升效率的關鍵就是不要再產生一份相似的資料。要是沒有很明白到底變數都導向去哪裡,除了可能產生冗贅資料,也可能對資料產生改動而牽動整體,導致實際運行出乎自己的預期。

我也不是很會說物件導向觀念到底哪裡容易讓人誤會,不過一開始學物件導向語言,說 a = b,把 a 指向 b,但是原先 b = 1,接下來 b = 2,但是 a 的值仍是 1 並不是 2 表面上看來也沒有 a = b 去。

雖然寫完這篇,我覺得對一個沒有基礎的初學者,yield還是有一點困難。

因為對於物件導向不是很熟悉的話,就會在各種變數的指向間混淆。yield厲害之處在於不用一次全部跑完,要妥善使用這個特性,在各個指定之間都要清楚掌握資料存在於哪,改動資料會影響所有對該資料的指向,改動指向並不會影響資料。

如果以實體跟指向來看,b並不是實體,b 指向的實體原先是 1, a 要是單純地指向 b 是沒有任何實體的,所以 a 的指向會跟 b 一樣是實體1。如果 b 是實體,像是類別物件等, 那麼 a = b之後,修改 b 也會反映到 a 上。

這裡用「實體」是想給人這是一種真實存在的東西說法,正確該說是「物件」。

int(3)都會指向同一個 id,不論 a=3、b=4-1、c=9/3,abc三者的id會是跟int(3)一樣的。(abc三者都是int的話

虛實問題,也反應到各種傳遞資料問題,到底傳的是實體?還只是一個指向?少用 = ,多用內部方法增修清除。串列是一個可修改自己的實體,裡面是一連串的指向。字串是不可修改自己的實體(設計上),增修則是變成新的實體回傳。

如果把類別的觀念擴大至整個架構,就會懂得 a = int(1) + int(3) ,其中 + 是呼叫該類別 int 的 __add__ ,得到回傳值 int(4)。a 也就指向新的實體 int(4)。

為什麼 a = int(1)、b = a、a = int(1) + int(3)、b = int(1)。一開始,a 指向 int(1)、b 也透過 a 指向 int(1),對 int 加減乘除會得到(回傳)一個 int 的答案,a 指向新的值,並沒有對 int(1) 做修改,故 b 仍然為 int(1)。

也許有天寫 leetcode 的時候,不明白為什麼運行時間這麼慢的時候,就能想想這個物件導向的虛實問題,看看自己的程式碼是不是產生過多不必要的實體。

PYTHON 群益API 2.13.20 錯誤代碼2017 錯誤原因與解法

從2.13.16版本之後,登入前就需要 SKReplyLib_OnReplyMessage 回傳指定值 0xFFFF 才可以登入。

一開始我也是矇了,不管怎麼傳就好像沒有作用?明明照著說明做了啊!
錯誤通常分兩種,第一種就是真的忘記寫到 OnReplyMessage 然後回傳 0xFFFF或寫錯,這種就不在這裡討論。第二種就是都按照說明寫了也確認無誤,但還是不知所以然的錯誤。
到底  2017 SK_WARNING_REGISTER_REPLYLIB_ONREPLYMESSAGE_FIRST 是從何而來?網路上沒有一個明確的解釋與通用的解法,這可以是基於討論的人並不多。
的確,確實回傳值設甚麼都沒有作用,那不是錯覺,那是真的。COM接口的Py檔中 OnReplyMessage 是沒有回傳值。
於是這形成了最怪異的事情,不管怎麼樣登入,由於接口的 OnReplyMessage 沒有回傳值 sConfirmCode,所以 SKCenterLib_Login 永遠都接收不到註冊公告,永遠的錯誤代碼 2017。
問題點是錯在哪裡?本來有個見解,在 2020-03-20 被提醒之後,終於確認是暫存檔的問題。(不是dll

執行以下幾行就能夠刪除掉舊版COM的py檔模型。

import comtypes.client, os
try:
    os.remove(comtypes.client.gen_dir + r"\_75AAD71C_8F4F_4F1F_9AEE_3D41A8C9BA5E_0_1_0.py")
    os.remove(comtypes.client.gen_dir + r"\SKCOMLib.py")
except:
    pass
繼續追根究柢下去。
來講解一下運作機制。
在python中,最初的三行通常是:
import comtypes.client
comtypes.client.GetModule(os.path.split(os.path.realpath(__file__))[0] + r'\SKCOM.dll') #加此行需將API放與py同目錄
import comtypes.gen.SKCOMLib as sk
第二行是從 SKCOM.dll 取得模型,顧名思義。而第三行就是取出第二行建立的模型。

然而 GetModule() 會先從 comtypes.client.gen_dir 路徑上尋找模型,如果路徑上已經有對應的模型,就會優先讀入,沒有才會從COM中生成py檔模型。

如果以前曾經使用過  GetModule(舊版COM),那麼就會優先讀取已存在的舊版COM的py模型,而不是從新版COM去創造一個新的覆蓋掉舊的,這也就是版本差異的所導致的錯誤。

透過 print(comtypes.client.gen_dir) 可以看到模型py檔放在哪裡。

有人會在快取夾 cache,有人會在 anaconda 中 /Lib/site-packages/comtypes/gen裡。複製貼上路徑,可以找到兩個檔案 SKCOMLib.py 及 _75AAD71C-8F4F-4F1F-9AEE-3D41A8C9BA5E.py 這就是從 SKCOM.dll 生出的 .py檔,也是這次的目標。

兩者位置差異在於該專案的解譯器使用甚麼環境,從專案→屬性就能查看到。Visual Studio可供選安裝的環境通常會是在 C:\Users\使用者名稱\AppData\Roaming\Python\ 裡面,如果是官網下載的Anaconda,則會在 anaconda環境/Lib/site-packages/comtypes/gen。

刪了檔案之後,重新執行 GetModule(新版COM),就能產生新版COM的Py檔,這樣便解決到舊版COM的Py檔在OnReplyMessage沒有回傳值( [‘out’], c_short, ‘sConfirmCode’ ),所導致的錯誤代碼2017。

就能順利運行了。

雖然一開始有找到問題所在,但是我卻誤認了真相。被提醒COM版本差異之後,就回頭更注重比對不同COM版本的差異,確實如提醒所指的方向去找就把不確定性解決了。

昨日基於一個忽略,就沒有測驗不同COM版本產生的Py檔模型差異。忘記刪除掉重新產生,只在乎修改,卻沒有發覺使用GetModule並沒有影響到模型py檔,沒有被修改,沒有產生新模型去覆蓋掉舊的,更應該去注意修改時間。

網路上是有人說刪掉gen資料夾,就能順利運作,但是不一定會有這個資料夾啊!當時我用VS的Anaconda,在comtypes裡並沒有gen這個資料夾,當時沒有想知道真相的衝勁,所以也就沒有花時間多思考為什麼可行。

如今可以解釋了,不確定也在提醒之後被解決了,當初是覺得.dll -> .py 錯誤,最後只有.py錯誤。正也因為確認錯誤在哪裡,終於也有了一套滿意的解決方法,能適用於各種的python環境。

思考自己的在尋找答案的過程,一有甚麼想法就去試,如果可行很快就會結束。也就沒有好好釐清各種可能的時間,排序去做確認。

如果先入為主,先設想認為dll問題,所以找解法的過程才不會刪除補好的檔案。
但如果先把其他人的方法整理,自然就會認為是暫存檔的問題,那就會先試刪掉後再用新版COM產生。

兩者同樣都能解決錯誤,但是解決完成度是不一樣的。

官方Documentation並沒有提到會保存檔案且優先讀入已有檔案,或是可供選擇覆寫掉,如果有,那會更快找到答案,直找documentation是優先得順位。所以看來還是不得不進入看看機制為何。

從package裡順一遍邏輯再得出設想,是最硬的,不過也是最根本的,如果當初有更深入去撈,也會發現它會優先讀入已有檔案吧,並不是產生新的覆蓋過去。

Python 群益API Error in sys.excepthook

從一開始使用群益API時,就經常收到這個錯誤輸出。

Error in sys.excepthook:

Original exception was:

不以為意,因為運行不會受到任何影響。

關於這個錯誤,用GOOGLE搜尋會找到一篇文章,有做探討。

確實是直指出錯誤問題來源,但是並沒有解釋到底為什麼會出錯。

前幾天著手多進程,因此更瞭解當中的運作模式,這篇會更詳細講看法與解法。


首先要來到基本運作解釋:

 comtypes.client.GetEvents(),是可以把COM event傳到對應接收函式。

根據官方文件 COM events,comtypes.client.GetEvents() 會收到一個連接物件 ,只要保持這個物件,就能保持狀態。

群益API範例沒有寫很多,後來看確實都有意義在的。

當登入報價之後,若是只有使用 time.sleep(),是會收不到回傳資料。

於是使用 pythoncom.PumpWaitingMessages(),好讓那些訊息有進到到主線程的時間。

訊息跟主線程並不在同一條線程上,因此可想有個子線程專門接收資料,到時候在送到對應的接收函式。

comtypes.client.GetEvents()的回傳值,就相當於一個這樣功能的子線程。

重新回到 Error in sys.excepthook 錯誤上,從網路上可以很容易查到解釋,就是主線程關掉了,但子線程卻還在運行。

這個物件的有無,對應著子線程運行有無,這可以解釋探討錯誤該文章的十八格狀態。

為什麼函式內用 comtypes.client.GetEvents() 就不會報錯,而在全域用就會報錯?函式結束時,函式內部變數也就被刪除,所以變數指定透過 comtypes.client.GetEvents() 得到的連接物件也就被刪除,連接子線程跟著中止。

在全域使用,在主線程結束時,自然是不會刪去這個連接物件。

要刪去這個連接物件,只需要使用 del 就能刪除了,是相當簡單的。

導致錯誤的關鍵點,就只是有沒有在主線程結束前刪掉這個連接物件。

其實不論在哪個域或類別中使用,也只需要在主線程結束之前,刪掉這個物件,或從類別中刪除這個屬性,就不會報錯了。

Python 資料讀寫方法比較與資料壓縮比較 (二) 多進程

上一回邊寫時就在思考多進程是否能夠幫助提升資料讀寫的速度。

答案是否定的,為什麼?
首先是資料大小,資料規模不到一定程度,光是多進程的準備時間就輸了一大截。
接著是沒有考慮到資料轉傳,測試皆採取原地寫入原地讀出,未經壓縮的資料寫入比較快,同時讀出也比較快,即便它的容量可能是上百倍甚至更多,省下處理的時間就有優勢。
多進程的優勢在於啟動更多的 cpu 計算能力,對於 壓縮 與 解壓縮 需要用到 cpu 計算的,如果需要越多計算,多進程的優勢就會能體現。如 lzma 。

此外,將越多任務交給多進程處例,有助於提升速度。例如把資料json化、bytes化、壓縮化及解壓縮等盡可能在各進程上執行,主線程只是發包資料到各進程去。

雖然讀取也可以多進程(搭配多進程寫入成不同檔案),但是往往輸給主線程直接一一讀取,因為多進程的讀取結果最後還是得回到一個主線程上顯示,這使得多進程讀取最後還是會回到單核上,失去了多核的優勢。

多進程在寫入的時候,可以不等待寫完,就讓主線程繼續到讀取階段。如果資料量一大,會碰到讀取時多進程卻沒有寫完而報錯,解決方法是在多進程讀取中做判斷。由於這是寫入時偷跑,看似寫入時間少很多是假象,少掉的時間會跑到讀取時間。

mp開頭代表使用多進程,bj 和 zl 和 xz 代表主要方法,後面  j  與 z 代表多進程執行 json 與 zip 。「n」代表不等待寫完的  no wait。

像是 mpzlzjn,使用 mp 多進程 ,主要方法為 zl(Json Zlib) ,每個進程 zip 壓縮 json 化且 no wait 寫完就繼續到讀取,就有優於 json zlib單核與其他多核但多核任務量不同的速度。

當使用多進程的時候,在運行中修改目標函式的代碼,也會直接反應到目前運行的當中,這表示多進程的是從實體取出目標函式。這是我在運行中目標函示中添加sleep時,直接觀察到數據有立即的反應。

多進程是使用上也有一定的局限,使用上來說不是那麼直接。如果沒有容量限制,也沒有轉傳的需要,用 bstr json 是最好的方法。

bstr json 是將資料 bytes(json.dumps(data), encoding = “acsii”) 用 ‘wb’ 寫入 f,再用 ‘rb’ 開啟 f 以 json.loads(f) 讀出。跟 str json 的差別在於寫入 f 時是用 ‘w’ 而 bstr json 是 ‘wb’。

如果需要轉傳,使用 mpzljzn 會是非常棒的方法,光是每秒 1 MB,用 bstr json 就不知道要傳多久。而 mpxzz系列只能在可憐的(< 1MB/s)傳輸速度上有優於 mpzljzn,考量到方便性,用 zl 而不使用多核也不錯。

""" 100萬筆資料寫讀
data = [(i,"阿明", "身體狀況", {"體重":78.9, "肝功能": "正常"}, 90, 85, 97, 81, 85, True) for i in range(100000)]
[Statistics]
Method: bstr json, Compress Ratio: 0.06:1
Average Use Time: avgWtime:2.46935, avgRtime:3.14803, avgUtime:5.61739
Data size: 8697464(8.49MB), Wspeed:3.43961 MB/s, Rspeed:2.69807 MB/s, Uspeed:1.51202 MB/s
File size: 141888890(138.56MB), Wspeed:56.11319 MB/s, Rspeed:44.01585 MB/s, Uspeed:24.66687 MB/s
Method: json zlib, Compress Ratio: 2.79:1
Average Use Time: avgWtime:2.35145, avgRtime:3.29203, avgUtime:5.64348
Data size: 8697464(8.49MB), Wspeed:3.61208 MB/s, Rspeed:2.58005 MB/s, Uspeed:1.50503 MB/s
File size: 3117715(3.04MB), Wspeed:1.29480 MB/s, Rspeed:0.92485 MB/s, Uspeed:0.53950 MB/s
Method: json xz, Compress Ratio: 71.11:1
Average Use Time: avgWtime:14.73717, avgRtime:3.62832, avgUtime:18.36548
Data size: 8697464(8.49MB), Wspeed:0.57634 MB/s, Rspeed:2.34093 MB/s, Uspeed:0.46248 MB/s
File size: 122316(0.12MB), Wspeed:0.00811 MB/s, Rspeed:0.03292 MB/s, Uspeed:0.00650 MB/s
Method: mpbj, Compress Ratio: 0.06:1
Average Use Time: avgWtime:2.60734, avgRtime:3.34684, avgUtime:5.95418
Data size: 8697464(8.49MB), Wspeed:3.25758 MB/s, Rspeed:2.53781 MB/s, Uspeed:1.42650 MB/s
File size: 141888890(138.56MB), Wspeed:53.14356 MB/s, Rspeed:41.40131 MB/s, Uspeed:23.27163 MB/s
Method: mpbjn, Compress Ratio: 0.06:1
Average Use Time: avgWtime:2.19224, avgRtime:3.98733, avgUtime:6.17957
Data size: 8697464(8.49MB), Wspeed:3.87440 MB/s, Rspeed:2.13015 MB/s, Uspeed:1.37447 MB/s
File size: 141888890(138.56MB), Wspeed:63.20630 MB/s, Rspeed:34.75091 MB/s, Uspeed:22.42282 MB/s
Method: mpbjj, Compress Ratio: 0.06:1
Average Use Time: avgWtime:2.05410, avgRtime:3.13490, avgUtime:5.18900
Data size: 8697464(8.49MB), Wspeed:4.13496 MB/s, Rspeed:2.70937 MB/s, Uspeed:1.63685 MB/s
File size: 141888890(138.56MB), Wspeed:67.45707 MB/s, Rspeed:44.20021 MB/s, Uspeed:26.70329 MB/s
Method: mpbjjn, Compress Ratio: 0.06:1
Average Use Time: avgWtime:0.06299, avgRtime:4.78647, avgUtime:4.84947
Data size: 8697464(8.49MB), Wspeed:134.83090 MB/s, Rspeed:1.77450 MB/s, Uspeed:1.75145 MB/s
File size: 141888890(138.56MB), Wspeed:2199.60744 MB/s, Rspeed:28.94895 MB/s, Uspeed:28.57290 MB/s
Method: mpzl, Compress Ratio: 2.95:1
Average Use Time: avgWtime:2.93715, avgRtime:3.43182, avgUtime:6.36897
Data size: 8697464(8.49MB), Wspeed:2.89179 MB/s, Rspeed:2.47496 MB/s, Uspeed:1.33359 MB/s
File size: 2950294(2.88MB), Wspeed:0.98093 MB/s, Rspeed:0.83954 MB/s, Uspeed:0.45237 MB/s
Method: mpzlz, Compress Ratio: 2.79:1
Average Use Time: avgWtime:2.57715, avgRtime:3.46488, avgUtime:6.04203
Data size: 8697464(8.49MB), Wspeed:3.29574 MB/s, Rspeed:2.45134 MB/s, Uspeed:1.40575 MB/s
File size: 3118214(3.05MB), Wspeed:1.18159 MB/s, Rspeed:0.87886 MB/s, Uspeed:0.50399 MB/s
Method: mpzlzn, Compress Ratio: 2.79:1
Average Use Time: avgWtime:2.19316, avgRtime:3.75349, avgUtime:5.94665
Data size: 8697464(8.49MB), Wspeed:3.87278 MB/s, Rspeed:2.26286 MB/s, Uspeed:1.42830 MB/s
File size: 3118214(3.05MB), Wspeed:1.38847 MB/s, Rspeed:0.81128 MB/s, Uspeed:0.51207 MB/s
Method: mpzljz, Compress Ratio: 2.79:1
Average Use Time: avgWtime:2.07069, avgRtime:3.32371, avgUtime:5.39440
Data size: 8697464(8.49MB), Wspeed:4.10182 MB/s, Rspeed:2.55547 MB/s, Uspeed:1.57452 MB/s
File size: 3117371(3.04MB), Wspeed:1.47019 MB/s, Rspeed:0.91594 MB/s, Uspeed:0.56435 MB/s
Method: mpzljzn, Compress Ratio: 2.79:1
Average Use Time: avgWtime:0.04629, avgRtime:4.76512, avgUtime:4.81140
Data size: 8697464(8.49MB), Wspeed:183.49081 MB/s, Rspeed:1.78246 MB/s, Uspeed:1.76531 MB/s
File size: 3117371(3.04MB), Wspeed:65.76732 MB/s, Rspeed:0.63887 MB/s, Uspeed:0.63273 MB/s
Method: mpxz, Compress Ratio: 71.11:1
Average Use Time: avgWtime:14.85270, avgRtime:4.02111, avgUtime:18.87381
Data size: 8697464(8.49MB), Wspeed:0.57186 MB/s, Rspeed:2.11226 MB/s, Uspeed:0.45002 MB/s
File size: 122316(0.12MB), Wspeed:0.00804 MB/s, Rspeed:0.02971 MB/s, Uspeed:0.00633 MB/s
Method: mpxzz, Compress Ratio: 69.15:1
Average Use Time: avgWtime:7.99850, avgRtime:2.42350, avgUtime:10.42200
Data size: 8697464(8.49MB), Wspeed:1.06190 MB/s, Rspeed:3.50469 MB/s, Uspeed:0.81497 MB/s
File size: 125776(0.12MB), Wspeed:0.01536 MB/s, Rspeed:0.05068 MB/s, Uspeed:0.01179 MB/s
Method: mpxzzj, Compress Ratio: 71.62:1
Average Use Time: avgWtime:6.89620, avgRtime:3.59600, avgUtime:10.49220
Data size: 8697464(8.49MB), Wspeed:1.23164 MB/s, Rspeed:2.36197 MB/s, Uspeed:0.80952 MB/s
File size: 121444(0.12MB), Wspeed:0.01720 MB/s, Rspeed:0.03298 MB/s, Uspeed:0.01130 MB/s
Method: mpxzzjn, Compress Ratio: 71.62:1
Average Use Time: avgWtime:0.04867, avgRtime:8.94961, avgUtime:8.99828
Data size: 8697464(8.49MB), Wspeed:174.53017 MB/s, Rspeed:0.94905 MB/s, Uspeed:0.94392 MB/s
File size: 121444(0.12MB), Wspeed:2.43699 MB/s, Rspeed:0.01325 MB/s, Uspeed:0.01318 MB/s
"""

透過 win10工作排程器 定時以 potplayer 播放音樂(二)

既上一篇之後,又出現了沒有辦法自動播放的現象。

然而這次就有找到要如何觸發這種沒有辦法自動播放的現象,就是打開別的播放清單,然後方形的停止鍵,等到播放器完整停止後關掉,下次運行以開啟播放清單就不會順利自動播放。

這次又再一次面對這個問題,花時間搜尋,卻發現想找的cmd指令在播放器所在的資料夾有 CmdLine64.txt ,終於可以了解有甚麼cmd指令。

最終嘗試各種結果,我找到了更好的指令。

在工作排程器,工作,內容,動作,改成:

程式碼:PotPlayerMini64.exe
引數:"C:\BGM.dpl"
位置:J:\Program Files\DAUM\PotPlayer\

就不用再掉原本執行的批次檔 (.bat)。 上一篇

已經知道甚麼清況下無法自動播放,就很好測試各種情況下的影響。

雖然cmd指令裡面有 /autoplay 但是並不能順利解決掉自動播放的問題。上一次的引數是使用相對路徑,然而 CmdLine64.txt 裡面的例子是使用絕對路徑。

於是最新版就改成了 絕對路徑 ,同時避免中字出現造成亂碼錯誤而改成全英文路徑。

最後加上壓力測試,連續執行十五次,每一次都能夠順利播放,也不會因為此文章找到無法自動播放的操作方式給影響到。

一個使用絕對路徑,難道就真的是解決自動播放的方式嗎?

再多看看幾天,希望這一次就能夠解決。

Python 資料讀寫方法比較與資料壓縮比較

python內置的讀寫方式是透過 open() 。

如果有一筆資料,例如有五萬筆的串列,要寫入檔案並且讀出,甚麼樣的方式會是最快的?

這裡先寫結論,個人依照讀寫完成速度的私心排行:

「bstr json > json zlib > json gz > str json」
""" 完成速度排行榜
Method: bstr json, Compress Ratio: 0.06:1
Data size: 400064(0.39MB), Wspeed:4.16025 MB/s, Rspeed:2.97668 MB/s, Uspeed:1.73516 MB/s
Method: json zlib, Compress Ratio: 15.32:1, level = 6
Data size: 400064(0.39MB), Wspeed:2.99731 MB/s, Rspeed:2.76445 MB/s, Uspeed:1.43809 MB/s
Method: str json, Compress Ratio: 0.06:1
Data size: 400064(0.39MB), Wspeed:3.16940 MB/s, Rspeed:2.35598 MB/s, Uspeed:1.35141 MB/s
Method: json gz, Compress Ratio: 15.31:1, CompressLevel = 9
Data size: 400064(0.39MB), Wspeed:2.93210 MB/s, Rspeed:2.36939 MB/s, Uspeed:1.31044 MB/s
Method: json xz, Compress Ratio: 330.09:1, preset = 6
Data size: 400064(0.39MB), Wspeed:0.81887 MB/s, Rspeed:1.98104 MB/s, Uspeed:0.57938 MB/s
Method: json json, Compress Ratio: 0.06:1
Data size: 400064(0.39MB), Wspeed:0.57614 MB/s, Rspeed:2.28234 MB/s, Uspeed:0.46002 MB/s
Method: bstr list, Compress Ratio: 0.08:1
Data size: 400064(0.39MB), Wspeed:3.77832 MB/s, Rspeed:0.34935 MB/s, Uspeed:0.31978 MB/s
Method: str str(list), Compress Ratio: 0.10:1
Data size: 400064(0.39MB), Wspeed:3.58643 MB/s, Rspeed:0.33802 MB/s, Uspeed:0.30890 MB/s
Method: str str(tuple), Compress Ratio: 0.10:1
Data size: 400064(0.39MB), Wspeed:3.59002 MB/s, Rspeed:0.33762 MB/s, Uspeed:0.30860 MB/s
Method: json bz, Compress Ratio: 137.76:1, CompressLevel = 9
Data size: 400064(0.39MB), Wspeed:0.22510 MB/s, Rspeed:1.76317 MB/s, Uspeed:0.19962 MB/s
"""

先教大家如何使用 json zlib 壓縮並寫讀資料 (※其他壓縮方法大同小異

import json, zlib

data = [("阿明", "身體狀況", {"體重":78.9, "肝功能": "正常"}, 90, 85, 97, 81,  85, True)]*50000

# 寫入
with open(file, 'wb') as f:
    f.write(zlib.compress(bytes(json.dumps(data), encoding="ascii")))
f.close()

# 讀取
with open(file, "rb") as f:
    data = json.loads(zlib.decompress(f.read()))
f.close()

初學者講解:
json.dumps(data) -> 將資料轉換成json格式的string
bytes(data) -> 將資料 (string or int or list[int]) 轉換成 bytes。int 為正整數限制在 0 – 255間。
zlib.compress(data, level=-1) -> 將資料(bytes)轉換成經zlib壓縮後的帶有壓縮後資料的物件
如此以來就能用 open(file, ‘wb’) 來寫入壓縮後的資料。

zlib.decompress(data) -> 將壓縮後的資料解壓縮還原
json.loads(data) -> 將json格式資料string讀取還原。

範例程式碼:github

由於資料在 cpu 內的處理速度比硬碟讀寫速度快上許多,因此減少輸出檔案大小並讓 cpu 承擔解壓工作,將可以加速資料傳輸速度。

對於 pyhon 內建 讀寫io,最簡單的方式是將資料經 str(data) 後寫入,讀出後用 list(eval(data))還原。

用 open() 讀寫時, bytes讀寫速度都優於 string,使用mode: ‘wb’, ‘rb’ 快於 mode: ‘w’, ‘r’ 。因此將 string 解碼後用 ‘wb’, ‘rb’,雖然寫入的資料容量大小會比用 ‘w’, ‘r’ 大一些,但是會稍微快一點。

除了 str() -> list(eval)之外,另外一種能將一個有複雜結構保存的方法是使用 json。

在 python 中有 json5 跟 json,個人會比較推薦使用 json,因為 json 比較快。兩者在使用上的方法都大同小異,不過此例測試,使用 json5 不僅會報錯,壓縮還慢上數倍。

json的優勢在於它的讀取還原速度,用 json.load(fp) 比一般的 list(eval(read()) 約快上 6-7倍,但是在使用其模組的 json.dump(fp) 寫入io時,比起 open(f, ‘w’) -> f.write(str(data)) 還慢上 6-7倍,而讀取會花比較久的時間,所以 json 整體寫讀是比較快的。

既然 json 的優勢在於讀, 而 內置io 的優勢在於寫,兩者結合 string json 就能讓整體讀寫速度提升到 3-4 倍。 用 內置io  f.write(json.dumps(data)) 寫,用 json 的 json.load(fp) 讀。

""" 輸出檔案大小,依完成速度排列
Method: json zlib, Compress Ratio: 15.32:1, level = 6
Average Use Time: avgWtime:0.13035, avgRtime:0.14133, avgUtime:0.27167
File size: 26112(0.03MB), Wspeed:0.19563 MB/s, Rspeed:0.18043 MB/s, Uspeed:0.09386 MB/s
Method: str json, Compress Ratio: 0.06:1
Average Use Time: avgWtime:0.12327, avgRtime:0.16583, avgUtime:0.28910
File size: 6700000(6.54MB), Wspeed:53.07888 MB/s, Rspeed:39.45637 MB/s, Uspeed:22.63245 MB/s
Method: json gz, Compress Ratio: 15.31:1, CompressLevel = 9
Average Use Time: avgWtime:0.13324, avgRtime:0.16489, avgUtime:0.29813
File size: 26124(0.03MB), Wspeed:0.19146 MB/s, Rspeed:0.15472 MB/s, Uspeed:0.08557 MB/s
Method: json xz, Compress Ratio: 330.09:1, preset = 6
Average Use Time: avgWtime:0.47711, avgRtime:0.19721, avgUtime:0.67432
File size: 1212(0.00MB), Wspeed:0.00248 MB/s, Rspeed:0.00600 MB/s, Uspeed:0.00176 MB/s
Method: bstr list, Compress Ratio: 0.08:1
Average Use Time: avgWtime:0.10340, avgRtime:1.11832, avgUtime:1.22172
File size: 4750000(4.64MB), Wspeed:44.86032 MB/s, Rspeed:4.14789 MB/s, Uspeed:3.79683 MB/s
Method: str str(list), Compress Ratio: 0.10:1
Average Use Time: avgWtime:0.10894, avgRtime:1.15582, avgUtime:1.26476
File size: 4100000(4.00MB), Wspeed:36.75500 MB/s, Rspeed:3.46412 MB/s, Uspeed:3.16575 MB/s
Method: json bz, Compress Ratio: 137.76:1, CompressLevel = 9
Average Use Time: avgWtime:1.73560, avgRtime:0.22158, avgUtime:1.95718
File size: 2904(0.00MB), Wspeed:0.00163 MB/s, Rspeed:0.01280 MB/s, Uspeed:0.00145 MB/s
"""

不過本來資料大小為 400064 bytes(0.39MB),不論用 str() 或是 json.dump(),輸出的檔案都大上十倍多來到 4100000 bytes(4.00MB)、6700000(6.54MB)。因為檔案大,這會使得硬碟讀寫速度影響比較大,這不符合減少硬碟負擔並讓 cup 承擔的想法。

將資料壓縮後寫入,將壓縮後資料讀出後還原。這符合加速資料讀寫的設想。

python的資料壓縮有四個常見套件:zlib, gzip, lzma, bz2。
四者使用上大同小異,不過表現卻各有不同。共同點是,經過壓縮之後,檔案大小都遠小於沒有壓縮過且直接輸出的資料。這裡四者壓縮時皆使用預設的壓縮等級。

資料壓縮後優勢的檔案大小,彌補了在讀取速度上的劣勢,反而更優於沒有壓縮的資料寫讀。

到底影響速度上限,如果是資料壓縮,那麼就是cpu,如果直接輸出,那麼就是硬碟寫讀速度。個人是用SSD硬碟,以下加大資料筆數進行HDD跟SSD的測試讀寫。

""" 資料筆數1000000
Method:[SSD][壓縮] json zlib, Compress Ratio: 15.39:1
Average Use Time: avgWtime:2.45219, avgRtime:3.07391, avgUtime:5.52610
Data size: 8000064(7.81MB), Wspeed:3.18596 MB/s, Rspeed:2.54157 MB/s, Uspeed:1.41376 MB/s
File size: 519928(0.51MB), Wspeed:0.20706 MB/s, Rspeed:0.16518 MB/s, Uspeed:0.09188 MB/s
Method:[HDD] [壓縮]json zlib, Compress Ratio: 15.39:1
Average Use Time: avgWtime:2.47452, avgRtime:3.15085, avgUtime:5.62538
Data size: 8000064(7.81MB), Wspeed:3.15720 MB/s, Rspeed:2.47951 MB/s, Uspeed:1.38881 MB/s
File size: 519928(0.51MB), Wspeed:0.20519 MB/s, Rspeed:0.16114 MB/s, Uspeed:0.09026 MB/s
Method:[SSD][直輸] str json, Compress Ratio: 0.06:1
Average Use Time: avgWtime:2.52982, avgRtime:3.39660, avgUtime:5.92642
Data size: 8000064(7.81MB), Wspeed:3.08819 MB/s, Rspeed:2.30011 MB/s, Uspeed:1.31826 MB/s
File size: 134000000(130.86MB), Wspeed:51.72675 MB/s, Rspeed:38.52662 MB/s, Uspeed:22.08069 MB/s
Method:[HDD] [直輸] str json, Compress Ratio: 0.06:1
Average Use Time: avgWtime:3.08978, avgRtime:3.42463, avgUtime:6.51441
Data size: 8000064(7.81MB), Wspeed:2.52852 MB/s, Rspeed:2.28129 MB/s, Uspeed:1.19927 MB/s
File size: 134000000(130.86MB), Wspeed:42.35231 MB/s, Rspeed:38.21123 MB/s, Uspeed:20.08767 MB/s
"""

在直輸方面,可以看到HDD的速度就比在SSD上慢了。
在壓縮方面,其實在HDD跟SSD上差不多,兩者在誤差範圍內。

資料壓縮的速度上限取決於 cpu,而資料壓縮程度 compresslevel 的高低會有不同的工作量。由於python執行若沒有特殊設定之下,是使用單核,資料壓縮與解壓縮時通常這個單核會吃爆,因此降低壓縮程度,減少單核的運算工作,可以提升資料寫讀速度。

""" 資料筆數1000000,壓縮程度 6 vs 3
Method: json zlib, Compress Ratio: 15.39:1, level = 6
Average Use Time: avgWtime:2.45219, avgRtime:3.07391, avgUtime:5.52610
Data size: 8000064(7.81MB), Wspeed:3.18596 MB/s, Rspeed:2.54157 MB/s, Uspeed:1.41376 MB/s
File size: 519928(0.51MB), Wspeed:0.20706 MB/s, Rspeed:0.16518 MB/s, Uspeed:0.09188 MB/s
Method: json zlib, Compress Ratio: 8.21:1, level = 3
Average Use Time: avgWtime:2.10440, avgRtime:3.07207, avgUtime:5.17648
Data size: 8000064(7.81MB), Wspeed:3.71248 MB/s, Rspeed:2.54309 MB/s, Uspeed:1.50924 MB/s
File size: 974407(0.95MB), Wspeed:0.45218 MB/s, Rspeed:0.30975 MB/s, Uspeed:0.18383 MB/s
"""

對於這四個套件: zlib, gzip, lzma, bz2,在此例上的表現。

壓縮率最好的是 lzma,產生的 .xz ,大小又小於zlib及gzip許多,速度也優於直輸。

然而 bz2,在壓縮上面的速度太劣勢,而且壓縮比在此例也不如 lzma,速度也輸給直輸。

而 zlib 和 gzip 不論在壓縮比跟速度都很接近,因為 gzip 是引入 zlib 用,所以 zlib 速度總是略贏 gzip。

Python @ Property 修飾符 初學者速理解

初學者第一次看到「@」,像我,就滿頭問號。

網路上教學很多,而且不難懂,只是沒有我的風格。(推薦: 官方文件)

@又名Property屬性,property() 是python的內置函數。在定義 類別class 的時候,也經常設定其相關屬性。

Property由四個部分組成:getter setterdeleter、 doc。
以a為例,getter、 setter、 deleter分別對應:

a # call getter
a = 1 # call setter
del a # call deleter

發生以上三種狀況,就會分別執行對應的函式。

依照官方的範例稍微改寫,例1:

class C(object):
    def __init__(self):
        self._x = None
    def getx(self):
        print("得了")
        return self._x
    def setx(self, value):
        print("設了")
        self._x = value
    def delx(self):
        print("刪了")
        del self._x
    x = property(getx, setx, delx, "docstring")

a = C()
a.x # a.getx()
a.x = 1 # a.setx()
del a.x # a.delx()

得到以下輸出。

得了
設了
刪了

使用Property後可以減少代碼,除了例1的寫法,Property可以寫得更簡化。
用上「@」,更快速使用Property,簡化代碼。例1也可以寫成例2:

class C(object):
    def __init__(self):
        self._x = None

    @property
    def x(self):
        print("得了")
        return self._x

    @x.setter
    def x(self, value):
        print("設了")
        self._x = value

    @x.deleter
    def x(self):
        print("刪了")
        del self._x

@property 就是將下方函式定義為 x 的 getter, 然後 x = property(getter),下方函式名 x 就會變成屬性。
看例2,@property 為 getter。 setter也可以寫成@屬性.setter 、 deleter 寫成@屬性.deleter。
getter 最常被使用也一定要設置,setter、 deleter  就不一定要設置。

知道使用 @property 可以將 a.getx() 省略成 a.x 。a.x 就等於 a.getx() 的回傳值。

舉一個實例,如果要定義一個長方形類別,可能會在 __init__() 中,設定其長與寬,然後透過長與寬計算出面積。

class Rectangle():
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.area = width * height

如果 width = 5、 height = 4,那麼 area = 20。

但是,要是後面只改變了 width 或是 height, area 並不會自動更新數值,仍是舊的 20。
如果用上 @property 設定 area,就會在呼叫 area 時,重新計算數值。

class Rectangle():
    def __init__(self, width, height):
        self.width = width
        self.height = height

    @property
    def area(self):
        return self.width * self.height

使用  @property 可將像 area 這類動態數值的過程簡化成屬性名,除了大幅減少代碼篇幅,使用上也變得更直覺,。

學到這裡,講完了property的基本運作,現在來看一下其他教學中的舉例。
如果有好好從前面讀懂,這個範例應該是很好理解,範例3:

def f1(f2):
    return 1

@f1
def f2(x):
    return 2
 f2(1) # Error 

先不講結果,先順著 property 的邏輯看。
@f1,getter 為 f1,f2 = property(f1),下方函式名 f2 變成屬性。
於是 f2 == 「f1的回傳值」 == 1、f2 + f2 == 2。
行7嘗試 f2(1) ,就會報錯,無法理解 f2(1) 為什麼會錯, 就換成 1(1) 理解看看吧。

行1寫成 f1(f2),是方便理解整個 f2函式 都會傳入到 f1。
由於函式內外部差異,可以將傳入的 f2 跟外部的 f2 視作不同的東西。
因此呼叫傳入的 f2 並不會觸發外部帶有@的 f2。

最後範例,如果無法理解,就回過頭看上方的說明吧。範例4:

def addition(f):
    answer = 0
    for num in f():
        answer += num
    return answer

@addition
def threePlusFive():
    return [3, 5]

@addition
def threePlusFivePlusEight():
    return [3, 5, 8]

@addition
def threePlusFivePlusEight2():
    return [threePlusFive, 8]

print(threePlusFive)
print(threePlusFivePlusEight)
print(threePlusFivePlusEight2)

Python pyqt + logging 整合範例1

當有了圖形化介面的時候,發現工作列卡一個 cmd 實在想隱藏起來。
用 pythonw + .pyw,就能不卡 cmd。
但是一旦將 cmd 隱藏起來,就看不到輸出了甚麼。
logging可以將紀錄在輸出並同時保存下來,操作起來比 open() 還便捷。

GUI的操作功能得寫在一塊,格局被侷限了。
如果想整合更多的功能,而且是單獨寫過寫好的部分,或是打開音樂播放器聽音樂與執行某串程式碼等。
大夥都進來專案,會讓專案顯得異常混雜,也可能產生多個同名同功能但不同亞種的函式,搞得你混淆誰新誰舊。
此外,這些功能不需要傳入或只有固定參數,啟動後就能自動獨立運行完成。

那些其他程式運作情形,可以靠logging紀錄,不一定要回傳給主程式。
主程式透過查閱記錄,就能在GUI上觀察到程式進度。

然而可預期的錯誤能被處理,不可預期的錯誤會讓程式跳掉而無法用logging傳出ERROR以上的紀錄。
為了避免看不到錯誤訊息的憾事,得將錯誤訊息輸出保存下來以供來日查閱。

範例1:githubyoutube

展示以下功能:
1.透過整合logging,看見額外程式運行狀況。
2.能篩選紀錄,顯示一定level以上的紀錄。
3.有做錯誤處理且ERROR以上的紀錄,會跳出通知視窗。
4.萬一沒有做錯誤處理,會把錯誤訊息保存下來。

對於這個範例不是的某些部份若不是很瞭解,請找我最近寫了幾篇範例與文章。
我想唯有這個 cmd “python pyfile.py > output.txt 2>&1 “沒有講到。
簡單來說,把pyfile.py的結果標準輸出到output.txt,cmd上有甚麼,txt就大概會有甚麼。
但是沒有錯誤訊息,因為錯誤訊息不在標準輸出裡面。而 2>&1 意思是把錯誤輸出重定向到標準輸出,錯誤訊息才會輸出到output.txt。

程式敘述:

有一個介面,上面有一個清單會顯示過去10秒的紀錄,以及對應四個程式的四個按鈕,按下去會執行對應的程式,及其他按鈕等。

四個程式分別為ProgramA、ProgramB、ProgramC、ProgramD,都有整合logging,會把紀錄傳到主程式查閱的.log檔案。

ProgramA,有錯誤處理,而且有錯誤發生
ProgramB,有錯誤處理,但沒有錯誤發生
ProgramC,沒有錯誤處理,但有錯誤發生
ProgramD,無限循環。

執行這四個程式時,會在 ./error 生成一個 txt ,如果順利執行, txt 的大小會是0,然後被刪除掉。
ProgramA 與 ProgramB 順利執行完畢,而 ProgramC 會報錯,因此 ./error 會有記錄錯誤訊息的 ProgramC 的 txt。
而ProgramD,無限循環,觀察是否使用多核。會。

語:
花了好幾天,一點一滴摸索最終達到這個架構。

如果要做一個任務管理器,執行各種任務,自然得靠多線程,不然會看到主程式執行下去動彈不得就難堪。雖然多線程無法讓主程式使用多核,但用此例方法執行的程式,不會跟主程式擠在同一核,以往寫python需要引入多進程才能用到多核,這方法經測試過後,python程式會自動分配到其他核上。

關於logging的設定想了很久,雖然上一篇教學大開使用的自由度,但是卻沒有規劃一定的架構。從好幾種設定探索,並且探索其可能遇到的問題,最後把能想到的問題都有了解法。

這範例中各程式都引入同一個自訂Logging,這避免產生同名同功能的亞種,不論哪個專案,都是引入這個。之前我就遇到複製太多造成維護不易,更新了一個,還要手動複製貼上到其他的兄弟姊妹實在太費工。

這範例對我來說,就是改善過去不良的架構,為更好維護的架構打下基礎。

Python logging 初學者速理解

給對於 logging 未曾接觸過,或剛接觸過的人。

logging 可以將記錄輸出,也可同時將記錄保存。除錯除了靠print大法,也可以靠logging保存下來的日誌中找出錯誤點。

有些程式運行會因為時間改變環境而出現問題,出現嚴重錯誤就會跳掉。沒有辦法總是在編寫或除錯模式下運行,出錯就告訴你哪裡有問題。若程式未能紀錄到當下運行狀況,就少了線索去發現問題所在。

logging有很多教學,也有繁體中文教學,寫得也不錯。

這裡就簡單寫下一點筆記,幫助初學者快速理解logging運作。


logging有程度(level)之分:最嚴重CRITIAL > ERROR > WARNING > INFO > DEBUG > 最輕微 NOTSET。

假如我只想保留 ERROR 以上的記錄在 log,顯示 INFO 以上的記錄在終端。

假設我有一個程式,命名為 HELLO.py。例1:

Import logging

loggerName = "HELLO"
fileName = "hello.log"

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(loggerName) 
FileHandler = logging.FileHandler(fileName)
FileHandler.setLevel("ERROR")
logger.addHandler(FileHandler)

logger.info("哈囉")
logger.debug("這個不會顯示出來 INFO > DEBUG")
logger.warning("注意下一個記錄")
logger.error("只有這個會被保存在log")

行6,先跳過。
行7,得到一個名字為 “HELLO” 的 Logger 物件。
行8,得到一個 FileHandler 物件。Handler 功能是把 Logger 的記錄傳出去,看官網文件,Handler的種類有很多種,這次用的 FileHandler ,簡單來說就是把 Logger 的記錄傳到指定檔案。
行6,logging基本設定,level是指 root 的 level,會自動生成一個 StreamHandler 添加到 root 上去。上行解釋 Handler 功能,這自動生成的 StreamHandler 會將記錄傳出然後顯示在終端。至於要多高層級的訊息才會顯示,就看這裡設定 root 的 level,之後新增 Logger 默認level也會跟這裡設定的相同 。官網文件
行9,設定 FileHandler 的 level 為 ERROR。
行10,讓 logger 添加 FileHandler。

行12~15,用 logger 傳入不同 level 紀錄,看看實際效果。

INFO:test:哈囉
WARNING:test:注意下一個訊息
ERROR:test:只有這個會被保存在log

複習一下:CRITIAL > ERROR > WARNING > INFO > DEBUG >  NOTSET
或者:CRITIAL(50) > ERROR(40)  > WARNING(30)  > INFO(20)  > DEBUG(10)

因自動生成 StreamHandler 的 level 為 INFO ,故只將行12、行14、行15紀錄輸出到終端。而行13的 level 為 DEBUG,小於 StreamHandler 的 level  INFO,所以被刷掉。
FileHandler 的 level 為 ERROR ,所以只有行15被保存在 hello.log 裡。

注意:這裡顯示在終端的紀錄,是經由 root 的 StreamHandler ,不是 logger 的,這可以利用 Logger.handlers 查看 logger有甚麼 handlers。root 是最上層的 Logger,可觀察到將資料傳入 logger 時,也會往上層傳到 root。

瞭解了 例1 之後,其實我們能把每個 Logger 顯示在終端的 level 有不同設定。如果有兩個 Logger ,一個要 INFO 以上,一個 ERROR 以上,只要分別針對兩者的 StreamHandler 有不同設定。

例2:

import logging

loggerName = "test"
loggerName2 = "test2"
fmt = logging.Formatter("%(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(loggerName)
logger2 = logging.getLogger(loggerName2)
logger.setLevel("DEBUG")
logger2.setLevel("DEBUG")
StreamHandler = logging.StreamHandler()
StreamHandler.setLevel("INFO")
StreamHandler.setFormatter(fmt)
StreamHandler2 = logging.StreamHandler()
StreamHandler2.setLevel("ERROR")
StreamHandler2.setFormatter(fmt)
logger.addHandler(StreamHandler)
logger2.addHandler(StreamHandler2)

for lg in [logger, logger2]:
    print(lg.getEffectiveLevel())
    lg.info("哈囉")
    lg.debug("這個如何")
    lg.warning("那這個呢")
    lg.error("最後一個")

執行結果為:

10
test - INFO - 哈囉
test - WARNING - 那這個呢
test - ERROR - 最後一個
10
test2 - ERROR - 最後一個

這裡沒有用到 logging.basicConfig() ,所以 Logger 的 level 默認為 WARNING。
若不在行8、行9設定 Logger 的 level,結果會變成如下:

30
test - WARNING - 那這個呢
test - ERROR - 最後一個
30
test2 - ERROR - 最後一個

Logger 默認的 level 為 WARNING 換算等級數值為 30,如果有logging.basicConfig()就會先以這裡設定的 level 為主,於是行21 INFO、行22 DEBUG的紀錄在 logger 這關就會被默認的 level WARNING 濾掉,接下來就沒有辦法進到 StreamHandler。

每個 level 都有對應的數值,在 Logger.setLevel() 時,也可以輸入正整數, Logger.setLevel(30) == Logger.setLevel(“WARNING”)。

透過 例2 ,瞭解如何分別設定顯示在終端的條件,自然也可以分別設定輸出檔案跟級別等。

雖然可以分別設定 Logger 但會讓程式碼看起來相當的長,不過也有一個方法,只要兩句,然後就能簡化成 logging.getLogger() 解決掉。

from logging import config
config.fileConfig(configName)

這邊在資料夾要多新增一個檔案名為 logging.conf,裡面寫 Logging 的設定,可以用記事本讀寫。

logging.conf:

[loggers]
keys=root, test, test2

[handlers]
keys=consoleHandler, StreamHandler, StreamHandler2

[formatters]
keys= default

[logger_root]
level=WARNING
handlers=consoleHandler

[logger_test]
level=DEBUG
handlers=StreamHandler
qualname=test
propagate=1

[logger_test2]
level=DEBUG
handlers=StreamHandler2
qualname=test2
propagate=1

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=default
args=(sys.stdout,)

[handler_StreamHandler]
class=StreamHandler
level=INFO
formatter=default
args=(sys.stdout,)

[handler_StreamHandler2]
class=StreamHandler
level=ERROR
formatter=default
args=(sys.stdout,)

[formatter_default]
format=%(name)s - %(levelname)s - %(message)s
datefmt=

內容很明顯,就是將 例2 的那些設定寫進來,於是 例2 可以簡化成這個樣子。

例2改:

import logging
from logging import config

configName = "logging.conf"
loggerName = "test"
loggerName2 = "test2"

config.fileConfig(configName)
logger = logging.getLogger(loggerName)
logger2 = logging.getLogger(loggerName2)

for lg in [logger, logger2]:
    print(lg.getEffectiveLevel())
    lg.info("哈囉")
    lg.debug("這個如何")
    lg.warning("那這個呢")
    lg.error("最後一個")

這樣是不是就簡潔很多了。

這邊簡單整理一下 logging 的流程,舉個例:

假設我傳入一個 warning 的紀錄到 logger, logger 的級別若是超過 30,就會被濾掉。
沒有,則該紀錄傳往其所屬 handlers 跟上層的 root  handler 傳入。
若 logger handler 的級別小於等於 30,就會把該紀錄傳出去。
若 root handler 的級別小於等於 30,就會把該紀錄傳出去。
不重要:若 root 的級別超過 30與否都不影響,並沒有在判斷流程。

官網上也有流程圖,那裡寫得會更完整。

為了讓各位更清楚看見流程細節,最後一個範例4:

import logging
from logging import config
    
configName = "logging.conf"
loggerName = "test"
loggerName2 = "test2"

config.fileConfig(configName)
logger = logging.getLogger(loggerName) 
logger2 = logging.getLogger(loggerName2) 

fmt = logging.Formatter("%(name)s - %(levelname)s - %(message)s - root SH 20")
StreamHandler = logging.StreamHandler()
StreamHandler.setFormatter(fmt)
StreamHandler.setLevel(20)
fmt2 = logging.Formatter("%(name)s - %(levelname)s - %(message)s - root SH2 40")
StreamHandler2 = logging.StreamHandler()
StreamHandler2.setFormatter(fmt2)
StreamHandler2.setLevel(40)
logging.root.addHandler(StreamHandler)
logging.root.addHandler(StreamHandler2)

logger.setLevel(10)
logger2.setLevel(30)
logging.root.setLevel(20)

for lg in [logger, logger2]:
    lg.info("20")
    lg.debug("10")
    lg.warning("30")
    lg.error("40")

這裡將 root level = 20,並加了兩個 StreamHandler ,其 level 分別為 20、40。

test - INFO - 20
test - INFO - 20 - root SH 20
test - WARNING - 30
test - WARNING - 30 - root SH 20
test - ERROR - 40
test - ERROR - 40 - root SH 20
test - ERROR - 40 - root SH2 40
test2 - WARNING - 30 - root SH 20
test2 - ERROR - 40
test2 - ERROR - 40 - root SH 20
test2 - ERROR - 40 - root SH2 40

第一眼眼花撩亂,不要忘記:
test level = 10, test SH level = 20
test2 level = 30, test2 SH level = 40
root level = 20, root SH level = 20, root SH2 level = 40

順著 logger -> logger handlers & root handlers 這樣的流程就可以慢慢湊出每行輸出是怎麼來的。

如果不想要讓 Logger 往上傳遞,可以將其propagate設為 False。

總結一下:
如何設定 Logger 指定其輸出檔案跟終端的方式。
如何分別設定 Logger 。
如何使用 config.fileConfig(),簡化 Logger 設定。
描述 logging flow,瞭解傳播的方向與規則。

例1~例4的程式碼放在 github 。

祝各位學習順利,若有建議或想法可以留言告知。

透過 win10工作排程器 定時以 potplayer 播放音樂

用 工作排程器 在早上自動播放是我的習慣,不過有一個問題,它不一定每次都能播放出來。

這邊講一下基本準備:

將想要播放的音樂清單,用 potplayer 輸出成播放清單(.dpl)。
操作:把音樂擺入播放清單,按F2儲存。

在win10系統,螢幕左下有放大鏡,搜尋「工作排程器」就可以叫出來。
不然「開始」→「windows 系統管理工具」→「工作排程器」。

透過建立工作,觸發條件自行設定,我個人是設定每日早上七點。

動作新增一個:
啟動程式
程式或指令碼輸入程式名也就是 PotPlayerMini64.exe
引數為播放清單的位置如 xxx/xxx/xxx.dpl
開始位置為執行程式所在資料料夾,x:\Program Files\DAUM\PotPlayer


用cmd的概念講白話。
「cd x:\Program Files\DAUM\PotPlayer」進入到 x:\Program Files\DAUM\PotPlayer
「PotPlayerMini64.exe xxx/xxx/xxx.dpl」打開 PotPlayerMini64.exe 傳入 xxx/xxx/xxx.dpl

這樣的設定,在運行上是沒問題的,對著建立好的工作按右鍵執行,可以看到 potplayer 順利載入播放清單。

BUT!!!

如果剛開機或是重新開機,卻沒有辦法順利播放。可以看到清單載了,卻沒有播放。但這僅限於第一次,如果再重新執行工作一次,這次就能播放出來了。

基於觀察到的現象,個人做了許多不同變種的嘗試,都沒有辦法確保一定能播放出來。只有一種土法煉鋼的方法是一定可行的。

寫成批次檔(.bat),然後工作的動作就執行這個批次檔,這邊直接放上內容。

J:

cd J:\Program Files\DAUM\PotPlayer\

start ""PotPlayerMini64.exe 音樂.dpl"

timeout /t 4

taskkill /f /im PotPlayerMini64.exe

start ""PotPlayerMini64.exe 音樂.dpl"

exit

這裡逐行解釋:
第1行,進入J槽。因為我的播放器在J槽。
第3行,進入 J:\Program Files\DAUM\PotPlayer\ 。播放器所在的位置。
第5行,執行 PotPlayerMini64.exe 傳入 播放清單。這邊我把播放清單放在跟播放器同一個資料夾。用 start “”file”,能夠執行後繼續下一行,如果直接用 file,cmd就會卡在這一行指令。
第7行,等帶4秒。給第5行執行的potplayer跑好的時間。
第9行,關掉potplayer。因為開機後第一次執行不會播放,所以關掉重開。
第11行,同第5行。
第13行,離開cmd。

既然第一次不會自動播放,那就載入後關掉,再開第二次就能解決了。

雖然個人覺得這方法很醜,不過個人程度也就到這裡了。如果有更好的作法,歡迎留言告訴我。

Python pyqt5 與 matplotlib 結合示範之範例2

範例2是關於一元二次方程的線圖。

展示以下功能
1.可以添加一元二次方程至清單或從清單刪除之
2.依清單內的項目勾選狀況顯示或隱藏一元二次方程
3.在狀態列會顯示滑鼠指到線的相關資料

這次的重心,是要做出一個可調整的清單,也可分別勾選項目顯示與否。

如果線的數量未知,那就得以可擴充可變動的前提去寫。

對於不同情況,可能想看的線是不同的。有時候會覺得太多線,而想減少顯示在繪圖區的線。

這次各線都呈現在一個軸上,就沒有做區分。要控多軸,就再加點條件判斷就可以了。

因為範例1的cmd不會被錄到,所以這次把資訊顯示在狀態列上。

以下是範例2:程式碼(github) 展示(youtube)

因為有範例1,所以就沒有特別標上範例1有寫到的說明。

程式敘述:

有一個介面,有繪圖區跟控制區,控制區有清單,添加與刪除的按鈕等。

預設會添加三條方程,分別為: 「2x^2 + 1x^1 – 2」「0x^2 + 1x^1 – 2」「0x^2 + 0x^1 – 2」。

從控制區的清單,可以決定要顯示哪幾條線。

可以添加新的線,也可以刪除舊的線。

圖例可以顯示也可以隱藏,一旦線圖有變動,圖例也會變動。

在狀態列會顯示滑鼠指到的該線相關資料。