您好,登錄后才能下訂單哦!
如何Python代碼快速解析、整理上萬份數據文件,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
在這個世界上,人們每天都在用 Python 完成著不同的工作。而文件操作,則是大家最常需要解決的任務之一。使用 Python,你可以輕松為他人生成精美的報表,也可以用短短幾行代碼快速解析、整理上萬份數據文件。
當我們編寫與文件相關的代碼時,通常會關注這些事情:我的代碼是不是足夠快?我的代碼有沒有事半功倍的完成任務? 在這篇文章中,我會與你分享與之相關的幾個編程建議。我會向你推薦一個被低估的 Python 標準庫模塊、演示一個讀取大文件的最佳方式、最后再分享我對函數設計的一點思考。
下面,讓我們進入第一個“模塊安利”時間吧。
注意:因為不同操作系統的文件系統大不相同,本文的主要編寫環境為 Mac OS/Linux 系統,其中一些代碼可能并不適用于 Windows 系統。
如果你需要在 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 代碼后,我依然覺得:這些函數不光很難記,而且最終的成品代碼也不怎么討人喜歡。
為了讓文件處理變得更簡單,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]。
為了解決這個問題,我們需要暫時把這個“標準做法”放到一邊,使用更底層的
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)
可以在一些特定場景簡化代碼
難以編寫測試的代碼,通常也是需要改進的代碼
讓函數依賴“類文件對象”可以提升函數的適用面和可組合性
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。