# 文件读写

# open()

  • 用户读写一个文件时,基本流程如下:

    1. 调用 open() 函数来打开文件,它会返回一个文件对象。
    2. 调用文件对象的方法,比如 read() 读取数据、 write() 写入数据。
    3. 调用文件对象的 close() 方法来关闭文件。
  • open() 是 Python 的内置函数,用于打开磁盘中的一个文件。形参如下:

    open(
          file,               # 指定要打开的文件路径,或者一个 int 型的文件描述符
          mode      = 'r',    # 打开文件的模式
          buffering = -1,     # 缓冲策略
          encoding  = None,   # 编码格式,仅在文本模式可用
          errors    = None,   # 处理编码、解码错误的策略,仅在文本模式可用。取值为 None 时相当于 'strict' ,会抛出异常
          newline   = None,   # 换行符,仅在文本模式可用
          closefd   = True,   # 输入文件描述符打开文件时,调用 close() 方法是否关闭该文件描述符
          ...)
    
  • 进程每打开一个文件,会被操作系统自动分配一个文件描述符,取值为 int 类型,相当于文件编号。

    • 在 Windows 系统中,如果一个文件已经被某个进程打开了,则不允许该文件被其它进程修改,直到前一个进程释放文件描述符。

# 形参

# mode

  • open() 函数中,mode 参数表示对文件的访问模式,有以下几种取值:

    • r
      • :read ,只读模式。
      • 如果文件不存在,则报错:FileNotFoundError: No such file or directory
    • w
      • :write ,只写模式。
      • 如果文件不存在,则自动创建它,再进行写入。
      • 如果文件已存在,则将长度截断为 0 ,再进行写入。这样属于覆盖式写入。
    • a
      • :append ,追加写模式。
      • 如果文件不存在,则自动创建它,再进行写入。
      • 如果文件已存在,则到文件尾部进行追加写入。
    • x
      • :create ,先创建文件,再以只写模式打开。
      • 如果文件已存在,则报错:FileExistsError: File exists
    • +
      • :可读可写模式。
      • 必须与 r、w、a、x 模式其中之一组合使用,例如 r+w+
      • r+ 模式支持覆盖式写入,比如通过 seek() 和 write() 修改文件中的部分内容。
  • mode 参数中,还可以指定文件流的类型,有两种取值:

    • b
      • :binary ,二进制模式,读写的值是 bytes 类型。
      • 此时文件对象绑定的 IO 对象为二进制流,又称为字节流。
      • 所有类型的文件都是以二进制格式存储的,因此可以按二进制格式直接读写。不过通常将二进制值转换成字节,便于阅读。
    • t
      • :text ,文本模式,读写的值是 str 类型。
      • 此时文件对象绑定的 IO 对象为文本流,又称为字符流。
      • 与二进制模式相比,文本模式更方便人类阅读,但只适合打开文本格式的文件,不适合打开图片、音频等特殊编码的文件。
        • 读取文件时,需要先读取文件中存储的二进制数据,再按某种编码格式转换成 str 类型。
        • 写入文件时,需要先将写入的 str 类型的值,按某种编码格式转换成 bytes 类型,再写入文件。
  • 用户调用 open() 函数时,mode 参数的默认值为 rt

    • 可以组合使用多种模式,例如 rtrbrb+
    • t、b 两种模式,只能二选一。

