Python與文件流

Python讀寫文件非常簡(jiǎn)單,本文除了介紹簡(jiǎn)單的讀寫字符文件和字節(jié)文件以外,還會(huì)介紹文件對(duì)象的屬性方法和文件流的一些操作,文章內(nèi)容包含以下方面:

  1. 字符文件和字節(jié)文件
  2. 讀寫字符文件
  3. 讀寫字節(jié)文件
  4. 上下文with操作文件
  5. 分析文件對(duì)象的源碼
  6. 文件流的屬性方法

歡迎關(guān)注我的微信公眾號(hào):“數(shù)學(xué)編程”和個(gè)人博客

字符文件和字節(jié)文件

字符文件通常是一些存儲(chǔ)字符串的文本文件。在windows記事本創(chuàng)建的txt文件,程序的源代碼文件,網(wǎng)頁(yè)的html文件都是字符文件;字節(jié)文件是指存儲(chǔ)字節(jié)碼的文件,也是二進(jìn)制的文件。比如windows下可執(zhí)行的exe文件,圖片,音頻視頻文件都是二進(jìn)制文件。這些文件由0和1構(gòu)成。

理論上說(shuō)所有在計(jì)算機(jī)上面的文件最終存儲(chǔ)形式都是0和1的文件,下面是一個(gè)圖片文件的二進(jìn)制,通常是轉(zhuǎn)為16進(jìn)制,就長(zhǎng)這樣子:

00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
00000010: 0000 0374 0000 0370 0806 0000 0067 0128  ...t...p.....g.(
00000020: ad00 0004 1969 4343 506b 4347 436f 6c6f  .....iCCPkCGColo
00000030: 7253 7061 6365 4765 6e65 7269 6352 4742  rSpaceGenericRGB
00000040: 0000 388d 8d55 5d68 1c55 143e bb73 6723  ..8..U]h.U.>.sg#
00000050: 24ce 536c 3485 74a8 3f0d 250d 9356 34a1  $.Sl4.t.?.%..V4.
00000060: b4ba 7fdd dd36 6e96 4936 da22 e864 f6ee  .....6n.I6.".d..
00000070: ce98 c9ce 3833 bbfd a14f 4550 7c31 ea9b  ....83...OEP|1..
00000080: 14c4 bfb7 8020 28f5 0fdb 3eb4 2f95 0a25  ..... (...>./..%
00000090: dad4 2028 3eb4 f883 50e8 8ba6 eb99 3b33  .. (>...P.....;3
000000a0: 9969 bab1 de65 ee7c f39d ef9e 7bee b967  .i...e.|....{..g
000000b0: ef05 e8b9 aa58 9691 1401 169a ae2d 1732  .....X.......-.2
000000c0: e273 878f 883d 2b90 8487 a017 06a1 5751  .s...=+.......WQ
000000d0: 1d2b 5da9 4c02 364f 0b77 b55b df43 c27b  .+].L.6O.w.[.C.{
000000e0: 5fd9 d5dd fe9f adb7 461d 1520 711f 62b3  _.......F.. q.b.
000000f0: e6a8 0b88 8f01 f0a7 55cb 7601 7afa 911f  ........U.v.z...
00000100: 3fea 5a1e f662 e8b7 3140 c42f 7ab8 e163  ?.Z..b..1@./z..c

根據(jù)文件類型通常分為字符文件和字節(jié)文件(二進(jìn)制文件),于是Python中操作這兩種文件都有對(duì)應(yīng)的方法,不要用混了。

讀寫字符文件

讀寫普通的字符文件非常容易。比如創(chuàng)建一個(gè)字符文件,并且往里面寫入1-100的數(shù)字。每個(gè)數(shù)字占一行。

filename = "demo.txt"
# 以寫入的方式創(chuàng)建文件,如果不存在則直接創(chuàng)建
# 如果存在則會(huì)覆蓋
fd = open(filename, 'w')

for num in range(1, 101):
    # 寫入比如是str的字符
    # 拼接換行符
    fd.write(str(num) + "\n")
# 關(guān)閉文件對(duì)象
fd.close()

接下來(lái)讀取這個(gè)文件,并且計(jì)算出這些數(shù)字的總和。

total = 0

for line in open("demo.txt"):
    total += int(line.strip()) # 去除末尾換行符

print(total)

# 5050

讀取文件一行代碼就能搞定。

讀寫字節(jié)文件

字節(jié)文件的讀取與字符文件讀取基本一樣,唯一的區(qū)別在于指定讀寫文件模式,open函數(shù)有一個(gè)參數(shù)mode,字節(jié)文件的讀取為“rb”,寫入為“wb”,其中b的意思是binary。

首先讀取圖片,然后再把這張圖片寫入另外的字節(jié)文件,有點(diǎn)像復(fù)制與粘貼。先來(lái)看讀取字節(jié)碼文件,然后打印出來(lái):

fd = open("pic.png", "rb")
content = fd.read()
print(content)
fd.close()

輸出內(nèi)容是:

\x94\x9cqk\x04\x9c\x8cQMa\xb0S\xe9\xdb\x9d\xdb\xbc\x9dYxMO\x13...

這個(gè)就是unicode編碼,Python在處理數(shù)據(jù)時(shí),在內(nèi)存中統(tǒng)一的編碼都是unicode碼。我們把讀取的unicode編碼寫入字節(jié)文件,就得到原始的圖片了。

fd = open("pic.png", "rb")
content = fd.read()
print(content)
fd.close()

fw = open("out.png", "wb")
fw.write(content)
fw.close()

明白了這個(gè)原理,用Python在網(wǎng)上下載圖片或者視頻的時(shí)候就能用上了,本質(zhì)上就是把網(wǎng)上的字節(jié)文件,寫入本地磁盤。就是這么容易。

上下文with操作文件

with語(yǔ)句是一種上下文資源管理協(xié)議,使用with語(yǔ)句格式可以專注于你的操作,而忽視資源的關(guān)閉。因?yàn)橘Y源打開以后,可能會(huì)有問(wèn)題,導(dǎo)致資源無(wú)法回收而占用系統(tǒng)資源。使用with可以這樣操作。

with open("demo.txt") as fd:
    for num in range(1, 101):
        # 寫入比如是str的字符
        # 拼接換行符
        fd.write(str(num) + "\n")
with open("pic.png", "rb") as fd:
    content = fd.read()
    print(content)

with open("out.png", "wb") as fw:
    fw.write(content)

省掉了關(guān)閉文件的操作,在with的語(yǔ)境下,你的程序出現(xiàn)異常,文件依然能夠保證關(guān)閉。

分析文件對(duì)象的源碼

open函數(shù)的源碼使用C語(yǔ)言實(shí)現(xiàn)的,屬于builtins方法(內(nèi)建方法)。我們來(lái)看看幾個(gè)關(guān)鍵參數(shù):

def open(file, mode='r',
         buffering=None, 
         encoding=None, 
         errors=None, 
         newline=None, 
         closefd=True):  # known special case of open
         """
         Open file and return a stream.  Raise OSError upon failure...
         """
    pass

file 不需要多說(shuō),文件的名稱。mode用很多參數(shù),r和w以及b都已經(jīng)用過(guò)了,a是append的意思,以寫的方式打開,追加的文件最后。+號(hào)的含義是以更新的方式打開,包括讀和寫操作。

 ===============================================================
    Character Meaning
    --------- ---------------------------------------------------------------
    'r'       open for reading (default)
    'w'       open for writing, truncating the file first
    'x'       create a new file and open it for writing
    'a'       open for writing, appending to the end of the file if it exists
    'b'       binary mode
    't'       text mode (default)
    '+'       open a disk file for updating (reading and writing)
    'U'       universal newline mode (deprecated)
    ========= ===============================================================

buffering 參數(shù)表示緩沖區(qū)的的策略。分別取值為None,0,1,大于1。None表示默認(rèn)的策略,數(shù)據(jù)塊大小為8192個(gè)字節(jié),每次讀寫的單位就是這個(gè)數(shù)據(jù)塊大小。讀寫文件首先會(huì)讀寫在一塊緩沖區(qū),然后再把內(nèi)容flush進(jìn)文件,對(duì)于大文件的操作會(huì)修改這個(gè)參數(shù),例如要給一個(gè)大于內(nèi)存的文件內(nèi)容進(jìn)行排序或者搜索,只能分塊讀入內(nèi)存中。其余策略可以參考源碼。

encoding 編碼方式,只能在字符文件中有效,字節(jié)文件沒有編碼方式。不指定這個(gè)參數(shù)通常會(huì)根據(jù)系統(tǒng)的來(lái)決定,如果讀取文件出現(xiàn)亂碼,則考慮指定文件的編碼方方式,最常用的就是utf-8和gbk兩種。

看完了這幾個(gè)參數(shù)我們來(lái)看看返回值,就是io模塊下的TextIOWrapper類。于是我們打開這個(gè)類,順便打印出屬性和方法:

>>> open("file1.txt")
>>> <_io.TextIOWrapper name='file1.txt' mode='r' encoding='UTF-8'>
>>> from io import TextIOWrapper
>>> for each in dir(TextIOWrapper):
   ...: if not each.startswith("_"):
   ...:     print(each)
   # 內(nèi)容較多省略

如果使用IDE查看源碼最方便,打開源碼我們發(fā)現(xiàn)這個(gè)類又是內(nèi)建模塊。我們接下來(lái)看看文件流的一些常用方法。

文件流的屬性方法

open函數(shù)返回的對(duì)象就是文件流(Open file and return a stream. Raise OSError upon failure)。上面我們看到返回的是_io.TextIOWrapper這個(gè)類。文件流有很多方法,上面已經(jīng)用到了一些,例如read讀取全部?jī)?nèi)容,write寫入內(nèi)容。接下來(lái)我們通過(guò)一個(gè)例子來(lái)說(shuō)明這些方法。

# 創(chuàng)建文件,寫入1-100,并且每個(gè)一個(gè)數(shù)字占一行
In [5]: with open("demo.txt", 'w') as fd:
   ...:     for num in range(1, 101):
   ...:         fd.write(str(num)+"\n")
   ...:
   In [8]: fd = open("demo.txt")

In [9]: fd
Out[9]: <_io.TextIOWrapper name='demo.txt' mode='r' encoding='UTF-8'>

In [10]: help(fd.readline)

In [11]: fd.readline() # 每次讀取一行
Out[11]: '1\n'

In [12]: fd.readline()
Out[12]: '2\n'

In [13]: fd.readline()
Out[13]: '3\n'

In [14]: fd.readline()
Out[14]: '4\n'

In [15]: fd.readline()
Out[15]: '5\n'

In [16]: fd.readline()
Out[16]: '6\n'
# 每次讀取兩個(gè)字符,指針的位置在第13個(gè)字符的位置了(從0開始)
In [17]: fd.tell() # 查看指針位置
Out[17]: 12

In [18]: fd.read() # 從當(dāng)前指針位置讀取剩余全部?jī)?nèi)容
Out[18]: '7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n50\n51\n52\n53\n54\n55\n56\n57\n58\n59\n60\n61\n62\n63\n64\n65\n66\n67\n68\n69\n70\n71\n72\n73\n74\n75\n76\n77\n78\n79\n80\n81\n82\n83\n84\n85\n86\n87\n88\n89\n90\n91\n92\n93\n94\n95\n96\n97\n98\n99\n100\n'

In [19]: fd.read() # 再往下讀為空了
Out[19]: ''
In [20]: fd.tell() # 查看指針的位置
Out[20]: 292 # 292意味著已經(jīng)輸出了292個(gè)字符長(zhǎng)度了

很容易計(jì)算出來(lái)9\times 2 +3\times90 + 4=292,指針的偏移量為292個(gè)字符,已經(jīng)到了文件的末尾了。既然可以獲取到文件的指針位置,那么如何移動(dòng)文件指針的位置呢?這就用到seek方法了。

In [21]: fd.read()
Out[21]: ''
# 再次read發(fā)現(xiàn)指針的位置不會(huì)變化了
In [22]: fd.tell()
Out[22]: 292
In [23]: fd.seek(0) # 指針回到開始位置
Out[23]: 0
In [24]: fd.tell() # 獲取指針位置
Out[24]: 0
In [25]: data = fd.readlines()# 這次我們讀取所有行

In [26]: data[:10] # 查看前10個(gè)元素
Out[26]: ['1\n', '2\n', '3\n', '4\n', '5\n', '6\n', '7\n', '8\n', '9\n', '10\n']

readlines方法按行讀取所有的數(shù)據(jù)(注意每個(gè)元素包含了換行符)。這里補(bǔ)充seek(offset,reference_point),其中offset是偏移量,reference_point相對(duì)指針的位置,取值為0表示開始位置,1表示當(dāng)前位置,2表示結(jié)束位置。這種情況只適用于字節(jié)方式打開的文件,如果以字符方式打開,無(wú)法實(shí)現(xiàn)相對(duì)位置的偏移。

In [1]: fd = open("demo.txt", "rb") # 二進(jìn)制方式打開文件

In [2]: fd.readline()
Out[2]: b'1\n'

In [3]: fd.readline()
Out[3]: b'2\n'

In [4]: fd.tell()
Out[4]: 4

In [5]: fd.seek(-10, 2) # 支持相對(duì)位置偏移量
Out[5]: 282

In [6]: fd.read(10)
Out[6]: b'98\n99\n100\n'

除了這幾個(gè)常用的方法以外,還有一些方法不是很常用,例如detach,isatty,reconfigure等等,如果在實(shí)際中碰到復(fù)雜的文件流問(wèn)題,需要進(jìn)一步查看底層的C語(yǔ)言源碼,或者用C寫擴(kuò)展方法。

總結(jié)一下

本文重點(diǎn)整理了Python操作字符文件和字節(jié)文件的方法,對(duì)于基本的讀寫使用,幾行代碼就能搞定;除此之外建議使用with的上下文語(yǔ)句,這樣避免手動(dòng)進(jìn)行資源管理;后續(xù)進(jìn)一步介紹了文件流對(duì)象,以及對(duì)象的部分方法,重點(diǎn)介紹了文件指針的操作,操作文件指針時(shí)字節(jié)文件和字符文件是存在差別的。碰到復(fù)雜的問(wèn)題,例如需要對(duì)同一個(gè)文件進(jìn)行讀寫操作,會(huì)用到文件指針。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容