ABSP第8章 文件讀寫

讀寫文件

在程序運行的時候用變數存儲數據是沒什麼問題,但是如果你想讓自己的數據在程序關閉以後仍舊持續存在,你就需要存儲成文件。你可以把文件的內容理解成一長串的字元串,可能有幾個G的字元串。在這一張裡面,你會學習如何用Python新建,讀取和保存硬碟上的文件。

文件與文件路徑

一個文件具有兩個關鍵的屬性:文件名和路徑。路徑表示的是文件在電腦裡面的位置。比如在windows7的筆記本電腦上面有一個叫做project.docx的文件存在C:UsersasweigartDocuments裡面。文件名中最後一個.後面的稱作擴展名,表示文件的類型。project.docx是一個word發文檔,而Users,asweigart和Documents都表示文件夾(也可以稱作目錄)。文件夾里可以包含其他的文件夾,以及文件。比如project.docx在Documents文件夾里,Documents又在asweigart里,後者又在Users文件夾里。

C:這個部分稱作路徑的根目錄。是包含了其他文件夾的文件夾。在Windows裡面,根目錄的表示形式是C:,也可以被稱為C:盤。在OS X和linux裡面,根目錄是/。本書裡面我們會使用Windows形式的根目錄C:。如果你使用的是OSX或者linux的互動式編程環境,使用/表示根目錄。

額外的卷,比如一個DVD或者一個usb盤在不同的系統中表現形式有所不同。他們可以表現為其他的以字母標記的盤符:類似D:或者E:。而在OSX中,他們則是出現在/Volumes下面的新文件夾。Linux下面,這些新的卷是以新文件夾出現在/mnt中。需要注意的是儘管文件夾名稱在windows和osx里是大小寫不敏感的,但在linux裡面則是大小寫敏感的。

Windows的和osx,linux里的/

在windows裡面,路徑里的文件夾以作為分隔符。而在OSX和Linux裡面,使用的是/作為分隔符。如果你想讓自己的程序在各種系統上都能運行,記得對兩種情況都要做準備。

好在python的os模塊具有os.path.join()這個方法。如果你給它傳入各個文件夾以及文件名的話,os.path.join()會返回由這些組成的路徑,試一下下面的代碼:

>>> import os>>> os.path.join(usr, bin, spam)usr\bin\spam

這是在windows環境下運行的結果(之所以會有兩個,這是python對作了escape)。如果上面的代碼在osx或者linux中運行,返回的字元串會是usr/bin/spam

os.path.join()也可以用來生成文件名。這一章會介紹一些和文件相關的函數,介紹的過程中會經常用到下面的字元串。在下面的例子里,我們把幾個不同的文件名接在了一個路徑的後面:

>>> myFiles = [accounts.txt, details.csv, invite.docx]>>> for filename in myFiles: print(os.path.join(C:\Users\asweigart, filename))C:Usersasweigartaccounts.txtC:Usersasweigartdetails.csvC:Usersasweigartinvite.docx

當前的工作目錄

你電腦上的每一個程序都有自己的當前工作目錄current working directory, cwd)。如果一個文件夾或者文件名不以根目錄開始默認它就是位於當前工作目錄中你可以用os.getcwd()獲取當前工作目錄用os.chdir()設置當前工作目錄試試下面的代碼>>> import os>>> os.getcwd()C:\Python34>>> os.chdir(C:\Windows\System32)>>> os.getcwd()C:\Windows\System32

首先os.getcwd()返回了當前工作目錄是C:Python34,所以project.docx這個文件名就表示C:Python34project.docx. 當我們把當前工作路徑設置成C:Windows以後,project.docx就被解釋成C:Windowsproject.docx.

如果你想把當前工作目錄設置成一個不存在的文件夾的話, python會報錯.

>>> os.chdir(C:\ThisFolderDoesNotExist)Traceback (most recent call last): File "<pyshell#18>", line 1, in <module> os.chdir(C:\ThisFolderDoesNotExist)FileNotFoundError: [WinError 2] The system cannot find the file specified:C:\ThisFolderDoesNotExist

