在磁碟上讀寫文件的功能都是由操作系統提供的,現代操作系統不允許普通的程式直接操作磁碟,所以,讀寫文件就是請求操作系統打開一個文件對象(通常稱為文件描述符),然後,通過操作系統提供的介面從這個文件對象中讀取數據(讀文件),或者把數據寫入這個文件對象(寫文件)。 讀文件 要以讀文件的模式打開一個文件對象 ...
在磁碟上讀寫文件的功能都是由操作系統提供的,現代操作系統不允許普通的程式直接操作磁碟,所以,讀寫文件就是請求操作系統打開一個文件對象(通常稱為文件描述符),然後,通過操作系統提供的介面從這個文件對象中讀取數據(讀文件),或者把數據寫入這個文件對象(寫文件)。
讀文件
要以讀文件的模式打開一個文件對象,使用Python內置的open()
函數,傳入文件名和標示符:
>>> f = open('/Users/michael/test.txt', 'r')
標示符 'r' 表示讀,這樣,我們就成功地打開了一個文件。
如果文件不存在,open()
函數就會拋出一個IOError
的錯誤,並且給出錯誤碼和詳細的信息告訴你文件不存在:
Traceback (most recent call last): File "<stdin>", line 1, in <module> FileNotFoundError: [Errno 2] No such file or directory: '/Users/michael/notfound.txt'
如果文件打開成功,接下來,調用read()
方法可以一次讀取文件的全部內容,Python把內容讀到記憶體,用一個str
對象表示:
>>> f.read() 'Hello, world!'
最後一步是調用close()
方法關閉文件。文件使用完畢後必須關閉,因為文件對象會占用操作系統的資源,並且操作系統同一時間能打開的文件數量也是有限的:
>>> f.close()
由於文件讀寫時都有可能產生IOError
,一旦出錯,後面的f.close()
就不會調用。所以,為了保證無論是否出錯都能正確地關閉文件,我們可以使用try ... finally
來實現:
try:
f = open('/path/to/file', 'r') finally: if f: f.close()
但是每次都這麼寫實在太繁瑣,所以,Python引入了with
語句來自動幫我們調用close()
方法:
with open('/path/to/file', 'r') as f: print(f.read())
這和前面的try ... finally
是一樣的,但是代碼更佳簡潔,並且不必調用f.close()
方法。
調用read()
會一次性讀取文件的全部內容,如果文件有10G,記憶體就爆了,所以,要保險起見,可以反覆調用read(size)
方法,每次最多讀取size個位元組的內容。另外,調用readline()
可以每次讀取一行內容,調用readlines()
一次讀取所有內容並按行返回list
。因此,要根據需要決定怎麼調用。
如果文件很小,read()
一次性讀取最方便;如果不能確定文件大小,反覆調用read(size)
比較保險;如果是配置文件,調用readlines()
最方便:
for line in f.readlines(): print(line.strip()) # 把末尾的'\n'刪掉
file-like Object
像open()
函數返回的這種有個read()
方法的對象,在Python中統稱為file-like Object。除了file外,還可以是記憶體的位元組流,網路流,自定義流等等。file-like Object不要求從特定類繼承,只要寫個read()
方法就行。
StringIO
就是在記憶體中創建的file-like Object,常用作臨時緩衝。
二進位文件
前面講的預設都是讀取文本文件,並且是UTF-8編碼的文本文件。要讀取二進位文件,比如圖片、視頻等等,用 'rb'
模式打開文件即可:
>>> f = open('/Users/michael/test.jpg', 'rb') >>> f.read() b'\xff\xd8\xff\xe1\x00\x18Exif\x00\x00...' # 十六進位表示的位元組
字元編碼
要讀取非UTF-8編碼的文本文件,需要給open()
函數傳入encoding
參數,例如,讀取GBK編碼的文件:
>>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk')
遇到有些編碼不規範的文件,你可能會遇到UnicodeDecodeError
,因為在文本文件中可能夾雜了一些非法編碼的字元。遇到這種情況,open()
函數還接收一個errors
參數,表示如果遇到編碼錯誤後如何處理。最簡單的方式是直接忽略:
>>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk', errors='ignore')
寫文件
寫文件和讀文件是一樣的,唯一區別是調用open()
函數時,傳入標識符'w'
或者'wb'
表示寫文本文件或寫二進位文件:
>>> f = open('/Users/michael/test.txt', 'w') >>> f.write('Hello, world!') >>> f.close()
你可以反覆調用write()
來寫入文件,但是務必要調用f.close()
來關閉文件。當我們寫文件時,操作系統往往不會立刻把數據寫入磁碟,而是放到記憶體緩存起來,空閑的時候再慢慢寫入。只有調用close()
方法時,操作系統才保證把沒有寫入的數據全部寫入磁碟。忘記調用close()
的後果是數據可能只寫了一部分到磁碟,剩下的丟失了。所以,還是用with
語句來得保險:
with open('/Users/michael/test.txt', 'w') as f: f.write('Hello, world!')
要寫入特定編碼的文本文件,請給open()
函數傳入encoding
參數,將字元串自動轉換成指定編碼。
在Python中,文件讀寫是通過open()
函數打開的文件對象完成的。使用 with
語句操作文件IO是個好習慣。
StringIO
很多時候,數據讀寫不一定是文件,也可以在記憶體中讀寫。
StringIO顧名思義就是在記憶體中讀寫str。
要把str寫入StringIO,我們需要先創建一個StringIO,然後,像文件一樣寫入即可:
>>> from io import StringIO >>> f = StringIO() >>> f.write('hello') # 5 >>> f.write(' ') # 1 >>> f.write('world!') # 6 >>> print(f.getvalue()) # hello world!
getvalue()
方法用於獲得寫入後的str。
要讀取StringIO,可以用一個str初始化StringIO,然後,像讀文件一樣讀取:
>>> from io import StringIO >>> f = StringIO('Hello!\nHi!\nGoodbye!') >>> while True: ... s = f.readline() ... if s == '': ... break ... print(s.strip())
BytesIO
StringIO操作的只能是str,如果要操作二進位數據,就需要使用BytesIO。
BytesIO實現了在記憶體中讀寫bytes,我們創建一個BytesIO,然後寫入一些bytes:
>>> from io import BytesIO >>> f = BytesIO() >>> f.write('中文'.encode('utf-8')) # 6 >>> print(f.getvalue()) # b'\xe4\xb8\xad\xe6\x96\x87'
請註意,寫入的不是str,而是經過UTF-8編碼的bytes。
和StringIO類似,可以用一個bytes初始化BytesIO,然後,像讀文件一樣讀取:
>>> from io import StringIO >>> f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87') >>> f.read() # b'\xe4\xb8\xad\xe6\x96\x87'
StringIO和BytesIO是在記憶體中操作str和bytes的方法,使得和讀寫文件具有一致的介面。
操作文件和目錄
如果我們要操作文件、目錄,可以在命令行下麵輸入操作系統提供的各種命令來完成。比如dir
、cp
等命令。
如果要在Python程式中執行這些目錄和文件的操作怎麼辦?其實操作系統提供的命令只是簡單地調用了操作系統提供的介面函數,Python內置的os
模塊也可以直接調用操作系統提供的介面函數。
打開Python互動式命令行,我們來看看如何使用os
模塊的基本功能:
>>> import os >>> os.name # 操作系統類型 'posix'
如果是posix
,說明系統是Linux
、Unix
或Mac OS X
,如果是nt
,就是Windows
系統。
要獲取詳細的系統信息,可以調用uname()
函數:
>>> os.uname() posix.uname_result(sysname='Darwin', nodename='MichaelMacPro.local', release='14.3.0', version='Darwin Kernel Version 14.3.0: Mon Mar 23 11:59:05 PDT 2015; root:xnu-2782.20.48~5/RELEASE_X86_64', machine='x86_64')
註意
uname()
函數在Windows上不提供,也就是說,os
模塊的某些函數是跟操作系統相關的。
環境變數
在操作系統中定義的環境變數,全部保存在os.environ
這個變數中,可以直接查看:
>>> os.environ environ({'VERSIONER_PYTHON_PREFER_32_BIT': 'no', 'TERM_PROGRAM_VERSION': '326', 'LOGNAME': 'michael', 'USER': 'michael', 'PATH': '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/X11/bin:/usr/local/mysql/bin', ...})
要獲取某個環境變數的值,可以調用os.environ.get('key')
:
>>> os.environ.get('PATH') '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/X11/bin:/usr/local/mysql/bin' >>> os.environ.get('x', 'default') 'default'
操作文件和目錄
操作文件和目錄的函數一部分放在os
模塊中,一部分放在os.path
模塊中,這一點要註意一下。查看、創建和刪除目錄可以這麼調用:
# 查看當前目錄的絕對路徑: >>> os.path.abspath('.') '/Users/michael' # 在某個目錄下創建一個新目錄,首先把新目錄的完整路徑表示出來: >>> os.path.join('/Users/michael', 'testdir') '/Users/michael/testdir' >>> os.mkdir('/Users/michael/testdir') # 然後創建一個目錄: >>> os.rmdir('/Users/michael/testdir') # 刪掉一個目錄:
把兩個路徑合成一個時,不要直接拼字元串,而要通過os.path.join()
函數,這樣可以正確處理不同操作系統的路徑分隔符。在Linux/Unix/Mac下,os.path.join()
返回這樣的字元串:
part-1/part-2
而Windows下會返回這樣的字元串:
part-1\part-2
同樣的道理,要拆分路徑時,也不要直接去拆字元串,而要通過os.path.split()
函數,這樣可以把一個路徑拆分為兩部分,後一部分總是最後級別的目錄或文件名:
>>> os.path.split('/Users/michael/testdir/file.txt') ('/Users/michael/testdir', 'file.txt')
os.path.splitext()
可以直接讓你得到文件擴展名,很多時候非常方便:
>>> os.path.splitext('/path/to/file.txt') ('/path/to/file', '.txt')
這些合併、拆分路徑的函數並不要求目錄和文件要真實存在,它們只對字元串進行操作。
文件操作使用下麵的函數。假定當前目錄下有一個test.txt
文件:
# 對文件重命名: >>> os.rename('test.txt', 'test.py') # 刪掉文件: >>> os.remove('test.py')
但是複製文件的函數居然在os
模塊中不存在!原因是複製文件並非由操作系統提供的系統調用。理論上講,我們通過上一節的讀寫文件可以完成文件複製,只不過要多寫很多代碼。
幸運的是shutil
模塊提供了copyfile()
的函數,你還可以在shutil
模塊中找到很多實用函數,它們可以看做是os
模塊的補充。
最後看看如何利用Python的特性來過濾文件。比如我們要列出當前目錄下的所有目錄,只需要一行代碼:
>>> [x for x in os.listdir('.') if os.path.isdir(x)] ['.lein', '.local', '.m2', '.npm', '.ssh', '.Trash', '.vim', 'Applications', 'Desktop', ...]
要列出所有的.py
文件,也只需一行代碼:
>>> [x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1]=='.py'] ['apis.py', 'config.py', 'models.py', 'pymonitor.py', 'test_db.py', 'urls.py', 'wsgiapp.py']
Python的os
模塊封裝了操作系統的目錄和文件操作,要註意這些函數有的在os
模塊中,有的在os.path
模塊中!
序列化
在程式運行的過程中,所有的變數都是在記憶體中,比如,定義一個dict:
d = dict(name='Bob', age=20, score=88)
可以隨時修改變數,比如把name
改成'Bill'
,但是一旦程式結束,變數所占用的記憶體就被操作系統全部回收。如果沒有把修改後的'Bill'
存儲到磁碟上,下次重新運行程式,變數又被初始化為'Bob'
。
我們把變數從記憶體中變成可存儲或傳輸的過程稱之為序列化,在Python中叫pickling,在其他語言中也被稱之為serialization,marshalling,flattening等等,都是一個意思。
序列化之後,就可以把序列化後的內容寫入磁碟,或者通過網路傳輸到別的機器上。
反過來,把變數內容從序列化的對象重新讀到記憶體里稱之為反序列化,即unpickling。
Python提供了pickle
模塊來實現序列化。
首先,我們嘗試把一個對象序列化並寫入文件:
>>> import pickle >>> d = dict(name='Bob', age=20, score=88) >>> pickle.dumps(d)
pickle.dumps()
方法把任意對象序列化成一個bytes
,然後,就可以把這個bytes
寫入文件。或者用另一個方法pickle.dump()
直接把對象序列化後寫入一個file-like Object:>>> f = open('dump.txt', 'wb') >>> pickle.dump(d, f) >>> f.close()
看看寫入的dump.txt
文件,一堆亂七八糟的內容,這些都是Python保存的對象內部信息。
當我們要把對象從磁碟讀到記憶體時,可以先把內容讀到一個bytes
,然後用pickle.loads()
方法反序列化出對象,也可以直接用pickle.load()
方法從一個file-like Object
中直接反序列化出對象。我們打開另一個Python命令行來反序列化剛纔保存的對象:
>>> f = open('dump.txt', 'rb') >>> d = pickle.load(f) >>> f.close() >>> d # {'age': 20, 'score': 88, 'name': 'Bob'}
Pickle的問題和所有其他編程語言特有的序列化問題一樣,就是它只能用於Python,並且可能不同版本的Python彼此都不相容,因此,只能用Pickle保存那些不重要的數據,不能成功地反序列化也沒關係。
JSON
如果我們要在不同的編程語言之間傳遞對象,就必須把對象序列化為標準格式,比如XML,但更好的方法是序列化為JSON,因為JSON表示出來就是一個字元串,可以被所有語言讀取,也可以方便地存儲到磁碟或者通過網路傳輸。JSON不僅是標準格式,並且比XML更快,而且可以直接在Web頁面中讀取,非常方便。
JSON表示的對象就是標準的JavaScript語言的對象,JSON和Python內置的數據類型對應如下:
JSON類型 | Python類型 |
{} | dict |
[] | list |
"string" | str |
1234.56 | int或float |
true/false | True/False |
null | None |
Python內置的json
模塊提供了非常完善的Python對象到JSON格式的轉換。我們先看看如何把Python對象變成一個JSON:
>>> import json >>> d = dict(name='Bob', age=20, score=88) >>> json.dumps(d) #'{"age": 20, "score": 88, "name": "Bob"}'
dumps()
方法返回一個str
,內容就是標準的JSON。類似的,dump()
方法可以直接把JSON寫入一個file-like Object
。
要把JSON反序列化為Python對象,用loads()
或者對應的load()
方法,前者把JSON的字元串反序列化,後者從file-like Object
中讀取字元串並反序列化:
>>> json_str = '{"age": 20, "score": 88, "name": "Bob"}' >>> json.loads(json_str) {'age': 20, 'score': 88, 'name': 'Bob'}
由於JSON標準規定JSON編碼是UTF-8,所以我們總是能正確地在Python的str
與JSON的字元串之間轉換。
JSON進階
Python的dict
對象可以直接序列化為JSON的{}
,不過,很多時候,我們更喜歡用class
表示對象,比如定義Student
類,然後序列化:
import json class Student(object): def __init__(self, name, age, score): self.name = name self.age = age self.score = score s = Student('Bob', 20, 88) print(json.dumps(s))
運行代碼,毫不留情地得到一個TypeError
:
Traceback (most recent call last): ... TypeError: <__main__.Student object at 0x10603cc50> is not JSON serializable
錯誤的原因是Student
對象不是一個可序列化為JSON的對象。
如果連class
的實例對象都無法序列化為JSON,這肯定不合理!
別急,我們仔細看看dumps()
方法的參數列表,可以發現,除了第一個必須的obj
參數外,dumps()
方法還提供了一大堆的可選參數:
https://docs.python.org/3/library/json.html#json.dumps
這些可選參數就是讓我們來定製JSON序列化。前面的代碼之所以無法把Student
類實例序列化為JSON,是因為預設情況下,dumps()
方法不知道如何將Student
實例變為一個JSON的{}
對象。
可選參數default
就是把任意一個對象變成一個可序列為JSON的對象,我們只需要為Student
專門寫一個轉換函數,再把函數傳進去即可:
def student2dict(std): return { 'name': std.name, 'age': std.age, 'score': std.score }
這樣,Student
實例首先被student2dict()
函數轉換成dict
,然後再被順利序列化為JSON:
>>> print(json.dumps(s, default=student2dict)) {"age": 20, "name": "Bob", "score": 88}
不過,下次如果遇到一個Teacher
類的實例,照樣無法序列化為JSON。我們可以偷個懶,把任意class
的實例變為dict
:
print(json.dumps(s, default=lambda obj: obj.__dict__))
因為通常class
的實例都有一個__dict__
屬性,它就是一個dict
,用來存儲實例變數。也有少數例外,比如定義了__slots__
的class。
同樣的道理,如果我們要把JSON反序列化為一個Student
對象實例,loads()
方法首先轉換出一個dict
對象,然後,我們傳入的object_hook
函數負責把dict
轉換為Student
實例:
def dict2student(d): return Student(d['name'], d['age'], d['score'])
運行結果如下:
>>> json_str = '{"age": 20, "score": 88, "name": "Bob"}' >>> print(json.loads(json_str, object_hook=dict2student)) <__main__.Student object at 0x10cd3c190>
列印出的是反序列化的Student
實例對象。
Python語言特定的序列化模塊是pickle
,但如果要把序列化搞得更通用、更符合Web標準,就可以使用json
模塊。
json
模塊的dumps()
和loads()
函數是定義得非常好的介面的典範。當我們使用時,只需要傳入一個必須的參數。但是,當預設的序列化或反序列機制不滿足我們的要求時,我們又可以傳入更多的參數來定製序列化或反序列化的規則,既做到了介面簡單易用,又做到了充分的擴展性和靈活性。