Python与文件流

Python读写文件非常简单,本文除了介绍简单的读写字符文件和字节文件以外,还会介绍文件对象的属性方法和文件流的一些操作,文章内容包含以下方面:

  1. 字符文件和字节文件
  2. 读写字符文件
  3. 读写字节文件
  4. 上下文with操作文件
  5. 分析文件对象的源码
  6. 文件流的属性方法

欢迎关注我的微信公众号:“数学编程”和个人博客

字符文件和字节文件

字符文件通常是一些存储字符串的文本文件。在windows记事本创建的txt文件,程序的源代码文件,网页的html文件都是字符文件;字节文件是指存储字节码的文件,也是二进制的文件。比如windows下可执行的exe文件,图片,音频视频文件都是二进制文件。这些文件由0和1构成。

理论上说所有在计算机上面的文件最终存储形式都是0和1的文件,下面是一个图片文件的二进制,通常是转为16进制,就长这样子:

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

根据文件类型通常分为字符文件和字节文件(二进制文件),于是Python中操作这两种文件都有对应的方法,不要用混了。

读写字符文件

读写普通的字符文件非常容易。比如创建一个字符文件,并且往里面写入1-100的数字。每个数字占一行。

filename = "demo.txt"
# 以写入的方式创建文件,如果不存在则直接创建
# 如果存在则会覆盖
fd = open(filename, 'w')

for num in range(1, 101):
    # 写入比如是str的字符
    # 拼接换行符
    fd.write(str(num) + "\n")
# 关闭文件对象
fd.close()

接下来读取这个文件,并且计算出这些数字的总和。

total = 0

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

print(total)

# 5050

读取文件一行代码就能搞定。

读写字节文件

字节文件的读取与字符文件读取基本一样,唯一的区别在于指定读写文件模式,open函数有一个参数mode,字节文件的读取为“rb”,写入为“wb”,其中b的意思是binary。

首先读取图片,然后再把这张图片写入另外的字节文件,有点像复制与粘贴。先来看读取字节码文件,然后打印出来:

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

输出内容是:

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

这个就是unicode编码,Python在处理数据时,在内存中统一的编码都是unicode码。我们把读取的unicode编码写入字节文件,就得到原始的图片了。

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

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

明白了这个原理,用Python在网上下载图片或者视频的时候就能用上了,本质上就是把网上的字节文件,写入本地磁盘。就是这么容易。

上下文with操作文件

with语句是一种上下文资源管理协议,使用with语句格式可以专注于你的操作,而忽视资源的关闭。因为资源打开以后,可能会有问题,导致资源无法回收而占用系统资源。使用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)

省掉了关闭文件的操作,在with的语境下,你的程序出现异常,文件依然能够保证关闭。

分析文件对象的源码

open函数的源码使用C语言实现的,属于builtins方法(内建方法)。我们来看看几个关键参数:

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 不需要多说,文件的名称。mode用很多参数,r和w以及b都已经用过了,a是append的意思,以写的方式打开,追加的文件最后。+号的含义是以更新的方式打开,包括读和写操作。

 ===============================================================
    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 参数表示缓冲区的的策略。分别取值为None,0,1,大于1。None表示默认的策略,数据块大小为8192个字节,每次读写的单位就是这个数据块大小。读写文件首先会读写在一块缓冲区,然后再把内容flush进文件,对于大文件的操作会修改这个参数,例如要给一个大于内存的文件内容进行排序或者搜索,只能分块读入内存中。其余策略可以参考源码。

encoding 编码方式,只能在字符文件中有效,字节文件没有编码方式。不指定这个参数通常会根据系统的来决定,如果读取文件出现乱码,则考虑指定文件的编码方方式,最常用的就是utf-8和gbk两种。

看完了这几个参数我们来看看返回值,就是io模块下的TextIOWrapper类。于是我们打开这个类,顺便打印出属性和方法:

>>> 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)
   # 内容较多省略

如果使用IDE查看源码最方便,打开源码我们发现这个类又是内建模块。我们接下来看看文件流的一些常用方法。

文件流的属性方法

open函数返回的对象就是文件流(Open file and return a stream. Raise OSError upon failure)。上面我们看到返回的是_io.TextIOWrapper这个类。文件流有很多方法,上面已经用到了一些,例如read读取全部内容,write写入内容。接下来我们通过一个例子来说明这些方法。

# 创建文件,写入1-100,并且每个一个数字占一行
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'
# 每次读取两个字符,指针的位置在第13个字符的位置了(从0开始)
In [17]: fd.tell() # 查看指针位置
Out[17]: 12

In [18]: fd.read() # 从当前指针位置读取剩余全部内容
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意味着已经输出了292个字符长度了

很容易计算出来,指针的偏移量为292个字符,已经到了文件的末尾了。既然可以获取到文件的指针位置,那么如何移动文件指针的位置呢?这就用到seek方法了。

In [21]: fd.read()
Out[21]: ''
# 再次read发现指针的位置不会变化了
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个元素
Out[26]: ['1\n', '2\n', '3\n', '4\n', '5\n', '6\n', '7\n', '8\n', '9\n', '10\n']

readlines方法按行读取所有的数据(注意每个元素包含了换行符)。这里补充seek(offset,reference_point),其中offset是偏移量,reference_point相对指针的位置,取值为0表示开始位置,1表示当前位置,2表示结束位置。这种情况只适用于字节方式打开的文件,如果以字符方式打开,无法实现相对位置的偏移

In [1]: fd = open("demo.txt", "rb") # 二进制方式打开文件

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) # 支持相对位置偏移量
Out[5]: 282

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

除了这几个常用的方法以外,还有一些方法不是很常用,例如detach,isatty,reconfigure等等,如果在实际中碰到复杂的文件流问题,需要进一步查看底层的C语言源码,或者用C写扩展方法。

总结一下

本文重点整理了Python操作字符文件和字节文件的方法,对于基本的读写使用,几行代码就能搞定;除此之外建议使用with的上下文语句,这样避免手动进行资源管理;后续进一步介绍了文件流对象,以及对象的部分方法,重点介绍了文件指针的操作,操作文件指针时字节文件和字符文件是存在差别的。碰到复杂的问题,例如需要对同一个文件进行读写操作,会用到文件指针。

你可能感兴趣的:(Python与文件流)