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

Python二进制文件读写

2022-12-012.8k 阅读

Python二进制文件的基础概念

在Python中,处理二进制文件与处理文本文件有着显著的区别。二进制文件以字节序列的形式存储数据,这些数据没有像文本文件那样的字符编码和行分隔等概念。常见的二进制文件类型包括图像文件(如JPEG、PNG)、音频文件(如MP3、WAV)、视频文件(如MP4、AVI)以及可执行文件(.exe等)。

与文本文件不同,二进制文件的内容不能直接被人类读取和理解,因为它们是为特定的应用程序或硬件设备设计的。例如,一个图像文件包含了描述图像像素颜色和布局的二进制数据,只有图像查看器程序才能正确解析这些数据并显示出图像。

Python的文件操作模式

在Python中,使用内置的open()函数来打开文件,而操作二进制文件时,需要使用特定的模式。

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

当以'rb'模式打开文件时,Python将文件视为二进制数据进行读取。例如:

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

在上述代码中,open('example.bin', 'rb')打开名为example.bin的文件,以二进制读取模式。with语句确保文件在使用完毕后自动关闭。file.read()读取整个文件内容,并返回一个bytes对象。print(type(data))将输出<class 'bytes'>,表明读取的数据类型是字节类型。

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

'wb'模式打开文件用于将二进制数据写入文件。如果文件已存在,将覆盖原有内容;如果文件不存在,将创建一个新文件。示例代码如下:

data = b'\x48\x65\x6c\x6c\x6f\x2c\x20\x57\x6f\x72\x6c\x64'
try:
    with open('new_example.bin', 'wb') as file:
        file.write(data)
except Exception as e:
    print(f"写入文件时出错: {e}")

这里b'\x48\x65\x6c\x6c\x6f\x2c\x20\x57\x6f\x72\x6c\x64'是一个字节串,它表示字符串Hello, World的ASCII编码。open('new_example.bin', 'wb')以二进制写入模式打开new_example.bin文件,file.write(data)将字节串数据写入文件。

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

'ab'模式用于在二进制文件末尾追加数据。如果文件不存在,会创建新文件。例如:

new_data = b'\x21'
try:
    with open('new_example.bin', 'ab') as file:
        file.write(new_data)
except Exception as e:
    print(f"追加文件时出错: {e}")

这段代码以追加模式打开new_example.bin文件,并向文件末尾写入一个字节b'\x21',即字符!的ASCII编码。

读取二进制文件的常用方法

read()方法

read()方法是读取二进制文件最常用的方法之一,它会读取整个文件内容并返回一个bytes对象。如前文示例:

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

该方法会一次性将文件内容读入内存,如果文件非常大,可能会导致内存占用过高。

read(size)方法

read(size)方法可以指定读取的字节数。例如:

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

这里file.read(10)会从文件中读取10个字节的数据并返回。这种方式适用于需要逐步处理大文件的情况,可以有效控制内存使用。

readline()方法

虽然readline()方法在处理文本文件时用于读取一行内容,但在二进制文件中,它会读取直到遇到\n字节(如果有的话)或文件末尾的内容。示例如下:

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

需要注意的是,在二进制文件中使用readline()可能并不总是如预期,因为二进制文件不一定有类似文本文件的行结构。

readlines()方法

readlines()方法会读取文件的所有行,并以列表形式返回,每个元素是一个bytes对象。但同样,在二进制文件中,其行为取决于文件中是否存在类似行分隔符(如\n)的字节序列。

try:
    with open('example.bin', 'rb') as file:
        lines = file.readlines()
        for line in lines:
            print(line)
except FileNotFoundError:
    print("文件未找到")

写入二进制文件的常用方法

write()方法

write()方法是写入二进制文件的主要方法,用于将bytes对象写入文件。如前面写入示例:

data = b'\x48\x65\x6c\x6c\x6f\x2c\x20\x57\x6f\x72\x6c\x64'
try:
    with open('new_example.bin', 'wb') as file:
        file.write(data)
except Exception as e:
    print(f"写入文件时出错: {e}")

file.write(data)将字节串data写入文件。

writelines()方法