說明

文件夾是目錄的新說法, 不過當前工作路徑還是當前工作路徑, 不叫做當前工作文件夾

絕對和相對路徑

指定一個文件夾路徑有兩種方法:

  1. 設置絕對路徑, 也就是從根目錄開始設置
  2. 設置相對路徑,也就是想對於程序的當前工作目錄設置.

除了普通的目錄以外, 還有點(.)和點點(..)目錄. 這兩個不是實際存在的文件夾, 而是在路徑裡面表示特定含義的符號. 一個點表示當前文件夾, 兩個點表示上一級文件夾.

上圖裡面有一些文件夾和文件.如果當前工作目錄設置成C:acon的話, 其他的目錄的相對路徑如圖中所示.

相對路徑裡面開頭的.是可以省略的. 比如.spam.txt和spam.txt指向同一個文件.

用os.makedir()新建文件夾

你的程序可以用os.makedir()新建文件夾. 在互動系統裡面輸入下面的代碼

>>> import os>>> os.makedirs(C:\delicious\walnut\waffles)

多說一句, 在進行文件操作的時候, 你是實打實的在自家電腦裡面操作. 如果不小心的話, 是可能發生無法挽回的事情的. 所以弄清楚自己在做什麼, 同時注意備份.

這個命令不僅僅會新建C:delicious文件夾,還會在下面建一個walnut文件夾, walnut文件夾下面還會再建一個waffles文件夾.

os.path模塊

os.path模塊有很多有用的處理文件名和路徑的函數.比如我們已經遇到的os.path.join()可以生成符合當前系統情況的路徑. 因為os.path是os的一個子模塊, 所以你只需要import os就可以把os.path一併導入. 如果你的程序需要處理文件, 文件夾, 路徑這些東西,你可以回顧一下這一段內容. 關於os.path的詳細文檔可以在這裡看到:docs.python.org/3/libra.

說明

我們接下來提到的絕大多數例子都需要用os模塊,所以記得在程序的開頭import os, 或者是每次你重啟IDLE的 時候都需要重新import一次. 如果你看到: NameError: name os is not defined error message, 那就說明你忘了導入了.

處理絕對和相對路徑

os.path模塊提供了一些方法, 讓你可以生成一個相對路徑的絕對路徑, 或者判斷一個路徑是不是絕對路徑.

  • os.path.abspath(path)會把傳入參數path轉換成絕對路徑, 這是用相對路徑生成絕對路徑的常用辦法
  • os.path.isabs(path)會判斷path是不是絕對路徑, 是絕對路徑返回True, 否則就False
  • os.path.relpath(path, start)會計算絕對路徑相對於start的相對路徑. 如果沒有提供start參數, 那就使用cwd(當前工作目錄)

試試下面的代碼:

>>> os.path.abspath(.)C:\Python34>>> os.path.abspath(.\Scripts)C:\Python34\Scripts>>> os.path.isabs(.)False>>> os.path.isabs(os.path.abspath(.))True

因為現在cwd還是C:Python34, 用單點"."表示的相對文件夾被os.path.abspath轉換成的絕對路徑就是c:Python34

說明

你的系統裡面文件的安排方式可能和我的不一樣,所以上面的例子里得到的結果很可能有所不同.不過還是盡量在你電腦上的文件系統上重複上面的例子.

現在試一下下面的代碼, 這些是關於relpath的:

>>> os.path.relpath(C:\Windows, C:\)Windows>>> os.path.relpath(C:\Windows, C:\spam\eggs)..\..\Windows>>> os.getcwd() C:\Python34

os.path.dirname(path)會返回path路徑裡面最後一個(windows)或者/(mac, linux)之前的內容. dirname和basename的示意圖如下

在下面的代碼中試驗dirname以及basename

