欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Python 處理文件的幾種方式

 更新時間:2019年08月23日 10:31:46   作者:Python編程時光  
這篇文章主要介紹了Python 處理文件的幾種方式,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

在這個世界上,人們每天都在用 Python 完成著不同的工作。而文件操作,則是大家最常需要解決的任務之一。使用 Python,你可以輕松為他人生成精美的報表,也可以用短短幾行代碼快速解析、整理上萬份數(shù)據(jù)文件。

當我們編寫與文件相關的代碼時,通常會關注這些事情:我的代碼是不是足夠快?我的代碼有沒有事半功倍的完成任務? 在這篇文章中,我會與你分享與之相關的幾個編程建議。我會向你推薦一個被低估的 Python 標準庫模塊、演示一個讀取大文件的最佳方式、最后再分享我對函數(shù)設計的一點思考。

下面,讓我們進入第一個“模塊安利”時間吧。

注意:因為不同操作系統(tǒng)的文件系統(tǒng)大不相同,本文的主要編寫環(huán)境為 Mac OS/Linux 系統(tǒng),其中一些代碼可能并不適用于 Windows 系統(tǒng)。

建議一:使用 pathlib 模塊

如果你需要在 Python 里進行文件處理,那么標準庫中的 os 和 os.path 兄弟倆一定是你無法避開的兩個模塊。在這兩個模塊里,有著非常多與文件路徑處理、文件讀寫、文件狀態(tài)查看相關的工具函數(shù)。

讓我用一個例子來展示一下它們的使用場景。有一個目錄里裝了很多數(shù)據(jù)文件,但是它們的后綴名并不統(tǒng)一,既有 .txt,又有 .csv。我們需要把其中以 .txt 結(jié)尾的文件都修改為 .csv 后綴名。

我們可以寫出這樣一個函數(shù):

  import os

  import os.path



  def unify_ext_with_os_path(path):

    """統(tǒng)一目錄下的 .txt 文件名后綴為 .csv

    """

    for filename in os.listdir(path):

      basename, ext = os.path.splitext(filename)

      if ext == '.txt':

        abs_filepath = os.path.join(path, filename)

        os.rename(abs_filepath, os.path.join(path, f'{basename}.csv'))

讓我們看看,上面的代碼一共用到了哪些與文件處理相關的函數(shù):

  • os.listdir(path):列出 path 目錄下的所有文件(含文件夾)
  • os.path.splitext(filename):切分文件名里面的基礎名稱和后綴部分
  • os.path.join(path,filename):組合需要操作的文件名為絕對路徑
  • os.rename(...):重命名某個文件

上面的函數(shù)雖然可以完成需求,但說句實話,即使在寫了很多年 Python 代碼后,我依然覺得:這些函數(shù)不光很難記,而且最終的成品代碼也不怎么討人喜歡。

使用 pathlib 模塊改寫代碼

為了讓文件處理變得更簡單,Python 在 3.4 版本引入了一個新的標準庫模塊:pathlib。它基于面向?qū)ο笏枷朐O計,封裝了非常多與文件操作相關的功能。如果使用它來改寫上面的代碼,結(jié)果會大不相同。

使用 pathlib 模塊后的代碼:

  from pathlib import Path


  def unify_ext_with_pathlib(path):

    for fpath in Path(path).glob('*.txt'):

      fpath.rename(fpath.with_suffix('.csv'))

和舊代碼相比,新函數(shù)只需要兩行代碼就完成了工作。而這兩行代碼主要做了這么幾件事:

  1. 首先使用 Path(path) 將字符串路徑轉(zhuǎn)換為 Path 對象
  2. 調(diào)用 .glob('*.txt') 對路徑下所有內(nèi)容進行模式匹配并以生成器方式返回,結(jié)果仍然是 Path 對象,所以我們可以接著做后面的操作
  3. 使用 .with_suffix('.csv') 直接獲取使用新后綴名的文件全路徑
  4. 調(diào)用 .rename(target) 完成重命名

相比 os 和 os.path,引入 pathlib 模塊后的代碼明顯更精簡,也更有整體統(tǒng)一感。所有文件相關的操作都是一站式完成。

其他用法

除此之外,pathlib 模塊還提供了很多有趣的用法。比如使用 / 運算符來組合文件路徑:

  # 舊朋友:使用 os.path 模塊

  >>> import os.path

  >>> os.path.join('/tmp', 'foo.txt')

  '/tmp/foo.txt'


  # ✨ 新潮流:使用 / 運算符

  >>> from pathlib import Path

  >>> Path('/tmp') / 'foo.txt'

  PosixPath('/tmp/foo.txt')

