Python之IO編程——文件讀寫、StringIO/BytesIO、操作文件和目錄、序列化

IO編程

    IO在計算機中指Input/Output,也就是輸入和輸出。由於程序和運行時數據是在內存中駐留,由CPU這個超快的計算核心來執行,涉及到數據交換的地方,通常是磁盤、網絡等,就需要IO接口。從磁盤讀取文件到內存,就只有Input操作,反過來,把數據寫到磁盤文件裏,就只是一個Output操作。

    由於CPU和內存的速度遠遠高於外設的速度,所以,在IO編程中,就存在速度嚴重不匹配的問題。舉個例子來說,比如要把100M的數據寫入磁盤,CPU輸出100M的數據只需要0.01秒,可是磁盤要接收這100M數據可能需要10秒,怎麼辦呢?有兩種辦法:

    第一種是CPU等着,也就是程序暫停執行後續代碼,等100M的數據在10秒後寫入磁盤,再接着往下執行,這種模式稱爲同步IO;

    另一種方法是CPU不等待,只是告訴磁盤,“您老慢慢寫,不着急,我接着幹別的事去了”,於是,後續代碼可以立刻接着執行,這種模式稱爲異步IO。

    同步和異步的區別就在於是否等待IO執行的結果。異步IO來編寫程序性能會遠遠高於同步IO,但是異步IO的缺點是編程模型複雜

一、文件讀寫

    讀寫文件是最常見的IO操作。Python內置了讀寫文件的函數,用法和C是兼容的。讀寫文件就是請求操作系統打開一個文件對象(通常稱爲文件描述符),然後,通過操作系統提供的接口從這個文件對象中讀取數據(讀文件),或者把數據寫入這個文件對象(寫文件)。

    1.從文件中讀取數據        

         1.1讀取整個文件

    要讀取文件,需要一個包含幾行文本的文件。下面首先來創建一個文件pi_digits.txt,它包含精確到小數點後30位的圓周率值,且在小數點後每10位處都換行:
#pi_digits.txt
3.1415926535
  8979323846
  2643383279
with open('pi_digits.txt') as file_object:
    contents = file_object.read()
    print(contents)

    函數open() 接受一個參數:要打開的文件的名稱。Python在當前執行的文件所在的目錄中查找指定的文件,函數open() 返回一個表示文件的對象。在這裏,open('pi_digits.txt') 返回一個表示文件pi_digits.txt 的對象;Python將這個對象存儲在我們將在後面使用的變量中。

    關鍵字with 在不再需要訪問文件後將其關閉。

    PS:在這個程序中,注意到我們調用了open() ,但沒有調用close() ;調用open() 和close() 來打開和關閉文件,如果程序存在bug,導致close() 語句未執行,文件將不會關閉。未妥善地關閉文件可能會導致數據丟失或受損。如果在程序中過早地調用close() ,需要使用文件時它已關閉(無法訪問),會導致更多的錯誤。通過使用前面所示的結構,可讓Python去確定:你只管打開文件,並在需要時使用它,Python自會在合適的時候自動將其關閉。

    函數read() 讀取這個文件的全部內容,並將其作爲一個長長的字符串存儲在變量contents中。這樣,通過打印contents 的值,就可將這個文本文件的全部內容顯示出來。相比於原始文件,該輸出不同的地方是末尾多了一個空行。read() 到達文件末尾時返回一個空字符串,而將這個空字符串顯示出來時就是一個空行。要刪除多出來的空行,可在print 語句中使用rstrip() :

with open('pi_digits.txt') as file_object:
    contents = file_object.read()
    print(contents.rstrip())    

    調用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')
>>> f.read()
'測試'
    遇到有些編碼不規範的文件,你可能會遇到UnicodeDecodeError,因爲在文本文件中可能夾雜了一些非法編碼的字符。遇到這種情況,open()函數還接收一個errors參數,表示如果遇到編碼錯誤後如何處理。最簡單的方式是直接忽略:
>>> f = open('/Users/michael/gbk.txt', 'r', encoding='gbk', errors='ignore')

    1.2文件路徑

    程序文件存儲在文件夾python_work中,而在文件夾python_work中,有一個名爲text_files的文件夾,用於存儲程序文件操作的文本文件。使用相對文件路徑來打開該文件夾中的文件。相對文件路徑讓Python到指定的位置去查找,而該位置是相對於當前運行的程序所在目錄的。在Linux和OS X中,你可以這樣編寫代碼:
with open('text_files/filename.txt') as file_object:
    在Windows系統中,在文件路徑中使用反斜槓(\ )而不是斜槓(/ ):
with open('text_files\filename.txt') as file_object:

    在相對文件路徑行不通時,可使用絕對文件路徑,絕對路徑通常比相對路徑更長,因此將其存儲在一個變量中,再將該變量傳遞給open() 會有所幫助。在Linux和OS X中,絕對路徑類似於下面這樣:

file_path = '/home/ehmatthes/other_files/text_files/filename.txt'
with open(file_path) as file_object:

    在Windows系統中,它們類似於下面這樣:

file_path = 'C:\Users\ehmatthes\other_files\text_files\filename.txt'
with open(file_path) as file_object:

    通過使用絕對路徑,可讀取系統任何地方的文件。就目前而言,最簡單的做法是,要麼將數據文件存儲在程序文件所在的目錄,要麼將其存儲在程序文件所在目錄下的一個文件夾(如text_files)中。

        1.3逐行讀取

    讀取文件時,常常需要檢查其中的每一行:你可能要在文件中查找特定的信息,或者要以某種方式修改文件中的文本。
    例如,你可能要遍歷一個包含天氣數據的文件,並使用天氣描述中包含字樣sunny的行。在新聞報道中,你可能會查找包含標籤<headline> 的行,並按特定的格式設置它。要以每次一行的方式檢查文件,可對文件對象使用for 循環:
❶ filename = 'pi_digits.txt'

❷ with open(filename) as file_object:
❸     for line in file_object:
          print(line)

    在這個文件中,每行的末尾都有一個看不見的換行符,而print 語句也會加上一個換行符,因此每行末尾都有兩個換行符:一個來自文件,另一個來自print 語句。

3.1415926535

  8979323846

  2643383279

    要消除這些多餘的空白行,可在print 語句中使用rstrip() :

filename = 'pi_digits.txt'

with open(filename) as file_object:
    for line in file_object:
        print(line.rstrip())

        1.4創建一個包含文件各行內容的列表

      使用關鍵字with 時,open() 返回的文件對象只在with 代碼塊內可用。如果要在with 代碼塊外訪問文件的內容,可在with 代碼塊內將文件的各行存儲在一個列表中,並在with 代碼塊外使用該列表:你可以立即處理文件的各個部分,也可推遲到程序後面再處理。
filename = 'pi_digits.txt' 
with open(filename) as file_object:
    lines = file_object.readlines()

for line in lines:
    print(line.rstrip())

        1.5使用文件內容

        將文件讀取到內存中後,就可以以任何方式使用這些數據了。下面以簡單的方式使用圓周率的值。首先,我們將創建一個字符串,它包含文件中存儲的所有數字,且沒有任何空格:
filename = 'pi_digits.txt'

with open(filename) as file_object:
    lines = file_object.readlines()

pi_string = ''
for line in lines:
    pi_string += line.strip()  #變量pi_string存儲的字符串中,包含原來位於每行左邊的空格,爲刪除這些空格,可使用strip()

print(pi_string)
print(len(pi_string))
注意
  讀取文本文件時,Python將其中的所有文本都解讀爲字符串。如果你讀取的是數字,並要將其作爲數值使用,就必須使用函數int() 將其轉換爲整數,或使用函數float() 將其轉換爲浮點數。

        1.6包含一百萬位的大型文件

    對於你可處理的數據量,Python沒有任何限制;只要系統的內存足夠多,你想處理多少數據都可以。包含精確到小數點後1 000 000位,打印前52位,判斷是否包含你的生日。
filename = 'pi_million_digits.txt'

with open(filename) as file_object:
    lines = file_object.readlines()

pi_string = ''
for line in lines:
    pi_string += line.strip()

print(pi_string[:52] + "...")
print(len(pi_string))
birthday = input("Enter your birthday, in the form mmddyy: ")
if birthday in pi_string:
    print("Your birthday appears in the first million digits of pi!")
else:
    print("Your birthday does not appear in the first million digits of pi.")

    2.寫入文件

    保存數據的最簡單的方式之一是將其寫入到文件中

        2.1寫入空文件

    要將文本寫入文件,你在調用open() 時需要提供另一個實參,告訴Python你要寫入打開的文件。傳入標識符'w'或者'wb'表示寫文本文件或寫二進制文件:

 filename = 'programming.txt'

with open(filename, 'w') as file_object:
    file_object.write("I love programming.\n")
    file_object.write("I love creating new games.")#像顯示到終端的輸出一樣,還可以使用空格、製表符和空行來設置這些輸出的格式。

    第一個實參也是要打開的文件的名稱;第二個實參('w' )告訴Python,我們要以寫入模式 打開這個文件。打開文件時,可指定讀取模式 ('r' )、寫入模式 ('w' )、附加模式 ('a' )或讓你能夠讀取和寫入文件的模式('r+' )。如果省略了模式實參,Python將以默認的只讀模式打開文件。

    如果要寫入的文件不存在,函數open() 將自動創建它。然而,以寫入('w' )模式打開文件時,如果指定的文件已經存在,Python將在返回文件對象前清空該文件
    注意:  Python只能將字符串寫入文本文件。要將數值數據存儲到文本文件中,必須先使用函數str() 將其轉換爲字符串格式

        2.2附加到文件

    如果你要給文件添加內容,而不是覆蓋原有的內容,可以附加模式 打開文件。你以附加模式打開文件時,Python不會在返回文件對象前清空文件,而你寫入到文件的行都將添加到文件末尾。如果指定的文件不存在,Python將爲你創建一個空文件。
