MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python对二进制文件的读写操作

2024-01-163.2k 阅读

二进制文件基础概念

在深入探讨Python对二进制文件的读写操作之前,我们先来了解一下什么是二进制文件。计算机中的所有数据最终都是以二进制形式存储的,即由0和1组成的序列。二进制文件直接存储二进制数据,与文本文件不同,文本文件以字符编码(如ASCII、UTF - 8等)的形式存储文本信息,并且每行通常以换行符(\n)作为结束标志。

二进制文件用于存储非文本数据,例如图像、音频、视频、可执行程序等。这些文件中的数据没有明显的文本结构,其内容按照特定的格式组织,由文件格式规范定义。例如,JPEG图像文件遵循JPEG格式规范,其中包含图像的像素数据、颜色信息、压缩格式等内容。

二进制文件的数据存储和读取与文本文件有很大差异。文本文件可以按字符、行等方式读取,而二进制文件需要按照其内部的格式结构来处理数据。这就要求在读取和写入二进制文件时,我们必须精确地知道文件的格式以及如何解释其中的数据。

Python中的二进制文件读写模式

在Python中,使用内置的open()函数来打开文件,通过指定不同的模式来决定文件是用于读、写还是其他操作。对于二进制文件,主要有以下几种模式:

读取二进制文件模式('rb')

当以'rb'模式打开文件时,表示以二进制只读方式打开文件。这种模式适用于从二进制文件中读取数据,例如读取图像文件、音频文件等。示例代码如下:

try:
    with open('example.bin', 'rb') as file:
        data = file.read()
        print(type(data))
except FileNotFoundError:
    print("文件未找到")

在上述代码中,使用with语句打开名为example.bin的文件,with语句会在代码块结束时自动关闭文件。file.read()方法将整个文件内容读取为一个字节对象(bytes类型)。print(type(data))会输出<class 'bytes'>,表明读取的数据类型是字节类型。

写入二进制文件模式('wb')

'wb'模式用于以二进制写入方式打开文件。如果文件不存在,会创建一个新文件;如果文件已存在,会覆盖原有内容。这种模式常用于将二进制数据写入文件,比如保存图像的修改结果、创建新的可执行文件等。示例代码如下:

data_to_write = b'\x48\x65\x6c\x6c\x6f'  # 字节对象
try:
    with open('new_example.bin', 'wb') as file:
        file.write(data_to_write)
except Exception as e:
    print(f"写入文件时出错: {e}")

在这段代码中,定义了一个字节对象data_to_write,其内容为b'Hello'的字节表示(\x48是'H'的十六进制字节表示,以此类推)。使用'wb'模式打开new_example.bin文件,并将字节对象写入文件。

追加二进制文件模式('ab')

'ab'模式是以二进制追加方式打开文件。如果文件不存在,会创建新文件;如果文件已存在,则在文件末尾追加数据,而不会覆盖原有内容。这在需要不断向二进制文件添加新数据的场景中很有用,例如日志文件的记录。示例代码如下:

new_data = b'\x20\x57\x6f\x72\x6c\x64'  # 字节对象表示" World"
try:
    with open('new_example.bin', 'ab') as file:
        file.write(new_data)
except Exception as e:
    print(f"追加文件时出错: {e}")

上述代码打开之前创建的new_example.bin文件,以追加模式写入新的字节数据b' World',最终文件内容为b'Hello World'

读取二进制文件的方法

读取整个文件内容

如前面示例所示,使用file.read()方法可以一次性读取整个二进制文件的内容。这种方法简单直接,适用于文件较小的情况。因为它会将整个文件加载到内存中,如果文件过大,可能会导致内存不足的问题。例如:

try:
    with open('small_image.jpg', 'rb') as file:
        image_data = file.read()
        print(f"图像文件大小: {len(image_data)} 字节")
except FileNotFoundError:
    print("图像文件未找到")

此代码读取一个名为small_image.jpg的图像文件,并打印出文件的字节大小。

按指定字节数读取

file.read(size)方法可以按指定的字节数size读取文件内容。每次调用该方法,它会从文件当前位置读取指定字节数的数据,并返回读取到的字节对象。如果到达文件末尾,返回空字节对象b''。这种方式适合逐块读取大文件,避免一次性加载过多数据到内存。示例代码如下:

try:
    with open('large_file.bin', 'rb') as file:
        chunk_size = 1024  # 每次读取1024字节(1KB)
        while True:
            chunk = file.read(chunk_size)
            if not chunk:
                break
            print(f"读取到 {len(chunk)} 字节数据")
            # 这里可以对读取到的chunk进行处理
except FileNotFoundError:
    print("大文件未找到")