或者使用 .read_text() 來快速讀取文件內(nèi)容:

  # 標準做法,使用 with open(...) 打開文件

  >>> with open('foo.txt') as file:

  ...   print(file.read())

  ...

  foo


  # 使用 pathlib 可以讓這件事情變得更簡單

  >>> from pathlib import Path

  >>> print(Path('foo.txt').read_text())

  foo

除了我在文章里介紹的這些,pathlib 模塊還提供了非常多有用的方法,強烈建議去 官方文檔 詳細了解一下。

如果上面這些都不足以讓你動心,那么我再多給你一個使用 pathlib 的理由:PEP-519 里定義了一個專門用于“文件路徑”的新對象協(xié)議,這意味著從該 PEP 生效后的 Python 3.6 版本起,pathlib 里的 Path 對象,可以和以前絕大多數(shù)只接受字符串路徑的標準庫函數(shù)兼容使用:

  >>> p = Path('/tmp')

  # 可以直接對 Path 類型對象 p 進行 join

  >>> os.path.join(p, 'foo.txt')

  '/tmp/foo.txt'

所以,無需猶豫,趕緊把 pathlib 模塊用起來吧。

Hint: 如果你使用的是更早的 Python 版本,可以嘗試安裝 pathlib2 模塊 。

建議二:掌握如何流式讀取大文件

幾乎所有人都知道,在 Python 里讀取文件有一種“標準做法”:首先使用 withopen(fine_name) 上下文管理器的方式獲得一個文件對象,然后使用 for 循環(huán)迭代它,逐行獲取文件里的內(nèi)容。

下面是一個使用這種“標準做法”的簡單示例函數(shù):

  def count_nine(fname):

    """計算文件里包含多少個數(shù)字 '9'

    """

    count = 0

    with open(fname) as file:

      for line in file:

        count += line.count('9')

    return count

假如我們有一個文件 small_file.txt,那么使用這個函數(shù)可以輕松計算出 9 的數(shù)量。

  # small_file.txt

  feiowe9322nasd9233rl

  aoeijfiowejf8322kaf9a


  # OUTPUT: 3

  print(count_nine('small_file.txt'))

為什么這種文件讀取方式會成為標準?這是因為它有兩個好處:

  • with 上下文管理器會自動關閉打開的文件描述符
  • 在迭代文件對象時,內(nèi)容是一行一行返回的,不會占用太多內(nèi)存

標準做法的缺點

但這套標準做法并非沒有缺點。如果被讀取的文件里,根本就沒有任何換行符,那么上面的第二個好處就不成立了。當代碼執(zhí)行到 forlineinfile 時,line 將會變成一個非常巨大的字符串對象,消耗掉非??捎^的內(nèi)存。

讓我們來做個試驗:有一個 5GB 大的文件 big_file.txt,它里面裝滿了和 small_file.txt 一樣的隨機字符串。只不過它存儲內(nèi)容的方式稍有不同,所有的文本都被放在了同一行里:

# FILE: big_file.txt
df2if283rkwefh... <剩余 5GB 大小> ...

如果我們繼續(xù)使用前面的 count_nine 函數(shù)去統(tǒng)計這個大文件里 9 的個數(shù)。那么在我的筆記本上,這個過程會足足花掉 65 秒,并在執(zhí)行過程中吃掉機器 2GB 內(nèi)存 [注1]。

使用 read 方法分塊讀取

為了解決這個問題,我們需要暫時把這個“標準做法”放到一邊,使用更底層的 file.read() 方法。與直接循環(huán)迭代文件對象不同,每次調(diào)用 file.read(chunk_size) 會直接返回從當前位置往后讀取 chunk_size 大小的文件內(nèi)容,不必等待任何換行符出現(xiàn)。

所以,如果使用 file.read() 方法,我們的函數(shù)可以改寫成這樣:

  def count_nine_v2(fname):

    """計算文件里包含多少個數(shù)字 '9',每次讀取 8kb

    """

    count = 0

    block_size = 1024 * 8

    with open(fname) as fp:

      while True:

        chunk = fp.read(block_size)

        # 當文件沒有更多內(nèi)容時,read 調(diào)用將會返回空字符串 ''

        if not chunk:

          break

        count += chunk.count('9')

    return count

在新函數(shù)中,我們使用了一個 while 循環(huán)來讀取文件內(nèi)容,每次最多讀取 8kb 大小,這樣可以避免之前需要拼接一個巨大字符串的過程,把內(nèi)存占用降低非常多。