# buffering

  • open() 读写文件时,默认启用了缓冲,但没有启用缓存。

    • 缓冲(buffer)

      • 原理:
        • 调用 f.write() 写入一段数据到磁盘文件时,该方法会立即执行完毕,但数据不一定立即写入磁盘文件。
        • 这些数据会先放在内存中的缓冲区,等累积了一定数量 bytes 之后,才写入磁盘。
      • 优点:
        • f.write() 方法能立即执行完毕,不必等待磁盘 IO 完成。
        • 减少了磁盘 IO 的次数。否则每写入 1 byte 就进行一次磁盘 IO ,开销大,效率低。
      • 缺点:
        • 如果进程突然挂掉,缓冲区中的数据,可能来不及保存到磁盘,从而丢失。
      • 例:
        • 调用 f.flush() 方法,会将刷新缓冲区,将其中等待写入磁盘的数据,立即写入磁盘。
        • 调用 f.close() 方法时,会自动调用 f.flush() 方法。
    • 缓存(cache)

      • 原理:
        • 调用 f.read() 从磁盘文件读取一段数据到内存时,即使处理完这段数据,这段数据也会在内存中缓存一段时间,方便用户未来再次读取。
      • 优点:
        • 方便重复读取数据。
      • 缺点:
        • 会增加 Python 程序占用的内存空间。
      • 例:
        • 可以执行 io.BytesIO() ,在内存中创建一个缓存区。
  • open() 函数中,buffering 参数表示缓冲策略,可取值:

    • 0 :取消缓冲,仅在二进制模式可用。
    • 1 :采用行缓冲,仅在文本模式可用。
    • n > 0 :采用 n 字节大小的缓冲区。
    • 默认值为 -1 ,表示自动选择缓冲策略,
      • 如果文件为 tty 类型,则采用行缓冲。
      • 如果文件为其它类型,则采用系统默认大小的缓冲区。这取决于 io.DEFAULT_BUFFER_SIZE ,通常为 8 KB 。

# encoding

  • open() 函数中, encoding 参数表示文件内容的编码格式。

    • 如果不指定 encoding 参数,则 Python 会采用当前计算机的默认编码格式。
      • 可执行 locale.getpreferredencoding(False) ,查看当前的编码格式。
      • 在 Linux 系统上,它通常是 'utf-8' 。
      • 在 Windows 中文版系统上,它通常是 'cp936' 。
  • 同一个计算机中,

    • 不同文件的内容,可能采用不同的编码格式,例如 'utf-8'、'gbk' 。
      • 调用 open() 打开一个文件时,用户需要指定正确的 encoding 参数,否则不能解读文件的内容。
    • 不同文件的文件名,通常统一采用 'utf-8' 编码格式。
      • 可执行 sys.getfilesystemencoding() ,查看当前的编码格式。
      • 调用 open() 打开一个文件时,会自动处理文件名的编码格式,不需要用户干预。

# newline

  • open() 函数中,newline 参数表示换行符,用于自动划分每行文本。

    • 例如类 Unix 系统的 newline 默认是 \n ,Windows 系统的 newline 默认是 \r\n
  • newline 参数可取值:

    • 默认为 newline=None
      • 读取文件时,会将文件中的 '\r''\n''\r\n' 都识别为换行符,并且统一转换成 '\n' ,然后返回。
      • 写入文件时,会将待写文本中的 '\n' 都替换成当前平台的默认换行符(取决于 os.linesep ),再写入文件。
    • 如果指定了 newline=''
      • 读取文件时,会将文件中的 '\r''\n''\r\n' 都识别为换行符,然后返回该行文本。
      • 写入文件时,会直接写入。
    • 如果指定了 newline 等于其它值:
      • 读取文件时,只会识别指定的换行符,然后返回该行文本。
      • 写入文件时,会将待写文本中的 '\n' 都替换成指定的换行符,再写入文件。

# 属性

  • 文件对象的常用属性:

    >>> f.name        # 文件名
    '1.txt'
    >>> f.mode        # 打开文件的模式
    'w'
    >>> f.fileno()    # 文件描述符
    3
    
  • 以文本模式打开文件时,可访问以下属性:

    >>> f.encoding
    'cp936'
    >>> f.errors
    'strict'
    >>> f.line_buffering
    False
    >>> f.newlines    # 这里 newlines 为 None
    >>> f.buffer      # 每个文件会绑定一个 BinaryIO 对象,用于存储文件的二进制内容
    <_io.BufferedWriter name='1.txt'>
    >>> f.buffer.mode # 以文本模式打开文件时,实际上是先用二进制模式打开文件,再转换成文本模式
    'wb'
    

