Python讀寫文件非常簡(jiǎn)單,本文除了介紹簡(jiǎn)單的讀寫字符文件和字節(jié)文件以外,還會(huì)介紹文件對(duì)象的屬性方法和文件流的一些操作,文章內(nèi)容包含以下方面:
- 字符文件和字節(jié)文件
- 讀寫字符文件
- 讀寫字節(jié)文件
- 上下文
with操作文件 - 分析文件對(duì)象的源碼
- 文件流的屬性方法
歡迎關(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),指針的偏移量為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ì)用到文件指針。