writelines()方法接受一个可迭代对象(如列表),其中的元素必须是bytes对象,并将这些字节串依次写入文件。例如:

lines = [b'Line 1\n', b'Line 2\n', b'Line 3\n']
try:
    with open('lines_example.bin', 'wb') as file:
        file.writelines(lines)
except Exception as e:
    print(f"写入文件时出错: {e}")

这里file.writelines(lines)将列表lines中的字节串逐行写入lines_example.bin文件。

二进制文件的定位操作

在处理二进制文件时,有时需要在文件中进行定位操作,以读取或写入特定位置的数据。

seek()方法

seek()方法用于移动文件指针到指定位置。它接受两个参数:偏移量(offset)和 whence。whence的值可以是0(表示从文件开头开始计算偏移量)、1(表示从当前位置开始计算偏移量)、2(表示从文件末尾开始计算偏移量,偏移量应为负数)。

从文件开头定位示例:

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

上述代码中file.seek(5)将文件指针从文件开头移动到第5个字节的位置,然后file.read(3)从该位置读取3个字节的数据。

从当前位置定位示例:

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

这里先读取3个字节,然后file.seek(2, 1)从当前位置(即第3个字节之后)再移动2个字节,接着读取2个字节的数据。

从文件末尾定位示例:

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

file.seek(-5, 2)从文件末尾向前移动5个字节,然后读取5个字节的数据。

tell()方法

tell()方法用于获取当前文件指针的位置。例如:

try:
    with open('example.bin', 'rb') as file:
        file.read(3)
        position = file.tell()
        print(f"当前文件指针位置: {position}")
except FileNotFoundError:
    print("文件未找到")

此代码先读取3个字节,然后使用file.tell()获取当前文件指针的位置并打印。

处理二进制数据结构

在实际应用中,经常需要处理特定格式的二进制数据结构,如结构体。Python提供了struct模块来处理C语言风格的结构体。

struct.pack()方法

struct.pack()方法用于将Python数据类型按照指定的格式打包成字节串。例如,将一个整数和一个字符串打包:

import struct

num = 42
text = 'Hello'
packed_data = struct.pack('i5s', num, text.encode('utf-8'))
print(packed_data)

这里'i5s'是格式字符串,i表示整数,5s表示长度为5的字符串。struct.pack()将整数num和字符串text(先编码为字节串)按照指定格式打包成一个字节串。

struct.unpack()方法

struct.unpack()方法用于将字节串按照指定格式解包成Python数据类型。例如,解包上面打包的数据:

import struct

packed_data = b'\x2a\x00\x00\x00Hello'
unpacked_data = struct.unpack('i5s', packed_data)
print(unpacked_data)

struct.unpack('i5s', packed_data)将字节串packed_data按照'i5s'的格式解包,返回一个包含解包后数据的元组。

二进制文件处理的实际应用

图像文件处理

以处理PNG图像文件为例,虽然Python有专门的库(如Pillow)来处理图像,但了解底层二进制处理有助于理解图像存储原理。

PNG文件以特定的文件头开始,包含了文件类型和版本信息等。通过读取文件头的前几个字节,可以验证文件是否为PNG格式。

try:
    with open('example.png', 'rb') as file:
        header = file.read(8)
        if header == b'\x89\x50\x4e\x47\x0d\x0a\x1a\x0a':
            print("这是一个PNG文件")
        else:
            print("这不是一个PNG文件")
except FileNotFoundError:
    print("文件未找到")

这段代码读取example.png文件的前8个字节,并与PNG文件头的标准字节序列进行比较,以判断文件类型。

音频文件处理

对于音频文件,如WAV文件,其文件结构包含文件头和音频数据部分。通过二进制读取可以获取音频的采样率、声道数等信息。

import struct

try:
    with open('example.wav', 'rb') as file:
        file.seek(20)
        format_chunk_size = struct.unpack('<i', file.read(4))[0]
        audio_format = struct.unpack('<h', file.read(2))[0]
        num_channels = struct.unpack('<h', file.read(2))[0]
        sample_rate = struct.unpack('<i', file.read(4))[0]

        print(f"音频格式: {audio_format}")
        print(f"声道数: {num_channels}")
        print(f"采样率: {sample_rate}")