利用生成器解耦代碼

假如我們在討論的不是 Python,而是其他編程語言。那么可以說上面的代碼已經(jīng)很好了。但是如果你認真分析一下 count_nine_v2 函數(shù),你會發(fā)現(xiàn)在循環(huán)體內(nèi)部,存在著兩個獨立的邏輯:數(shù)據(jù)生成(read 調(diào)用與 chunk 判斷) 與 數(shù)據(jù)消費。而這兩個獨立邏輯被耦合在了一起。

正如我在《編寫地道循環(huán)》里所提到的,為了提升復用能力,我們可以定義一個新的 chunked_file_reader 生成器函數(shù),由它來負責所有與“數(shù)據(jù)生成”相關的邏輯。這樣 count_nine_v3 里面的主循環(huán)就只需要負責計數(shù)即可。

  def chunked_file_reader(fp, block_size=1024 * 8):

    """生成器函數(shù):分塊讀取文件內(nèi)容

    """

    while True:

      chunk = fp.read(block_size)

      # 當文件沒有更多內(nèi)容時,read 調(diào)用將會返回空字符串 ''

      if not chunk:

        break

      yield chunk



  def count_nine_v3(fname):

    count = 0

    with open(fname) as fp:

      for chunk in chunked_file_reader(fp):

        count += chunk.count('9')

    return count

進行到這一步,代碼似乎已經(jīng)沒有優(yōu)化的空間了,但其實不然。iter(iterable) 是一個用來構(gòu)造迭代器的內(nèi)建函數(shù),但它還有一個更少人知道的用法。當我們使用 iter(callable,sentinel) 的方式調(diào)用它時,會返回一個特殊的對象,迭代它將不斷產(chǎn)生可調(diào)用對象 callable 的調(diào)用結(jié)果,直到結(jié)果為 setinel 時,迭代終止。

  def chunked_file_reader(file, block_size=1024 * 8):

    """生成器函數(shù):分塊讀取文件內(nèi)容,使用 iter 函數(shù)

    """

    # 首先使用 partial(fp.read, block_size) 構(gòu)造一個新的無需參數(shù)的函數(shù)

    # 循環(huán)將不斷返回 fp.read(block_size) 調(diào)用結(jié)果,直到其為 '' 時終止

    for chunk in iter(partial(file.read, block_size), ''):

      yield chunk

最終,只需要兩行代碼,我們就完成了一個可復用的分塊文件讀取函數(shù)。那么,這個函數(shù)在性能方面的表現(xiàn)如何呢?

和一開始的 2GB 內(nèi)存/耗時 65 秒 相比,使用生成器的版本只需要 7MB 內(nèi)存 / 12 秒 就能完成計算。效率提升了接近 4 倍,內(nèi)存占用更是不到原來的 1%。

建議三:設計接受文件對象的函數(shù)

統(tǒng)計完文件里的 “9” 之后,讓我們換一個需求。現(xiàn)在,我想要統(tǒng)計每個文件里出現(xiàn)了多少個英文元音字母(aeiou)。只要對之前的代碼稍作調(diào)整,很快就可以寫出新函數(shù) count_vowels。

  def count_vowels(filename):

    """統(tǒng)計某個文件中,包含元音字母(aeiou)的數(shù)量

    """

    VOWELS_LETTERS = {'a', 'e', 'i', 'o', 'u'}

    count = 0

    with open(filename, 'r') as fp:

      for line in fp:

        for char in line:

          if char.lower() in VOWELS_LETTERS:

            count += 1

    return count



  # OUTPUT: 16

  print(count_vowels('small_file.txt'))

和之前“統(tǒng)計 9”的函數(shù)相比,新函數(shù)變得稍微復雜了一些。為了保證程序的正確性,我需要為它寫一些單元測試。但當我準備寫測試時,卻發(fā)現(xiàn)這件事情非常麻煩,主要問題點如下:

  1. 函數(shù)接收文件路徑作為參數(shù),所以我們需要傳遞一個實際存在的文件
  2. 為了準備測試用例,我要么提供幾個樣板文件,要么寫一些臨時文件
  3. 而文件是否能被正常打開、讀取,也成了我們需要測試的邊界情況

如果,你發(fā)現(xiàn)你的函數(shù)難以編寫單元測試,那通常意味著你應該改進它的設計。上面的函數(shù)應該如何改進呢?答案是:讓函數(shù)依賴“文件對象”而不是文件路徑。