在上述代码中,以1KB为单位逐块读取large_file.bin文件,每次读取后可以对chunk进行相应处理,直到读取完整个文件(即chunk为空)。

读取一行数据(针对特定二进制文件格式)

虽然二进制文件通常没有像文本文件那样明确的“行”概念,但对于某些特定格式的二进制文件,可能会定义类似行的结构。例如,一些二进制日志文件可能每行记录一条日志,并且以特定的字节序列(如b'\n')作为行结束标志。对于这种情况,可以使用file.readline()方法。示例代码如下:

try:
    with open('binary_log.bin', 'rb') as file:
        while True:
            line = file.readline()
            if not line:
                break
            print(f"读取到一行数据: {line}")
except FileNotFoundError:
    print("二进制日志文件未找到")

此代码逐行读取binary_log.bin文件,直到文件末尾。需要注意的是,这里的“行”是根据文件内部格式定义的,与文本文件中的行概念不同。

写入二进制文件的方法

写入字节对象

使用file.write()方法可以将字节对象写入二进制文件。如前面写入示例所示,file.write()方法接受一个字节对象作为参数,并将其写入文件当前位置。例如:

new_bytes = b'\x43\x6f\x6e\x74\x65\x6e\x74'  # 字节对象表示"Content"
try:
    with open('output_file.bin', 'wb') as file:
        file.write(new_bytes)
except Exception as e:
    print(f"写入文件时出错: {e}")

这段代码将字节对象b'Content'写入名为output_file.bin的文件。

写入多个字节对象

有时候需要将多个字节对象依次写入文件。可以多次调用file.write()方法,也可以将多个字节对象拼接后再写入。例如:

part1 = b'\x48\x65\x6c'
part2 = b'\x6c\x6f'
try:
    with open('combined_file.bin', 'wb') as file:
        file.write(part1)
        file.write(part2)
except Exception as e:
    print(f"写入文件时出错: {e}")

上述代码分两次将part1part2写入combined_file.bin文件,最终文件内容为b'Hello'。也可以将两个字节对象拼接后写入:

part1 = b'\x48\x65\x6c'
part2 = b'\x6c\x6f'
combined = part1 + part2
try:
    with open('combined_file.bin', 'wb') as file:
        file.write(combined)
except Exception as e:
    print(f"写入文件时出错: {e}")

这两种方式效果相同,但在处理复杂数据结构时,选择合适的方式可以提高代码的可读性和效率。

二进制文件的定位操作

在读写二进制文件时,有时需要精确控制文件的读取或写入位置,这就涉及到文件的定位操作。Python的文件对象提供了seek()tell()方法来实现这些功能。

seek()方法

file.seek(offset, whence)方法用于将文件指针移动到指定位置。其中,offset表示偏移量,whence表示参考位置。whence有三个可选值:

  • 0:表示从文件开头开始计算偏移量,这是默认值。例如,file.seek(10, 0)会将文件指针移动到文件开头后第10个字节的位置。
  • 1:表示从当前位置开始计算偏移量。例如,file.seek(5, 1)会将文件指针从当前位置向后移动5个字节。
  • 2:表示从文件末尾开始计算偏移量。例如,file.seek(-20, 2)会将文件指针移动到文件末尾前20个字节的位置。

示例代码如下:

try:
    with open('example.bin', 'rb+') as file:
        file.seek(5, 0)  # 从文件开头移动到第5个字节位置
        data = file.read(10)  # 从当前位置读取10个字节
        print(f"读取到的数据: {data}")
        file.seek(-15, 2)  # 从文件末尾移动到前15个字节位置
        new_data = b'\x4e\x65\x77'  # 字节对象表示"New"
        file.write(new_data)  # 在当前位置写入新数据
except FileNotFoundError:
    print("文件未找到")

在上述代码中,先将文件指针从开头移动到第5个字节位置,读取10个字节数据。然后将文件指针从文件末尾移动到前15个字节位置,并写入新的字节数据。

tell()方法

file.tell()方法用于返回文件指针当前所在的位置,即从文件开头到当前位置的字节数。这在需要记录当前读取或写入位置时非常有用。例如:

try:
    with open('example.bin', 'rb') as file:
        position = file.tell()
        print(f"文件指针初始位置: {position}")
        file.read(10)
        new_position = file.tell()
        print(f"读取10个字节后文件指针位置: {new_position}")
except FileNotFoundError:
    print("文件未找到")

此代码先获取文件指针的初始位置,然后读取10个字节数据,再次获取文件指针位置,从而可以了解读取操作对文件指针位置的影响。

处理二进制文件中的结构体数据

在很多二进制文件格式中,数据是以结构体的形式组织的。结构体是一种自定义的数据类型,由多个不同类型的数据成员组成。例如,在一个简单的图像文件格式中,可能有一个结构体用于存储图像的宽度、高度和颜色模式等信息。