except FileNotFoundError:
    print("文件未找到")

这段代码通过定位和二进制解包操作,从WAV文件中提取音频格式、声道数和采样率等信息。

二进制文件处理中的错误处理

在处理二进制文件时,可能会遇到各种错误,如文件不存在、权限不足、文件损坏等。

文件不存在错误

当尝试打开一个不存在的文件时,会引发FileNotFoundError。如前文示例:

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

通过try - except语句捕获FileNotFoundError,并进行相应处理,如提示用户文件未找到。

权限不足错误

如果没有足够的权限打开或写入文件,会引发PermissionError。例如:

try:
    with open('/system/protected_file.bin', 'wb') as file:
        file.write(b'data')
except PermissionError:
    print("权限不足,无法写入文件")

这里尝试写入一个系统保护文件,会引发PermissionError,通过捕获该异常并给出提示,告知用户权限不足。

文件损坏错误

对于一些特定格式的二进制文件,如果文件损坏,在读取或解析过程中可能会引发各种异常,如struct.unpack()时格式不匹配引发struct.error。例如:

import struct

try:
    bad_data = b'\x01\x02\x03'
    struct.unpack('i', bad_data)
except struct.error as e:
    print(f"解包数据时出错: {e}")

这里bad_data的长度与'i'(表示整数)格式要求的长度不匹配,会引发struct.error,通过捕获该异常并打印错误信息,帮助定位文件损坏问题。

二进制文件与内存视图

Python的内存视图(memoryview)提供了一种高效访问二进制数据的方式,特别是在处理大型二进制对象时。

创建内存视图

可以通过memoryview()函数创建一个内存视图对象,该对象可以指向bytesbytearray对象。例如:

data = b'\x41\x42\x43\x44'
mv = memoryview(data)
print(mv)

这里memoryview(data)创建了一个指向字节串data的内存视图对象mvprint(mv)会输出类似<memory at 0x7f8e0c0b3040>的内存地址信息。

使用内存视图访问数据

内存视图对象支持通过索引和切片访问数据,并且可以转换为其他数据类型。例如:

data = b'\x01\x02\x03\x04'
mv = memoryview(data)
byte_value = mv[2]
print(byte_value)

sliced_mv = mv[1:3]
print(sliced_mv.tobytes())

mv[2]获取内存视图中索引为2的字节值,mv[1:3]创建一个切片内存视图,sliced_mv.tobytes()将切片内存视图转换回字节串。

内存视图在文件操作中的应用

在处理二进制文件时,内存视图可以提高数据读取和写入的效率。例如,使用内存视图读取文件内容:

try:
    with open('example.bin', 'rb') as file:
        mv = memoryview(file.read())
        # 处理内存视图数据
        pass
except FileNotFoundError:
    print("文件未找到")

这里通过memoryview(file.read())将文件读取的字节数据转换为内存视图,以便后续高效处理。在写入文件时,也可以将内存视图对象直接写入文件,避免不必要的数据复制。

二进制文件处理的性能优化

使用缓冲技术

在读取和写入二进制文件时,合理使用缓冲可以提高性能。Python的open()函数有一个buffering参数,默认值为-1,即使用系统默认的缓冲策略。可以根据文件大小和读写模式调整该参数。

对于大文件读取,可以设置较大的缓冲区,例如:

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

这里buffering=8192设置缓冲区大小为8KB,相比默认设置,在读取大文件时可以减少系统调用次数,提高读取效率。

在写入文件时,同样可以设置缓冲区:

data = b'...' # 大量数据
try:
    with open('output_file.bin', 'wb', buffering=4096) as file:
        file.write(data)
except Exception as e:
    print(f"写入文件时出错: {e}")

设置合适的缓冲区大小可以减少磁盘I/O操作,从而提高写入性能。

避免不必要的转换

在处理二进制数据时,尽量避免不必要的数据类型转换。例如,在读取二进制文件后,如果数据不需要转换为其他类型(如字符串),则直接以bytes类型进行处理。

错误示例:

try:
    with open('example.bin', 'rb') as file:
        data = file.read()
        text = data.decode('utf - 8') # 不必要的转换
        # 对text进行操作
except FileNotFoundError:
    print("文件未找到")

如果后续操作并不需要将二进制数据转换为字符串,这样的转换会增加额外的计算开销。应直接对databytes类型)进行操作,以提高性能。

多线程与异步处理

对于涉及大量二进制文件处理的任务,可以考虑使用多线程或异步编程来提高整体性能。例如,使用concurrent.futures模块的ThreadPoolExecutor进行多线程文件读取:

import concurrent.futures

def read_file(file_path):
    try:
        with open(file_path, 'rb') as file:
            return file.read()
    except FileNotFoundError:
        return None

file_paths = ['file1.bin', 'file2.bin', 'file3.bin']
with concurrent.futures.ThreadPoolExecutor() as executor:
    results = list(executor.map(read_file, file_paths))

这里ThreadPoolExecutor创建一个线程池,executor.map(read_file, file_paths)使用线程池并行调用read_file函数读取多个文件,提高文件读取的效率。

在异步处理方面,可以使用asyncio库结合aiofiles库进行异步文件操作。aiofiles提供了异步的文件读写方法。示例如下:

import asyncio
import aiofiles

async def read_file_async(file_path):
    async with aiofiles.open(file_path, 'rb') as file:
        return await file.read()

async def main():
    file_paths = ['file1.bin', 'file2.bin', 'file3.bin']
    tasks = [read_file_async(path) for path in file_paths]
    results = await asyncio.gather(*tasks)
    return results

loop = asyncio.get_event_loop()
results = loop.run_until_complete(main())

此代码使用asyncioaiofiles实现异步读取多个二进制文件,避免在文件I/O操作时阻塞主线程,提高整体性能。

跨平台考虑

在处理二进制文件时,需要考虑不同操作系统之间的差异。

路径分隔符

不同操作系统使用不同的路径分隔符,Windows使用反斜杠(\),而Linux和macOS使用正斜杠(/)。为了编写跨平台代码,可以使用os.path.join()函数来构建文件路径。例如:

import os

file_name = 'example.bin'
directory = 'data'
file_path = os.path.join(directory, file_name)
print(file_path)

os.path.join(directory, file_name)会根据当前操作系统的路径分隔符正确拼接路径,在Windows上会生成data\example.bin,在Linux和macOS上会生成data/example.bin

换行符

文本文件在不同操作系统上的换行符表示不同,Windows使用\r\n,Linux和macOS使用\n。虽然二进制文件通常不涉及换行符问题,但在处理可能包含文本片段的二进制文件(如某些日志文件格式)时,需要注意这一点。

在读取和写入这种文件时,应使用二进制模式,以避免Python自动转换换行符。例如:

try:
    with open('log_file.bin', 'rb') as file:
        data = file.read()
    # 处理数据,避免自动换行符转换
    with open('new_log_file.bin', 'wb') as new_file:
        new_file.write(data)
except FileNotFoundError:
    print("文件未找到")

通过使用二进制模式读取和写入文件,可以确保数据中的换行符保持原样,不被操作系统自动转换。

文件权限

不同操作系统对文件权限的管理方式不同。在Python中,可以使用os.chmod()函数来设置文件权限,但需要注意不同操作系统上权限的表示和含义。

在Linux和macOS上,文件权限可以用八进制数表示,例如0o644表示文件所有者有读写权限,其他用户有读权限。示例如下:

import os

file_path = 'example.bin'
os.chmod(file_path, 0o644)

在Windows上,文件权限管理相对复杂,且Python的os.chmod()函数功能有限。通常需要使用其他特定的Windows API或工具来进行更精细的权限管理。在编写跨平台代码时,需要考虑到这些差异,尽量提供通用的功能或针对不同操作系统进行适当的处理。

通过以上对Python二进制文件读写的全面介绍,涵盖了基础概念、操作模式、常用方法、数据结构处理、实际应用、错误处理、性能优化以及跨平台考虑等多个方面,希望能帮助开发者更好地掌握和应用二进制文件处理技术。