>>> path = C:\Windows\System32\calc.exe>>> os.path.basename(path)calc.exe>>> os.path.dirname(path)C:\Windows\System32

說明:

一個路徑實際上也是有basename的。接著上面的dirname來寫:

>>> os.path.basename(os.path.dirname(path))System32

也就是這個路徑的最下級的一個文件夾。

如果你需要一個路徑的目錄+文件名, 可以用另外一個函數os.path.split(), 這個函數會把dirname和basename放在一個tuple裡面返回來.

>>> calcFilePath = C:\Windows\System32\calc.exe>>> os.path.split(calcFilePath)(C:\Windows\System32, calc.exe)

當然像下面這樣做也是可以的:

>>> (os.path.dirname(calcFilePath), os.path.basename(calcFilePath))(C:\Windows\System32, calc.exe)

但是用os.path.split()更簡潔不是?

另外,os.path.split和給string用的split有些不同. 如果要想把c:WindowsSystem32calc.exe分割成C:, Windows, System32和calc.exe, 用os.path.split不合適,應該用普通的split. 考慮到之前提到的windows和mac/linux系統之間的分隔符區別, split的分隔符參數應該使用os.path.sep. 試著輸入下面的代碼

>>> calcFilePath.split(os.path.sep)[C:, Windows, System32, calc.exe]

在osx和linux系統裡面, 用os.path.sep去分割得到的結果如下:

>>> /usr/bin.split(os.path.sep)[, usr, bin]

注意列表第一個元素是空格

簡單地說,如果要分dirname和basename, 用os.path.split. 如果想用系統分隔符對路徑切分, 用字元串的split方法, 記得分隔符使用os.path.sep

獲得文件大小和文件夾內容

當你學會怎麼處理路徑以後, 下一步就可以開始收集特定文件或文件夾的信息了. os.path模塊可以幫你獲取文件的大小(byte為單位), 還可以獲得文件夾內的文件列表

  • os.path.getsize(path)返回path所指向文件的大小
  • os.listdir(path)返回path所指的目錄下面的所有文件, 用list列表形式返回

試一下下面的代碼:

>>> os.path.getsize(C:\Windows\System32\calc.exe)776192>>> os.listdir(C:\Windows\System32)[0409, 12520437.cpx, 12520850.cpx, 5U877.ax, aaclient.dll,--snip--xwtpdui.dll, xwtpw32.dll, zh-CN, zh-HK, zh-TW, zipfldr.dll]

電腦上的calc.exe大小是776,192位元組, 而在C:Windowssystem32下面還有很多文件. 如果要想知道這個文件夾下面的所有文件總共有多大呢? 把os.path.getsize()和os.listdir()結合起來:

>>> totalSize = 0>>> for filename in os.listdir(C:\Windows\System32): totalSize = totalSize + os.path.getsize(os.path.join(C:\Windows\System32, filename))>>> print(totalSize)1117846456

我們用os.listdir()拿到所有文件列表,然後對列表進行遍歷. 遍歷的每個文件都用os.path.getsize()獲取大小. 把所有大小用totalSize累加, 遍歷完成以後totalSize就是總共的大小了. 注意os.path.getsize()的path參數是怎麼合成的.

檢查path是否合理

如果path參數錯了, 我們這一章講的很多函數都沒法運行, python也會崩潰. os.path模塊為此準備了檢驗函數來確認path參數指向的文件或文件夾到底存不存在.

  • os.path.exists(path)會判斷path指向目錄或文件是否存在, 存在時返回True, 否則返回false.
  • os.path.isfile(path)會判斷path指向目標是否存在以及是否是一個文件, 如果滿足條件則返回True, 否則返回false
  • os.path.isdir(path)會半段path指向目標是否存在以及是否是一個目錄, 如果滿足條件則返回True, 否則返回false

自己試一下下面的代碼:

>>> os.path.exists(C:\Windows)True>>> os.path.exists(C:\some_made_up_folder)False>>> os.path.isdir(C:\Windows\System32)True>>> os.path.isfile(C:\Windows\System32)False>>> os.path.isdir(C:\Windows\System32\calc.exe)False>>> os.path.isfile(C:\Windows\System32\calc.exe)True