在Python中,可以使用struct模块来处理二进制结构体数据。struct模块提供了一些函数,用于将Python数据类型打包成二进制数据(按照指定的格式),以及将二进制数据解包成Python数据类型。

打包数据

struct.pack(format, v1, v2,...)函数用于将Python数据按照指定的格式format打包成字节对象。format是一个格式字符串,其中包含了每个数据成员的类型和字节顺序等信息。常见的格式字符有:

  • 'B':无符号字符(1字节)
  • 'H':无符号短整型(2字节)
  • 'I':无符号整型(4字节)
  • 'f':单精度浮点数(4字节)
  • 'd':双精度浮点数(8字节)

字节顺序可以通过在格式字符串开头添加字符来指定,例如:

  • '<':小端字节序(默认)
  • '>':大端字节序

示例代码如下:

import struct

width = 800
height = 600
color_mode = 2  # 假设2表示RGB模式

packed_data = struct.pack('<HHi', width, height, color_mode)
try:
    with open('image_info.bin', 'wb') as file:
        file.write(packed_data)
except Exception as e:
    print(f"写入文件时出错: {e}")

在上述代码中,使用struct.pack()函数将图像的宽度、高度和颜色模式打包成字节对象,格式字符串'<HHi'表示小端字节序,依次为两个无符号短整型(用于宽度和高度)和一个有符号整型(用于颜色模式)。然后将打包后的数据写入image_info.bin文件。

解包数据

struct.unpack(format, data)函数用于将字节对象按照指定的格式format解包成Python数据。例如,要解包前面打包的数据:

import struct

try:
    with open('image_info.bin', 'rb') as file:
        data = file.read()
        unpacked_data = struct.unpack('<HHi', data)
        width, height, color_mode = unpacked_data
        print(f"图像宽度: {width}")
        print(f"图像高度: {height}")
        print(f"颜色模式: {color_mode}")
except FileNotFoundError:
    print("文件未找到")

此代码从image_info.bin文件读取数据,并使用struct.unpack()函数按照相同的格式'<HHi'解包数据,然后打印出图像的宽度、高度和颜色模式。

处理二进制文件的常见问题及解决方法

文件不存在错误

在尝试打开二进制文件进行读写操作时,如果文件不存在,会抛出FileNotFoundError异常。如前面的示例代码,使用try - except语句捕获该异常,并进行相应处理,例如提示用户文件未找到。

try:
    with open('non_existent_file.bin', 'rb') as file:
        data = file.read()
except FileNotFoundError:
    print("文件未找到")

权限问题

如果没有足够的权限来打开或操作二进制文件,会抛出PermissionError异常。这通常发生在尝试写入受保护的系统文件,或者当前用户没有对文件所在目录的写入权限等情况。解决方法是确保当前用户具有相应的权限,例如在Linux系统中,可以使用sudo命令提升权限,但要谨慎使用,因为不当操作可能会导致系统问题。

try:
    with open('/system_protected_file.bin', 'wb') as file:
        file.write(b'data')
except PermissionError:
    print("没有权限写入该文件")

数据格式不匹配问题

在处理二进制结构体数据时,特别是在解包数据时,如果提供的格式字符串与实际数据格式不匹配,会抛出struct.error异常。例如,尝试将一个4字节的数据按照2字节的格式解包。要解决这个问题,需要确保在打包和解包数据时使用相同的格式字符串,并且要准确了解二进制文件中数据的实际格式。

import struct

try:
    data = b'\x01\x02\x03\x04'
    unpacked_data = struct.unpack('<H', data)  # 格式不匹配,应该用'I'解包4字节数据
except struct.error as e:
    print(f"解包数据时出错: {e}")

大文件处理问题

对于大的二进制文件,一次性读取整个文件可能会导致内存不足。如前面提到的,可以使用按块读取的方式,逐块处理数据。另外,在写入大文件时,也建议分块写入,以避免占用过多内存。例如,在写入大文件时,可以使用以下方式:

chunk_size = 1024 * 1024  # 1MB
try:
    with open('large_output_file.bin', 'wb') as file:
        for i in range(10):  # 模拟生成10MB数据
            chunk = b'\x00' * chunk_size
            file.write(chunk)
except Exception as e:
    print(f"写入大文件时出错: {e}")

此代码以1MB为单位逐块写入数据,从而有效地处理大文件写入操作。

通过以上对Python二进制文件读写操作的详细介绍,包括文件模式、读写方法、定位操作、结构体数据处理以及常见问题解决等方面,希望读者能够全面掌握Python在二进制文件处理方面的技能,在实际编程中能够灵活、高效地处理各种二进制文件。