# 方法

  • 文件对象的常用方法:

    .close()  -> None # 关闭文件
    .closed() -> bool # 判断文件是否已关闭
    
    .fileno() -> int  # 返回文件描述符
    .flush()  -> None # 刷新缓冲区
    .isatty() -> bool # 判断 IO 对象的输入、输出是否指向终端设备
    
    .readable() -> bool                          # 判断是否可读
    .read(n: int = -1) -> AnyStr                 # 读取 n 个字节的数据,然后返回。默认 n 为 -1 ,即无限制
    .readline(limit: int = -1) -> AnyStr         # 读取一行,最多读取 limit 个字节,然后返回(包括换行符)。默认 limit 为 -1 ,即无限制
    .readlines(hint: int = -1) -> List[AnyStr]   # 读取文件,最多读取 hint 行,然后返回一个包含各行的列表(包括换行符)。默认 hint 为 -1 ,无 f.限制
    
    .writable() -> bool                          # 判断是否可写
    .write(s: AnyStr) -> int                     # 写入 s 的值,然后返回写入的字节数
    .writelines(lines: Sequence[AnyStr]) -> None # 写入多行,需要输入一个包含各行的序列(包括换行符),例如 f.writelines(['Hello\n','world\n'])
    
    • 上述的 AnyStr 类型是指 str 或 bytes 等类型。
      • 以文本模式打开文件时,读写的数据必须为 Str 类型。
      • 以二进制模式打开文件时,读写的数据必须为 bytes 或 bytearray 类型。
    • 读取大文件时,直接调用 read() 会占用太多内存,应该改用 read(1000) 等方式。
  • 读写文件的示例:

    >>> f = open('1.txt', 'w')
    >>> type(f)
    <class '_io.TextIOWrapper'>       # 文件对象是对 IO 对象的封装
    >>> f.read()                      # 读取文件,这里因为不能读取而报错
    io.UnsupportedOperation: not readable
    >>> f.write('Hello')              # 写入文件
    5
    >>> f.close()                     # 关闭文件
    
  • 读取文件的示例:

    >>> f = open('1.txt', 'w+')
    >>> f.write('Hello\n')
    6
    >>> f.seek(0)       # 将文件指针移到文件首部
    0
    >>> f.read()        # 读取文件的全部内容
    'Hello\n'
    >>> f.seek(0)
    0
    >>> f.readline()    # 读取一行
    'Hello\n'
    >>> f.seek(0)
    0
    >>> f.readlines()   # 读取全部行
    ['Hello\n']
    >>> f.seek(0)
    0
    >>> [line for line in f]  # 文件对象支持遍历,每次遍历一行,相当于 f.readline()
    ['Hello\n']
    >>> f.close()
    

# close()

  • 用户操作完文件之后,应该调用 close() 方法来关闭文件。
    • 关闭文件时,会释放其缓冲区,避免内存泄漏。还会释放文件描述符。
    • 文件被关闭之后,不能再进行读写,否则会报错:ValueError: I/O operation on closed file
  • 可用关键字 with 打开文件,进行上下文管理。
    • 例:
      with open('1.txt') as f:
          f.read()
      
    • 当 Python 解释器执行完 with 语句块或者抛出异常时,会自动调用 close() 方法完成清理。因此上例相当于:
      try:
          f = open('1.txt')
          f.read()
      finally:
          f.close()
      
    • 关键字 with 支持同时打开多个上下文:
      with open('1.txt') as f1, open('2.txt') as f2:
          pass
      