你可以用os.path.exists()檢查你的DVD或者u盤是不是插在電腦上. 假如你的U盤盤符是D:, 那下面的代碼就是判斷U盤在不在電腦上的:

>>> os.path.exists(D:\)False

所以U盤還沒插進電腦

如果你的電腦里U盤盤符一般是f:的話就把上面的改成F:\吧.

文件讀寫

在你掌握了文件以及文件夾路徑相關的操作以後, 下一步就可以開始在指定位置上進行文件的讀寫操作了. 接下來所講的內容主要用於純文本文件. 純文本文件只有基本的字元, 不包括字體, 字型大小或者顏色這些額外信息. 以.txt作為拓展名的文本文件或者python的腳本文件(.py)都屬於純文本. 這類文件都可以用windows的Notepad或者OSX的TextEdit程序打開 (我主要還是用atom). 我們用python寫的程序也可以很容易的讀取純文本文件的內容, 並按照普通的字元串值去處理.

與之相對的, 二進位文件, 比如word文檔, PDF, 圖片, excel表或者其他一些可執行文件是另一類文件. 如果你用Notepad或者TextEdit打開一個二進位文檔的話一般會看到下面這樣的亂碼:

不同類型的二進位文檔有其特定的處理方式, 這些不會再本書中進行介紹. 好在python有很多處理特定二進位文檔的模塊, 其中的shelve模塊我們會在本章遲些介紹.

對文件進行讀寫包括三個步驟:

  1. 用open()函數打開文件,並返回一個file對象
  2. 用read()或者write()方法對file對象進行處理
  3. 用close()方法關閉file對象

用open()打開文件

要用open()打開文件, 給這個函數傳入一個path字元串制定需要打開的文件就可以. path的路徑可以是相對路徑或者是絕對路徑. open()函數返回的是file對象.

試著自己建立一個hello.txt, 用Notepad或者TextEdit建. 在hello.txt裡面輸入Hello world!並保存到你的用戶文件夾中(比如我的用戶名是ben, 那我的用戶文件夾就是C:Usersen). 在Windows環境下, 試試下面的代碼:

>>> helloFile = open(C:\Users\your_home_folder\hello.txt)

如果使用的是OSX, 使用下面的代碼:

>>> helloFile = open(/Users/your_home_folder/hello.txt)

記得把your_home_folder替換成你的用戶名

open()會用"閱讀純文本"的模式去打開文件, 這種模式也簡稱閱讀模式(read mode). 當一個文件在閱讀模式下面打開的話, python可以去讀取文件內部的數據, 不過你沒法去寫,或者修改. 閱讀模式是python打開文件的默認模式, 如果想要指定其他的模式, 你要傳入讀取模式作為參數. 剛才說到的閱讀模式對應的是r. 所以 open(/Users/asweigart/ hello.txt, r) 和open(/Users/asweigart/hello.txt)兩者等價.

open()函數返回file對象, 一個file對象表示你電腦上的一個文件; 這個file對象只是python裡面的另外一種數據類型, 就像list, dictionary一樣. 剛才你已經把文件存儲在helloFile這個file對象裡面了,接下來只要你需要從文件讀或者往文件寫, 都可以藉助helloFile這個文件對象的方法實現.

讀取文件內容

現在我們有了file對象, 你可以從裡面讀取內容了. 如果你想把文件的全部內容放在一個字元串里閱讀的話, 可以使用read()方法. 讓我們繼續使用指向hello.txt的file對象helloFile, 試著運行下面的代碼:

>>> helloContent = helloFile.read()>>> helloContentHello world!

read()方法返回的就是文檔里包含的所有文字, 用一個大的字元串返回來.

另外, 你還可以用readlines()一行一行的讀取. 比如同樣在用戶主文件夾建立sonnet29.txt,在裡面輸入下面內容並保存:

When, in disgrace with fortune and mens eyes,I all alone beweep my outcast state,And trouble deaf heaven with my bootless cries,And look upon myself and curse my fate,

注意是4行內容, 不要全放進一行裡面. 接下來試試下面的代碼

>>> sonnetFile = open(sonnet29.txt)>>> sonnetFile.readlines()[When, in disgrace with fortune and mens eyes,
, I all alone beweep myoutcast state,n, And trouble deaf heaven with my bootless cries,
, Andlook upon myself and curse my fate,]

readlines()返回的list里有4個元素,每個元素都以
結尾. 一行一行的處理文本不僅僅少佔用內存, 程序寫起來也往往更加容易.

寫入文件

在python裡面向文件寫入內容就和用print()輸出一樣簡單. 不過注意, 用閱讀模式打開的文件是不能直接寫入的. 要想寫入或修改, 需要用寫入模式或追加模式打開.

在寫入或者追加模式下, 如果給open()傳入的path指向一個不存在的文件, python不會報錯, 而是會生成一個新的文件. 另外記得在完成讀寫操作以後, 進行close()操作.

接下來試一下下面的代碼, 把前面的概念都練一遍.

>>> baconFile = open(bacon.txt, w)>>> baconFile.write(Hello world!
)13>>> baconFile.close()>>> baconFile = open(bacon.txt, a)>>> baconFile.write(Bacon is not a vegetable.)25>>> baconFile.close()>>> baconFile = open(bacon.txt)>>> content = baconFile.read()>>> baconFile.close()>>> print(content)Hello world!Bacon is not a vegetable.

在上面的代碼裡面, 我們在寫入模式下首先打開了bacon.txt, 由於bacon.txt還不存在, python幫你新建了一個. 對baconFile這個file對象使用write()方法, 把Hello world
寫入文件. 寫入完成後python返回的13是剛才寫入的字元數(包括換行符). 在這之後用close()關閉文件對象.

接下來向已經存在的文件追加文字, 用a模式打開bacon.txt. 向文件寫入Bacon is not vegetable.後, python再次提示寫入的字元數量. 用close()關閉file對象.

最後再次用默認模式打開bacon.txt, read()讀取內容, 存入content, 關閉baconFile文件對象, 輸出content.

注意write()不會自動在末尾添加換行符,你需要自己寫一個. 這點和print()還是不一樣的.

用shelve模塊保存變數

你可以用shelve把python程序裡面的變數用二進位shelf文檔保存. 這樣遲些就可以從硬碟讀取數據恢復變數的值了. shelve模塊讓你給程序添加保存與打開的功能. 比如,你現在的程序有一些變數儲存著程序的設置, 這些設置值就可以存成shelf文檔以備未來讀取.

試一下下面的程序:

>>> import shelve>>> shelfFile = shelve.open(mydata)>>> cats = [Zophie, Pooka, Simon]>>> shelfFile[cats] = cats>>> shelfFile.close()

在用shelve模塊讀寫文件以前先import shelve模塊. 給shelve.open()傳入一個路徑作為參數, 之後shelfFile這個對象就可以當做dictionary來設置shelf值. 我們用shelfFile存儲各種shelf值. 當你完成設置以後, 使用close()方法. 在上面的代碼中,我們生成了一個list: cat, 並且把cat交給shelfFile的cats值. 現在shelfFile[cats]值對應的內容就是cat這個list.

如果是在windows中運行上面的程序你可以在當前工作目錄得到三個新的文件: mydata.bak, bydata.dat以及mydata.dir, 如果你是在osx運行的話, 系統只會新增一個文件:mydata.db.

上面的二進位文件包含了你存儲在shelf對象里的數據. 具體數據是怎麼編碼的並不重要, 你只需要指導shelve能幹什麼就可以了, 怎麼做的不重要. shelve模塊讓你可以不用操心怎麼把自己程序的數據存進文件里.

