幾行Python程式碼快速解析、整理上萬份資料檔案
在這個世界上,人們每天都在用 Python 完成著不同的工作。而檔案操作,則是大家最常需要解決的任務之一。使用 Python,你可以輕鬆為他人生成精美的報表,也可以用短短几行程式碼快速解析、整理上萬份資料檔案。
當我們編寫與檔案相關的程式碼時,通常會關注這些事情: 我的程式碼是不是足夠快?我的程式碼有沒有事半功倍的完成任務? 在這篇文章中,我會與你分享與之相關的幾個程式設計建議。我會向你推薦一個被低估的 Python 標準庫模組、演示一個讀取大檔案的最佳方式、最後再分享我對函式設計的一點思考。
下面,讓我們進入第一個“模組安利”時間吧。
注意:因為不同作業系統的檔案系統大不相同,本文的主要編寫環境為 Mac OS/Linux 系統,其中一些程式碼可能並不適用於 Windows 系統。
建議一:使用 pathlib 模組
如果你需要在 Python 裡進行檔案處理,那麼標準庫中的
os
和
os.path
兄弟倆一定是你無法避開的兩個模組。在這兩個模組裡,有著非常多與檔案路徑處理、檔案讀寫、檔案狀態檢視相關的工具函式。
讓我用一個例子來展示一下它們的使用場景。有一個目錄裡裝了很多資料檔案,但是它們的字尾名並不統一,既有
.txt
,又有
.csv
。我們需要把其中以
.txt
結尾的檔案都修改為
.csv
字尾名。
我們可以寫出這樣一個函式:
在學習過程中有什麼不懂得可以加我的
python學習交流扣扣qun,784758214
群裡有不錯的學習視訊教程、開發工具與電子書籍。
與你分享python企業當下人才需求及怎麼從零基礎學習好python,和學習什麼內容
1. `import os`
2. `import os.path`
5. `def unify_ext_with_os_path(path):`
6. `"""統一目錄下的 .txt 檔名字尾為 .csv`
7. `"""`
8. `for filename in os.listdir(path):`
9. `basename, ext = os.path.splitext(filename)`
10. `if ext == '.txt':`
11. `abs_filepath = os.path.join(path, filename)`
12. `os.rename(abs_filepath, os.path.join(path, f'{basename}.csv'))`
讓我們看看,上面的程式碼一共用到了哪些與檔案處理相關的函式:
-
os.listdir(path)
:列出 path 目錄下的所有檔案(含資料夾) -
os.path.splitext(filename)
:切分檔名裡面的基礎名稱和字尾部分 -
os.path.join(path,filename)
:組合需要操作的檔名為絕對路徑 -
os.rename(...)
:重新命名某個檔案
上面的函式雖然可以完成需求,但說句實話,即使在寫了很多年 Python 程式碼後,我依然覺得: 這些函式不光很難記,而且最終的成品程式碼也不怎麼討人喜歡。
使用 pathlib 模組改寫程式碼
為了讓檔案處理變得更簡單,Python 在 3.4 版本引入了一個新的標準庫模組:pathlib。它基於物件導向思想設計,封裝了非常多與檔案操作相關的功能。如果使用它來改寫上面的程式碼,結果會大不相同。
使用 pathlib 模組後的程式碼:
1. `from pathlib import Path`
3. `def unify_ext_with_pathlib(path):`
4. `for fpath in Path(path).glob('*.txt'):`
5. `fpath.rename(fpath.with_suffix('.csv'))`
和舊程式碼相比,新函式只需要兩行程式碼就完成了工作。而這兩行程式碼主要做了這麼幾件事:
-
首先使用 Path(path) 將字串路徑轉換為
Path
物件 -
呼叫 .glob(‘*.txt’) 對路徑下所有內容進行模式匹配並以生成器方式返回,結果仍然是
Path
物件,所以我們可以接著做後面的操作 -
使用 .with_suffix(‘.csv’) 直接獲取使用新字尾名的檔案全路徑
-
呼叫 .rename(target) 完成重新命名
相比
os
和
os.path
,引入
pathlib
模組後的程式碼明顯更精簡,也更有整體統一感。所有檔案相關的操作都是一站式完成。
其他用法
除此之外,pathlib 模組還提供了很多有趣的用法。比如使用
/
運算子來組合檔案路徑:
1. `# ? 舊朋友:使用 os.path 模組`
2. `>>> import os.path`
3. `>>> os.path.join('/tmp', 'foo.txt')`
4. `'/tmp/foo.txt'`
6. `# ✨ 新潮流:使用 / 運算子`
7. `>>> from pathlib import Path`
8. `>>> Path('/tmp') / 'foo.txt'`
9. `PosixPath('/tmp/foo.txt')`
或者使用
.read_text()
來快速讀取檔案內容:
1. `# 標準做法,使用 with open(...) 開啟檔案`
2. `>>> with open('foo.txt') as file:`
3. `... print(file.read())`
4. `...`
5. `foo`
7. `# 使用 pathlib 可以讓這件事情變得更簡單`
8. `>>> from pathlib import Path`
9. `>>> print(Path('foo.txt').read_text())`
10. `foo`
除了我在文章裡介紹的這些,pathlib 模組還提供了非常多有用的方法,強烈建議去 官方文件 詳細瞭解一下。
如果上面這些都不足以讓你動心,那麼我再多給你一個使用 pathlib 的理由:PEP-519 裡定義了一個專門用於“檔案路徑”的新物件協議,這意味著從該 PEP 生效後的 Python 3.6 版本起,pathlib 裡的 Path 物件,可以和以前絕大多數只接受字串路徑的標準庫函式相容使用:
1. `>>> p = Path('/tmp')`
2. `# 可以直接對 Path 型別物件 p 進行 join`
3. `>>> os.path.join(p, 'foo.txt')`
4. `'/tmp/foo.txt'`
所以,無需猶豫,趕緊把 pathlib 模組用起來吧。
Hint: 如果你使用的是更早的 Python 版本,可以嘗試安裝 pathlib2 模組 。
建議二:掌握如何流式讀取大檔案
幾乎所有人都知道,在 Python 裡讀取檔案有一種“標準做法”:首先使用
withopen(fine_name)
上下文管理器的方式獲得一個檔案物件,然後使用
for
迴圈迭代它,逐行獲取檔案裡的內容。
下面是一個使用這種“標準做法”的簡單示例函式:
1. `def count_nine(fname):`
2. `"""計算檔案裡包含多少個數字 '9'`
3. `"""`
4. `count = 0`
5. `with open(fname) as file:`
6. `for line in file:`
7. `count += line.count('9')`
8. `return count`
假如我們有一個檔案
small_file.txt
,那麼使用這個函式可以輕鬆計算出 9 的數量。
1. `# small_file.txt`
2. `feiowe9322nasd9233rl`
3. `aoeijfiowejf8322kaf9a`
5. `# OUTPUT: 3`
6. `print(count_nine('small_file.txt'))`
為什麼這種檔案讀取方式會成為標準?這是因為它有兩個好處:
-
with
上下文管理器會自動關閉開啟的檔案描述符 -
在迭代檔案物件時,內容是一行一行返回的,不會佔用太多記憶體
標準做法的缺點
但這套標準做法並非沒有缺點。如果被讀取的檔案裡,根本就沒有任何換行符,那麼上面的第二個好處就不成立了。
當程式碼執行到
forlineinfile
時,line 將會變成一個非常巨大的字串物件,消耗掉非常可觀的記憶體。
讓我們來做個試驗:有一個
5GB 大的檔案
big_file.txt
,它裡面裝滿了和
small_file.txt
一樣的隨機字串。只不過它儲存內容的方式稍有不同,所有的文字都被放在了同一行裡:
1. `# FILE: big_file.txt`
2. `df2if283rkwefh... <剩餘 5GB 大小> ...`
如果我們繼續使用前面的
count_nine
函式去統計這個大檔案裡
9
的個數。那麼在我的筆記本上,這個過程會足足花掉
65 秒,並在執行過程中吃掉機器
2GB 記憶體 [注1]。
使用 read 方法分塊讀取
為了解決這個問題,我們需要暫時把這個“標準做法”放到一邊,使用更底層的
file.read()
方法。與直接迴圈迭代檔案物件不同,每次呼叫
file.read(chunk_size)
會直接返回從當前位置往後讀取
chunk_size
大小的檔案內容,不必等待任何換行符出現。
所以,如果使用
file.read()
方法,我們的函式可以改寫成這樣:
1. `def count_nine_v2(fname):`
2. `"""計算檔案裡包含多少個數字 '9',每次讀取 8kb`
3. `"""`
4. `count = 0`
5. `block_size = 1024 * 8`
6. `with open(fname) as fp:`
7. `while True:`
8. `chunk = fp.read(block_size)`
9. `# 當檔案沒有更多內容時,read 呼叫將會返回空字串 ''`
10. `if not chunk:`
11. `break`
12. `count += chunk.count('9')`
13. `return count`
在新函式中,我們使用了一個
while
迴圈來讀取檔案內容,每次最多讀取 8kb 大小,這樣可以避免之前需要拼接一個巨大字串的過程,把記憶體佔用降低非常多。
利用生成器解耦程式碼
假如我們在討論的不是 Python,而是其他程式語言。那麼可以說上面的程式碼已經很好了。但是如果你認真分析一下
count_nine_v2
函式,你會發現在迴圈體內部,存在著兩個獨立的邏輯:
資料生成(read 呼叫與 chunk 判斷) 與
資料消費。而這兩個獨立邏輯被耦合在了一起。
為了提升複用能力,我們可以定義一個新的
chunked_file_reader
生成器函式,由它來負責所有與“資料生成”相關的邏輯。這樣
count_nine_v3
裡面的主迴圈就只需要負責計數即可。
1. `def chunked_file_reader(fp, block_size=1024 * 8):`
2. `"""生成器函式:分塊讀取檔案內容`
3. `"""`
4. `while True:`
5. `chunk = fp.read(block_size)`
6. `# 當檔案沒有更多內容時,read 呼叫將會返回空字串 ''`
7. `if not chunk:`
8. `break`
9. `yield chunk`
12. `def count_nine_v3(fname):`
13. `count = 0`
14. `with open(fname) as fp:`
15. `for chunk in chunked_file_reader(fp):`
16. `count += chunk.count('9')`
17. `return count`
進行到這一步,程式碼似乎已經沒有優化的空間了,但其實不然。iter(iterable) 是一個用來構造迭代器的內建函式,但它還有一個更少人知道的用法。當我們使用
iter(callable,sentinel)
的方式呼叫它時,會返回一個特殊的物件,迭代它將不斷產生可呼叫物件 callable 的呼叫結果,直到結果為 setinel 時,迭代終止。
1. `def chunked_file_reader(file, block_size=1024 * 8):`
2. `"""生成器函式:分塊讀取檔案內容,使用 iter 函式`
3. `"""`
4. `# 首先使用 partial(fp.read, block_size) 構造一個新的無需引數的函式`
5. `# 迴圈將不斷返回 fp.read(block_size) 呼叫結果,直到其為 '' 時終止`
6. `for chunk in iter(partial(file.read, block_size), ''):`
7. `yield chunk`
最終,只需要兩行程式碼,我們就完成了一個可複用的分塊檔案讀取函式。那麼,這個函式在效能方面的表現如何呢?
和一開始的 2GB 記憶體/耗時 65 秒 相比,使用生成器的版本只需要 7MB 記憶體 / 12 秒 就能完成計算。效率提升了接近 4 倍,記憶體佔用更是不到原來的 1%。
建議三:設計接受檔案物件的函式
統計完檔案裡的 “9” 之後,讓我們換一個需求。現在,我想要統計每個檔案裡出現了多少個英文母音字母(aeiou)。只要對之前的程式碼稍作調整,很快就可以寫出新函式
count_vowels
。
在學習過程中有什麼不懂得可以加我的
python學習交流扣扣qun,784758214
群裡有不錯的學習視訊教程、開發工具與電子書籍。
與你分享python企業當下人才需求及怎麼從零基礎學習好python,和學習什麼內容
1. `def count_vowels(filename):`
2. `"""統計某個檔案中,包含母音字母(aeiou)的數量`
3. `"""`
4. `VOWELS_LETTERS = {'a', 'e', 'i', 'o', 'u'}`
5. `count = 0`
6. `with open(filename, 'r') as fp:`
7. `for line in fp:`
8. `for char in line:`
9. `if char.lower() in VOWELS_LETTERS:`
10. `count += 1`
11. `return count`
14. `# OUTPUT: 16`
15. `print(count_vowels('small_file.txt'))`
和之前“統計 9”的函式相比,新函式變得稍微複雜了一些。為了保證程式的正確性,我需要為它寫一些單元測試。但當我準備寫測試時,卻發現這件事情非常麻煩,主要問題點如下:
-
函式接收檔案路徑作為引數,所以我們需要傳遞一個實際存在的檔案
-
為了準備測試用例,我要麼提供幾個樣板檔案,要麼寫一些臨時檔案
-
而檔案是否能被正常開啟、讀取,也成了我們需要測試的邊界情況
如果,你發現你的函式難以編寫單元測試,那通常意味著你應該改進它的設計。上面的函式應該如何改進呢?答案是:讓函式依賴“檔案物件”而不是檔案路徑。
修改後的函式程式碼如下:
1. `def count_vowels_v2(fp):`
2. `"""統計某個檔案中,包含母音字母(aeiou)的數量`
3. `"""`
4. `VOWELS_LETTERS = {'a', 'e', 'i', 'o', 'u'}`
5. `count = 0`
6. `for line in fp:`
7. `for char in line:`
8. `if char.lower() in VOWELS_LETTERS:`
9. `count += 1`
10. `return count`
13. `# 修改函式後,開啟檔案的職責被移交給了上層函式呼叫者`
14. `with open('small_file.txt') as fp:`
15. `print(count_vowels_v2(fp))`
這個改動帶來的主要變化,在於它提升了函式的適用面。因為 Python 是“鴨子型別”的,雖然函式需要接受檔案物件,但其實我們可以把任何實現了檔案協議的 “類檔案物件(file-like object)” 傳入
count_vowels_v2
函式中。
而 Python 中有著非常多“類檔案物件”。比如 io 模組內的 StringIO 物件就是其中之一。它是一種基於記憶體的特殊物件,擁有和檔案物件幾乎一致的介面設計。
利用 StringIO,我們可以非常方便的為函式編寫單元測試。
1. `# 注意:以下測試函式需要使用 pytest 執行`
2. `import pytest`
3. `from io import StringIO`
6. `@pytest.mark.parametrize(`
7. `"content,vowels_count", [`
8. `# 使用 pytest 提供的引數化測試工具,定義測試引數列表`
9. `# (檔案內容, 期待結果)`
10. `('', 0),`
11. `('Hello World!', 3),`
12. `('HELLO WORLD!', 3),`
13. `('你好,世界', 0),`
14. `]`
15. `)`
16. `def test_count_vowels_v2(content, vowels_count):`
17. `# 利用 StringIO 構造類檔案物件 "file"`
18. `file = StringIO(content)`
19. `assert count_vowels_v2(file) == vowels_count`
使用 pytest 執行測試可以發現,函式可以通過所有的用例:
1. `❯ pytest vowels_counter.py`
2. `====== test session starts ======`
3. `collected 4 items`
5. `vowels_counter.py ... [100%]`
7. `====== 4 passed in 0.06 seconds ======`
而讓編寫單元測試變得更簡單,並非修改函式依賴後的唯一好處。除了 StringIO 外,subprocess 模組呼叫系統命令時用來儲存標準輸出的 PIPE 物件,也是一種“類檔案物件”。這意味著我們可以直接把某個命令的輸出傳遞給
count_vowels_v2
函式來計算母音字母數:
1. `import subprocess`
3. `# 統計 /tmp 下面所有一級子檔名(目錄名)有多少母音字母`
4. `p = subprocess.Popen(['ls', '/tmp'], stdout=subprocess.PIPE, encoding='utf-8')`
6. `# p.stdout 是一個流式類檔案物件,可以直接傳入函式`
7. `# OUTPUT: 42`
8. `print(count_vowels_v2(p.stdout))`
正如之前所說,將函式引數修改為“檔案物件”,最大的好處是提高了函式的 適用面 和 可組合性。通過依賴更為抽象的“類檔案物件”而非檔案路徑,給函式的使用方式開啟了更多可能,StringIO、PIPE 以及任何其他滿足協議的物件都可以成為函式的客戶。
不過,這樣的改造並非毫無缺點,它也會給呼叫方帶來一些不便。假如呼叫方就是想要使用檔案路徑,那麼就必須得自行處理檔案的開啟操作。
如何編寫相容二者的函式
有沒有辦法即擁有“接受檔案物件”的靈活性,又能讓傳遞檔案路徑的呼叫方更方便?答案是:有,而且標準庫中就有這樣的例子。
開啟標準庫裡的
xml.etree.ElementTree
模組,翻開裡面的
ElementTree.parse
方法。你會發現這個方法即可以使用檔案物件呼叫,也接受字串的檔案路徑。而它實現這一點的手法也非常簡單易懂:
1. `def parse(self, source, parser=None):`
2. `"""*source* is a file name or file object, *parser* is an optional parser`
3. `"""`
4. `close_source = False`
5. `# 通過判斷 source 是否有 "read" 屬性來判定它是不是“類檔案物件”`
6. `# 如果不是,那麼呼叫 open 函式開啟它並負擔起在函式末尾關閉它的責任`
7. `if not hasattr(source, "read"):`
8. `source = open(source, "rb")`
9. `close_source = True`
使用這種基於“鴨子型別”的靈活檢測方式,
count_vowels_v2
函式也同樣可以被改造得更方便,我在這裡就不再重複啦。
如果你依然在程式設計的世界裡迷茫,可以加入我們的Python學習扣qun:784758214,看看前輩們是如何學習的!交流經驗!自己是一名高階python開發工程師,從基礎的python指令碼到web開發、爬蟲、django、資料探勘等,零基礎到專案實戰的資料都有整理。送給每一位python的小夥伴!分享一些學習的方法和需要注意的小細節,點選加入我們的 python學習者聚集地
總結
檔案操作我們在日常工作中經常需要接觸的領域,使用更方便的模組、利用生成器節約記憶體以及編寫適用面更廣的函式,可以讓我們編寫出更高效的程式碼。
讓我們最後再總結一下吧:
-
使用 pathlib 模組可以簡化檔案和目錄相關的操作,並讓程式碼更直觀
-
PEP-519 定義了表示“檔案路徑”的標準協議,Path 物件實現了這個協議
-
通過定義生成器函式來分塊讀取大檔案可以節約記憶體
-
使用
iter(callable,sentinel)
可以在一些特定場景簡化程式碼 -
難以編寫測試的程式碼,通常也是需要改進的程式碼
-
讓函式依賴“類檔案物件”可以提升函式的適用面和可組合性
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69913713/viewspace-2654788/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Python 大資料量文字檔案高效解析方案程式碼實現Python大資料
- shell指令碼命令 執行python檔案&python命令列執行python程式碼指令碼Python命令列
- Python將表格檔案中某些列的資料整體向上移動一行Python
- python——幾行程式碼,偷偷複製U盤檔案到電腦Python行程
- 如何使用 ABAP 程式碼解析 XML 檔案XML
- python XML 檔案解析PythonXML
- Python程式碼打包成可執行檔案的常用方法!Python
- 教你不程式設計快速解析 JSON 資料程式設計JSON
- 增加程式碼的通用性-解析配置檔案
- 利用Python快速批量修改資料集檔名Python
- java程式碼實現excel檔案資料匯入JavaExcel
- 將自己的python程式碼打包成exe的可執行檔案Python
- Python解析XML檔案生成HTMLPythonXMLHTML
- python 建立mysql資料庫腳(執行sql)指令碼程式碼PythonMySql資料庫指令碼
- 使用JsonConverter處理上傳檔案的路徑JSON
- Python 高階程式設計:深入解析 CSV 檔案讀取Python程式設計
- 幾百行程式碼實現一個 JSON 解析器行程JSON
- Beam Search快速理解及程式碼解析
- MySQL:如何快速的檢視Innodb資料檔案MySql
- DBeaver如何調整sql檔案編碼SQL
- SQL server資料庫建立程式碼 filegroup檔案組修改的示例程式碼SQLServer資料庫
- DEX檔案解析--7、類及其類資料解析(完結篇)
- 「Python實用祕技08」一行程式碼解析地址資訊Python行程
- 指令碼執行 py 檔案,該如何調整瀏覽器大小?指令碼瀏覽器
- 將Python指令碼打包成可執行檔案Python指令碼
- Keil一鍵新增.c檔案和標頭檔案路徑指令碼--可遍歷新增整個資料夾指令碼
- SQLite3資料庫檔案結構解析SQLite資料庫
- python儲存檔案的幾種方法Python
- mybatis原始碼配置檔案解析之五:解析mappers標籤(解析XML對映檔案)MyBatis原始碼APPXML
- wpf winform 選擇檔案或選擇資料夾程式碼ORM
- 查詢某資料夾下所有子資料夾內的py檔案-3一行程式碼搞定行程
- 原始碼解析Flask的配置檔案原始碼Flask
- mysql整數資料型別深入解析MySql資料型別
- python基礎1 - 多檔案專案和程式碼規範Python
- 一枚Python資料工程師為媽媽寫的幾行程式碼Python工程師行程
- 【Python基礎】Python處理Excel檔案,進行篩選資料、排序等操作及儲存新的Excel檔案PythonExcel排序
- excel怎麼快速下拉到幾千行 表格資料太長如何快速下拉Excel
- Python提取文字檔案(.txt)資料的方法Python