2020年3月15日 星期日

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。