修改后的函數(shù)代碼如下:

  def count_vowels_v2(fp):

    """統(tǒng)計某個文件中,包含元音字母(aeiou)的數(shù)量

    """

    VOWELS_LETTERS = {'a', 'e', 'i', 'o', 'u'}

    count = 0

    for line in fp:

      for char in line:

        if char.lower() in VOWELS_LETTERS:

          count += 1

    return count



  # 修改函數(shù)后,打開文件的職責被移交給了上層函數(shù)調(diào)用者

  with open('small_file.txt') as fp:

    print(count_vowels_v2(fp))

這個改動帶來的主要變化,在于它提升了函數(shù)的適用面。因為 Python 是“鴨子類型”的,雖然函數(shù)需要接受文件對象,但其實我們可以把任何實現(xiàn)了文件協(xié)議的 “類文件對象(file-like object)” 傳入 count_vowels_v2 函數(shù)中。

而 Python 中有著非常多“類文件對象”。比如 io 模塊內(nèi)的 StringIO 對象就是其中之一。它是一種基于內(nèi)存的特殊對象,擁有和文件對象幾乎一致的接口設計。

利用 StringIO,我們可以非常方便的為函數(shù)編寫單元測試。

  # 注意:以下測試函數(shù)需要使用 pytest 執(zhí)行

  import pytest

  from io import StringIO



  @pytest.mark.parametrize(

    "content,vowels_count", [

      # 使用 pytest 提供的參數(shù)化測試工具,定義測試參數(shù)列表

      # (文件內(nèi)容, 期待結(jié)果)

      ('', 0),

      ('Hello World!', 3),

      ('HELLO WORLD!', 3),

      ('你好,世界', 0),

    ]

  )

  def test_count_vowels_v2(content, vowels_count):

    # 利用 StringIO 構(gòu)造類文件對象 "file"

    file = StringIO(content)

    assert count_vowels_v2(file) == vowels_count

使用 pytest 運行測試可以發(fā)現(xiàn),函數(shù)可以通過所有的用例:

  ❯ pytest vowels_counter.py

  ====== test session starts ======

  collected 4 items


  vowels_counter.py ... [100%]


  ====== 4 passed in 0.06 seconds ======

而讓編寫單元測試變得更簡單,并非修改函數(shù)依賴后的唯一好處。除了 StringIO 外,subprocess 模塊調(diào)用系統(tǒng)命令時用來存儲標準輸出的 PIPE 對象,也是一種“類文件對象”。這意味著我們可以直接把某個命令的輸出傳遞給 count_vowels_v2 函數(shù)來計算元音字母數(shù):

  import subprocess


  # 統(tǒng)計 /tmp 下面所有一級子文件名(目錄名)有多少元音字母

  p = subprocess.Popen(['ls', '/tmp'], stdout=subprocess.PIPE, encoding='utf-8')


  # p.stdout 是一個流式類文件對象,可以直接傳入函數(shù)

  # OUTPUT: 42

  print(count_vowels_v2(p.stdout))

正如之前所說,將函數(shù)參數(shù)修改為“文件對象”,最大的好處是提高了函數(shù)的 適用面 和 可組合性。通過依賴更為抽象的“類文件對象”而非文件路徑,給函數(shù)的使用方式開啟了更多可能,StringIO、PIPE 以及任何其他滿足協(xié)議的對象都可以成為函數(shù)的客戶。

不過,這樣的改造并非毫無缺點,它也會給調(diào)用方帶來一些不便。假如調(diào)用方就是想要使用文件路徑,那么就必須得自行處理文件的打開操作。

如何編寫兼容二者的函數(shù)

有沒有辦法即擁有“接受文件對象”的靈活性,又能讓傳遞文件路徑的調(diào)用方更方便?答案是:有,而且標準庫中就有這樣的例子。

打開標準庫里的 xml.etree.ElementTree 模塊,翻開里面的 ElementTree.parse 方法。你會發(fā)現(xiàn)這個方法即可以使用文件對象調(diào)用,也接受字符串的文件路徑。而它實現(xiàn)這一點的手法也非常簡單易懂:

  def parse(self, source, parser=None):

    """*source* is a file name or file object, *parser* is an optional parser

    """

    close_source = False

    # 通過判斷 source 是否有 "read" 屬性來判定它是不是“類文件對象”

    # 如果不是,那么調(diào)用 open 函數(shù)打開它并負擔起在函數(shù)末尾關閉它的責任

    if not hasattr(source, "read"):

      source = open(source, "rb")

      close_source = True