filename = 'programming.txt'

with open(filename, 'a') as file_object:
     file_object.write("I also love finding meaning in large datasets.\n")
     file_object.write("I love creating apps that can run in a browser.\n")

二、StringIO和BytesIO

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())
...
Hello!
Hi!
Goodbye!

BytesIO

    StringIO操作的只能是str,如果要操作二進制數據,就需要使用BytesIO。BytesIO實現了在內存中讀寫bytes,我們創建一個BytesIO,然後寫入一些bytes: 寫入的不是str,而是經過UTF-8編碼的bytes。

>>> from io import BytesIO
>>> f = BytesIO()
>>> f.write('中文'.encode('utf-8'))
6
>>> print(f.getvalue())
b'\xe4\xb8\xad\xe6\x96\x87'
和StringIO類似,可以用一個bytes初始化BytesIO,然後,像讀文件一樣讀取:
>>> from io import BytesIO
>>> f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')
>>> f.read()
b'\xe4\xb8\xad\xe6\x96\x87'

小結:StringIO和BytesIO是在內存中操作str和bytes的方法,使得和讀寫文件具有一致的接口。

三、操作文件和目錄

    操作文件和目錄的函數一部分放在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中叫pickling,序列化之後,就可以把序列化後的內容寫入磁盤,或者通過網絡傳輸到別的機器上。反過來,把變量內容從序列化的對象重新讀到內存裏稱之爲反序列化,即unpickling。Python提供了pickle模塊來實現序列化。
把一個對象序列化並寫入文件:

>>> import pickle
>>> d = dict(name='Bob', age=20, score=88)
>>> pickle.dumps(d)
b'\x80\x03}q\x00(X\x03\x00\x00\x00ageq\x01K\x14X\x05\x00\x00\x00scoreq\x02KXX\x04\x00\x00\x00nameq\x03X\x03\x00\x00\x00Bobq\x04u.'
>>> f = open('dump.txt', 'wb')
>>> pickle.dump(d, f)
>>> f.close()
    把對象從磁盤讀到內存時,可以先把內容讀到一個bytes,然後用pickle.loads()方法反序列化出對象
>>> f = open('dump.txt', 'rb')
>>> d = pickle.load(f)
>>> f.close()
>>> d
{'age': 20, 'score': 88, 'name': 'Bob'}

  1、JSON

    如果要在不同的編程語言之間傳遞對象,就必須把對象序列化爲標準格式,比如XML,但更好的方法是序列化爲JSON,因爲JSON表示出來就是一個字符串,可以被所有語言讀取,也可以方便地存儲到磁盤或者通過網絡傳輸。JSON不僅是標準格式,並且比XML更快,而且可以直接在Web頁面中讀取,非常方便。

    Python內置的json模塊提供了非常完善的Python對象到JSON格式的轉換

    JSON表示的對象就是標準的JavaScript語言的對象,JSON和Python內置的數據類型對應如下:

JSONPython
{}dict
[]list
"string"str
1234.56int或float
true/falseTrue/False
nullNone

    1.1使用模塊json 來存儲數據    

    1.1.1使用json.dump() 和json.load()

    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
    函數json.dump() 接受兩個實參:要存儲的數據以及可用於存儲數據的文件對象。下面演示瞭如何使用json.dump() 來存儲數字列表:
import json
numbers = [2, 3, 5, 7, 11, 13]
filename = 'numbers.json'
with open(filename, 'w') as f_obj:
    json.dump(numbers, f_obj)
    這個程序沒有輸出,但我們可以打開文件numbers.json,看看其內容。數據的存儲格式與Python中一樣。
     對中文進行JSON序列化時,json.dumps()提供了一個ensure_ascii參數
import json
obj = dict(name='小明', age=20)
s = json.dumps(obj, ensure_ascii=False)
print(s)
     要把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.load() 將這個列表讀取到內存中:
import json

filename = 'numbers.json'
with open(filename) as f_obj:
    numbers = json.load(f_obj)

print(numbers)

    1.2保存和讀取用戶生成的數據

    對於用戶生成的數據,使用json 保存它們大有裨益,因爲如果不以某種方式進行存儲,等程序停止運行時用戶的信息將丟失。
    程序運行時,我們將嘗試從文件username.json中獲取用戶名,因此我們首先編寫一個嘗試恢復用戶名的try 代碼塊。如果這個文件不存在,我們就在except 代碼塊中提示用戶輸入用戶名,並將其存儲在username.json中,以便程序再次運行時能夠獲取它:
import json

  # 如果以前存儲了用戶名,就加載它
  #  否則,就提示用戶輸入用戶名並存儲它
  filename = 'username.json'
  try:
❶     with open(filename) as f_obj:    #嘗試打開文件username.json。如果這個文件存在,就將其中的用戶名讀取到內存中(見❷)
❷         username = json.load(f_obj)
❸ except FileNotFoundError: #用戶首次運行這個程序時,文件username.json不存在,將引發FileNotFoundError 異常
❹     username = input("What is your name? ")
❺     with open(filename, 'w') as f_obj:
          json.dump(username, f_obj)
          print("We'll remember you when you come back, " + username + "!")
  else:
      print("Welcome back, " + username + "!")
    無論執行的是except 代碼塊還是else 代碼塊,都將顯示用戶名和合適的問候語。如果這個程序是首次運行,輸出將如下:
What is your name? Eric
We'll remember you when you come back, Eric!
否則,輸出將如下:
Welcome back, Eric!

    

    1.3重構

    你經常會遇到這樣的情況:代碼能夠正確地運行,但可做進一步的改進——將代碼劃分爲一系列完成具體工作的函數。這樣的過程被稱爲重構 。重構讓代碼更清晰、更易於理解、更容易擴展。

    要重構1.2代碼,可將其大部分邏輯放到一個或多個函數中。1.2代碼的重點是問候用戶,因此我們將其所有代碼都放到一個名爲greet_user() 的函數中:
import json

  def greet_user():
❶     """問候用戶,並指出其名字"""
      filename = 'username.json'
      try:
          with open(filename) as f_obj:
              username = json.load(f_obj)
      except FileNotFoundError:
          username = input("What is your name? ")
          with open(filename, 'w') as f_obj:
              json.dump(username, f_obj)
              print("We'll remember you when you come back, " + username + "!")
      else:
          print("Welcome back, " + username + "!")


greet_user()
這個程序更清晰些,但函數greet_user() 所做的不僅僅是問候用戶,還在存儲了用戶名時獲取它,而在沒有存儲用戶名時提示用戶輸入一個。
下面來重構greet_user() ,讓它不執行這麼多任務。爲此,我們首先將獲取存儲的用戶名的代碼移到另一個函數中:
import json

  def get_stored_username():
❶     """如果存儲了用戶名,就獲取它"""
      filename = 'username.json'
      try:
          with open(filename) as f_obj:
              username = json.load(f_obj)
      except FileNotFoundError:
❷         return None
      else:
          return username

  def greet_user():
      """問候用戶,並指出其名字"""
      username = get_stored_username()
❸     if username:
          print("Welcome back, " + username + "!")
      else:
          username = input("What is your name? ")
          filename = 'username.json'
          with open(filename, 'w') as f_obj:
              json.dump(username, f_obj)
              print("We'll remember you when you come back, " + username + "!")

  greet_user()
    新增的函數get_stored_username() 目標明確,❶處的文檔字符串指出了這一點。如果存儲了用戶名,這個函數就獲取並返回它;如果文件username.json不存在,這個函數就返回None (見❷)。這是一種不錯的做法:函數要麼返回預期的值,要麼返回None ;這讓我們能夠使用函數的返回值做簡單測試。在❸處,如果成功地獲取了用戶名,就打印一條歡迎用戶回來的消息,否則就提示用戶輸入用戶名。

    我們還需將greet_user() 中的另一個代碼塊提取出來:將沒有存儲用戶名時提示用戶輸入的代碼放在一個獨立的函數中:
import json

def get_stored_username():
    """如果存儲了用戶名,就獲取它"""
    --snip--

def get_new_username():
    """提示用戶輸入用戶名"""
    username = input("What is your name? ")
    filename = 'username.json'
    with open(filename, 'w') as f_obj:
        json.dump(username, f_obj)
    return username


def greet_user():
    """問候用戶,並指出其名字"""
    username = get_stored_username()
    if username:
        print("Welcome back, " + username + "!")
    else:
        username = get_new_username()
        print("We'll remember you when you come back, " + username + "!")


greet_user()
這個最終版本中,每個函數都執行單一而清晰的任務。我們調用greet_user() ,它打印一條合適的消息:要麼歡迎老用戶回來,要麼問候新用戶。爲此,它首先調用get_stored_username() ,這個函數只負責獲取存儲的用戶名(如果存儲了的話),再在必要時調用get_new_username() ,這個函數只負責獲取並存儲新用戶的用戶名。要編寫出清晰而易於維護和擴展的代碼,這種劃分工作必不可少。

發佈了35 篇原創文章 · 獲贊 21 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章