你的程序可以遲些用shelve模塊重新打開這個數據文件, 從裡面提取數據. shelf文件打開的時候不用設置閱讀或者書寫模式, 只要打開就可以同時讀寫. 試試下面的代碼:

>>> shelfFile = shelve.open(mydata)>>> type(shelfFile)<class shelve.DbfilenameShelf>>>> shelfFile[cats][Zophie, Pooka, Simon]>>> shelfFile.close()

我們打開了mydata這個shelf文件, 存進shelfFile. type(shelfFile)返回的結果說明shelfFile目前是shelf對象. 之後我們用shelfFile[cats]確認之前存進去的數據現在還在cats這個鍵下面. 隨後用close()關閉對象.

就像字典對象(dictionaries)一樣, shelf值也有key()和value()這樣的方法去獲取獲取鍵和值的列表. 不過鑒於這兩個方法返回的是像list一樣的結果, 真正用起來還需要使用list()轉一次. 試試下面的代碼:

>>> shelfFile = shelve.open(mydata)>>> list(shelfFile.keys())[cats]>>> list(shelfFile.values())[[Zophie, Pooka, Simon]]>>> shelfFile.close()

純文本在存儲文本編輯器可以讀寫的文字資料的時候很不錯, 但是如果想存儲程序裡面的數據,可以試試shelve模塊.

用print.pformat()存儲變數數據

在Pretty Printing 部分有提到, pprint.pprint()函數可以把列表或字典的內容"優化格式列印". 而pprint.pformat()可以把準備優化格式列印的這些內容用一個字元串返回. 這種格式優化以後的字元串不止容易閱讀, 同時也是符合python格式規範的. 比如你有一個字典數據存在某個變數里, 現在你想把裡面的內容保存起來並留作以後使用. 用pprint.pformat()可以生成一個符合python格式的字元串, 把這個字元串存進.py文件以後就能用了.

比如試試下面的代碼

>>> import pprint>>> cats = [{name: Zophie, desc: chubby}, {name: Pooka, desc: fluffy}]>>> pprint.pformat(cats)"[{desc: chubby, name: Zophie}, {desc: fluffy, name: Pooka}]">>> fileObj = open(myCats.py, w)>>> fileObj.write(cats = + pprint.pformat(cats) +
)83>>> fileObj.close()

上面的代碼里, 我們導入了pprint模塊來使用pprint.pformat(). cats變數存著一個dictionaries. 為了把cats的內容保存好, 我們用pprint.pformat()把cats做一下格式優化, 之後用file對象的write把做好了格式的cats內容寫進myCats.py文件.

之前我們用import導入的module本身就是python腳本, 當我們把pprint.pformat()返回的字元串存進一個.py文件以後, 遲些就可以和導入其他模塊一樣導入這個py文件了.

鑒於python腳本本身就是純文本文件, 你的python程序完全可以生成新的python腳本. 下面的程序把之前的myCats.py導入:

>>> import myCats>>> myCats.cats[{name: Zophie, desc: chubby}, {name: Pooka, desc: fluffy}]>>> myCats.cats[0]{name: Zophie, desc: chubby}>>> myCats.cats[0][name]Zophie

和shelve模塊比起來, 用一個.py腳本保存數據的優勢在於後者是純文本文檔, 閱讀和編輯都更加容易. 但是對於絕大多數的應用情境來說, shelve是更好的選擇. 同時, 只有像整型, 浮點數, 字元串, 列表和字典這樣的基礎類型才可以寫入文本文檔, File這樣的對象是不能存成文本文檔的.

本章練手項目和課後習題放在另外一個post里翻譯

推薦閱讀:

來編寫你的 setup 腳本(二)
python作為腳本語言和c/c++ 等語言的優勢和劣勢在哪裡地方?python比較成熟用途在哪裡方面?
數據分析精華文章大集合
原生Python寫parser
比特幣價格能預測嗎?(附python代碼) #1

TAG:Python | Python入門 | 辦公自動化 |