# seek()

  • 读写文件时,Python 会通过文件指针,记录当前读写到文件中第几个字节。
    • 使用 r、w 模式打开文件时,指针最初指向第 0 个字节。
      • 每次读、写 n 个字节,Python 就会自动将指针的位置向后移动 n 个字节。
    • 使用 a 模式打开文件时,每次读、写操作之前,都会自动将指针指向最后一个字节。
      • 可以调用 f.seek() ,但每次读、写操作之前依然会将指针指向最后一个字节。
  • 相关方法:
    .tell() -> int                              # 返回文件指针当前的位置
    .seekable() -> bool                         # 判断是否可以移动文件指针的位置,取决于 IO 对象是否支持随机位置访问
    .seek(offset: int, whence: int = 0) -> int  # 改变文件指针的位置,然后返回文件指针当前的位置
    
    • offset :表示指针从 wherece 开始的偏移量,即第几个字节。可以为负。
    • whence :可选参数,表示移动指针的起始位置。可取值:
      • 0 :文件首部,即第 0 个字节处。
      • 1 :指针当前位置,即 f.tell() 的值。
      • 2 :文件尾部,最最后一个字节处。
    • 例:
      >>> f.read()
      'Hello'
      >>> f.read()    # 当文件指针指向文件尾部时,如果继续读取,则返回的内容为空
      ''
      >>> f.tell()
      5
      >>> f.seek(0)
      0
      >>> f.read()
      'Hello'
      

# truncate()

  • 用 truncate() 方法可以截断文件:
    .truncate(size: int = None) -> int   # 将文件大小调整为 size ,然后返回调整后的大小
    
    • 截断文件时,需要允许写入,即 f.writeable() == True 。
    • 如果不指定 size ,则会采用文件指针当前的位置,即 f.tell() 的值。
      • 如果指定的 size 比文件长度小,则会截断文件,删除文件尾部多余的字节。
      • 如果指定的 size 比文件长度大,则会扩展文件,在文件尾部填充一些 Null 字节。
    • 例:
      >>> f = open('1.txt', 'w+')
      >>> f.write('Hello')
      5
      >>> f.truncate(0)
      0
      >>> f.tell()          # 调用 truncate() 方法时,不会自动移动文件指针
      5
      >>> f.write('Hello')  # 此时文件长度已经截断为 0 ,但指针依然指向第 5 个字节,如果继续写入则会在前面填上 4 个 Null 字节,产生稀疏文件
      5
      >>> f.seek(0)
      0
      >>> f.read()
      '\x00\x00\x00\x00\x00Hello'
      >>> f.close()
      
    • 例:清空文件之后重新写入
      with open('1.txt', 'w+') as f:
          f.read()
          f.seek(0)
          f.truncate()
          f.write('Hello')
      

# import io

:Python 的标准库,用于进行 IO 操作。

# 原理

  • 进行 IO 操作的对象,称为 IO 对象、流对象。
  • io 库中,常用的几个类:
    io.BytesIO    # 用作字节流(又称为二进制流),只能写入 bytes 类型的值
    io.StringIO   # 用作字符流,只能写入 str 类型的值
    
    • 用 open() 函数打开文件时,会返回一个文件对象,实际上是对 io.BytesIOio.StringIO 的封装。因此它们的操作方法,基本一致。

# 用法

  • 用法示例:

    >>> import io
    >>> s = io.StringIO('Hello\n')  # 创建 IO 对象,并初始化其内容
    >>> s.write('World')            # 创建 IO 对象之后,指针最初指向第 0 个字节处,此时写入可能覆盖原有数据
    5
    >>> s.read()                    # 当前指针倒数第二个字节处,因此只会读取到 '\n'
    '\n'
    >>> s.seek(0)                   # 将指针指向 IO 对象的首部
    0
    >>> s.read()
    'Hello\n'
    >>> s.getvalue()                # 获取 IO 对象的全部内容,该方法不受指针位置的影响
    'Hello\n'
    >>> s.truncate(3)               # 截断 IO 对象
    3
    >>> s.getvalue()
    'Wor'
    >>> s.close()
    
  • 操作完 IO 对象之后,应该调用 close() 方法来关闭它。或者通过 with 关键字创建 IO 对象,会自动关闭它。

    with io.StringIO() as s:
        ...
    
  • 例:读取磁盘文件的内容,缓存到 BytesIO 中,也就是手动创建一个缓存区

    with io.BytesIO() as buf:
        with open('1.jpg', 'rb') as f:
            buf.write(f.read())
        ...