使用這種基于“鴨子類型”的靈活檢測方式, count_vowels_v2 函數(shù)也同樣可以被改造得更方便,我在這里就不再重復啦。

總結(jié)

文件操作我們在日常工作中經(jīng)常需要接觸的領域,使用更方便的模塊、利用生成器節(jié)約內(nèi)存以及編寫適用面更廣的函數(shù),可以讓我們編寫出更高效的代碼。

讓我們最后再總結(jié)一下吧:

  • 使用 pathlib 模塊可以簡化文件和目錄相關的操作,并讓代碼更直觀
  • PEP-519 定義了表示“文件路徑”的標準協(xié)議,Path 對象實現(xiàn)了這個協(xié)議
  • 通過定義生成器函數(shù)來分塊讀取大文件可以節(jié)約內(nèi)存
  • 使用 iter(callable,sentinel) 可以在一些特定場景簡化代碼
  • 難以編寫測試的代碼,通常也是需要改進的代碼
  • 讓函數(shù)依賴“類文件對象”可以提升函數(shù)的適用面和可組合性

以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。

相關文章

  • Python字典dict常用內(nèi)置函數(shù)詳解

    Python字典dict常用內(nèi)置函數(shù)詳解

    這篇文章主要介紹了Python字典dict常用內(nèi)置函數(shù)詳解,dict是Python中的一種內(nèi)置數(shù)據(jù)類型,它是一種鍵值對的集合,類似于Java中的Map或其他語言中的字典,需要的朋友可以參考下
    2023-07-07
  • 一文詳述 Python 中的 property 語法

    一文詳述 Python 中的 property 語法

    這篇文章主要介紹了一文詳述 Python 中的 property 語法,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-09-09
  • Python OpenCV讀取視頻報錯的問題解決

    Python OpenCV讀取視頻報錯的問題解決

    大家好,本篇文章主要講的是Python OpenCV讀取視頻報錯的問題解決,感興趣的同學趕快來看一看吧,對你有幫助的話記得收藏一下
    2022-01-01
  • 一文帶你全面理解Python中的self

    一文帶你全面理解Python中的self

    對于初學Python的同學來說,在class中經(jīng)??吹絪elf。那么,到底self是個啥?這篇文章小編就來帶大家深入了解一下,希望對大家有所幫助
    2023-03-03
  • Python實現(xiàn)計算兩個指定日期相差幾年幾月幾日

    Python實現(xiàn)計算兩個指定日期相差幾年幾月幾日

    這篇文章主要為大家詳細介紹了如何使用Python實現(xiàn)計算兩個日期之間相差多少年,多少月,多少天,文中的的示例代碼講解詳細,需要的可以參考下
    2024-02-02
  • Python人工智能之混合高斯模型運動目標檢測詳解分析

    Python人工智能之混合高斯模型運動目標檢測詳解分析

    運動目標檢測是計算機視覺領域中的一個重要內(nèi)容,其檢測效果將會對目標跟蹤與識別造成一定的影響,本文將介紹用Python來進行混合高斯模型運動目標檢測,感興趣的朋友快來看看吧
    2021-11-11
  • Python兩個整數(shù)相除得到浮點數(shù)值的方法

    Python兩個整數(shù)相除得到浮點數(shù)值的方法

    這篇文章主要介紹了Python兩個整數(shù)相除得到浮點數(shù)值的方法,本文直接給出代碼示例,需要的朋友可以參考下
    2015-03-03
  • Python新手必讀bytearray對象使用技巧掌握

    Python新手必讀bytearray對象使用技巧掌握

    Python中的bytearray是一個可變序列,通常用于存儲二進制數(shù)據(jù),它允許在不創(chuàng)建新的對象的情況下就地修改數(shù)據(jù),非常適用于處理字節(jié)數(shù)據(jù),本文將深入學習bytearray對象的使用,包括創(chuàng)建、修改、切片和常見應用場景
    2023-12-12
  • Pyhton多線程采集圖片方式

    Pyhton多線程采集圖片方式

    這篇文章主要介紹了Pyhton多線程采集圖片方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-12-12
  • 通過代碼實例展示Python中列表生成式的用法

    通過代碼實例展示Python中列表生成式的用法

    這篇文章主要介紹了通過代碼實例展示Python中列表生成式的用法,包括找出質(zhì)數(shù)、算平方數(shù)等基本用法,需要的朋友可以參考下
    2015-03-03

最新評論