二进制文件以字节(byte)流的方式写入或读取,不会对输入数据进行任何处理,因此对计算机友好。相比文本文件,可以有效的减少存储空间,也很适合网络传输。二进制文件一般被用于存储自定义格式的数据、视频、音频、图片、程序指令等等。
有了前面2篇文章的知识,对于二进制文件的相关知识,就很容易掌握。首先,飞哥会给小伙伴讲解读写操作的相关函数。最后,带领小伙伴完成一个使用二进制文件保存私密信息的实战案例。
01 读写二进制文件的函数读写二进制文件的函数和读写文本文件的函数都是一样的,功能和用法也是一样,只是参数的含义有差别。但操作二进制文件的函数主要是下面3个:
1、object = open(file, mode)
打开指定文件,返回文件对象。注意: mode参数中要多加一个b# 写入模式打开二进制文件,文件不存在会先创建,文件存在则会空清空文件内容f = open("私密文件.bin", 'wb')# 追加模式打开二进制文件,文件不存在会先创建,文件存在不会空清空文件内容f = open("私密文件.bin", 'ab')# 读取模式打开二进制文件,文件不存在会报异常f = open("私密文件.bin", 'rb')# 读写模式打开二进制文件,文件不存在会先创建,文件存在则会空清空文件内容f = open("私密文件.bin", 'wb+')
2、f.read (n=-1)
从文件中读取n个字节(byte)的数据。返回bytes类型的字节数据。
3、f.write(s: Union[bytes,bytearray])
bytes或bytearray类型的字节数据写入到文件中。返回成功写入到文件中的数据的字节个数。
4、f.close()
关闭文件。
备注:有关bytes和字节数组(bytearray)的相关知识,在上一篇的文章《【Python技术进阶-10】Python数据类型bytes、bytearray详细指南》(点击左侧标题查看文章),有详细的讲解。
02 案例设计1、需求
老王有一天突然找到飞哥说,“我电脑上有很多的私密照片,由于涉及到个人隐私。我可不想在网络上出名,听说你是搞IT的,有什么办法,给我处理一下吗?”
飞哥,“没问题,小菜一碟。给我半天时间,给你做一个老王专用的图片加解密软件。傻瓜式操作,点点鼠标就可以了。保证隐私安全,只有用我这软件,同时输入你的密码才能查看你的私密图片。”
老王,“太好了,真是没找错人,搞定后,请你吃大餐。”
2、设计说明
需求的核心是对私密图片进行加密处理,非常适合自定义一个二进制文件的数据格式,自定义数据结构示意图如上图所示。处理思路如下所示:
1)整个文件分为头部数据和图片加密数据;
2)读取原始图片数据,加密图片数据。然后生成头部数据,最后将这2部分数据存入新的二进制文件中;
3)查看私密图片时,先读取头部数据,获取到被加密图片的文件名称,加密数据的长度,然后进行数据的校验;
4)然后解密数据,获取到真实的图片数据;
5)使用图片库显示图片。
3、文件头部数据说明
1)2个字节的标识符,如0xF66F。当读取一个文件时,先读取2个字节的数据,进行比对,进行一个简单的判断,看是不是我们生成的二进制文件。
2)3个字节的版本号,如字符串2.1,2.2。主要为了以后自定义的二进制数据格式升级后,能够兼容以前的老文件。在处理时根据版本号,分别进行处理。
3) 记录文件创建时的日期,可以使用时间戳,也可以使用格式化后的时间字符串。先读取1个字节的数据,就知道日期数据占用几个字节,然后读取出日期字节数据,转换为真实的日期,就知道文件的创建日期。
4) 文件名,可以记录当时加密时的真实图片的名称。由于名称的长度也不确定,因此也需要1个字节,用于保存文件名称的长度。
4) 32位md5校验码,主要是为了避免文件被损坏或篡改,通过这个校验码就可以知道文件是否完整。版本号、创建日期、文件名、数据长度、原始私密照片的二进制数据,都要参与md5校验码的生成。
5) 4字节的数据长度,由于图片文件的长度是不确定的,所以加密后的图片数据的长度也是不确定的,所以需要用4个字节存储加密数据的字节长度。
03 案例实现1、代码
# -*- coding:utf-8 -*-import osimport timeimport structfrom Crypto.Cipher import AESfrom Crypto import Randomimport hashlibdef add_16key(key): """ 将key的字节长度补齐为16字节的倍数 :param key: 加解密时使用的字符串key :return: 补齐后的bytes类型的key """ key_bytes = bytes(key, encoding='utf-8') count = len(key_bytes) if count % AES.block_size > 0: add_count = AES.block_size - (count % AES.block_size) # 以补齐的字节个数作为填充 key_bytes = key_bytes + bytes([0]) * add_count return key_bytesdef encrypt(key, data_bytes): """ 对传递来的data_bytes数据进行加密处理。 :param key: 加密时使用的key data_bytes: 被加密的数据,如果data_bytes不是16的倍数,需要进行处理补齐为16的倍数。 :return 返回加密后的bytes类型的数据 """ if len(data_bytes) == 0: return data_bytes key = add_16key(key) iv = Random.new().read(AES.block_size) cipher = AES.new(key, AES.MODE_CBC, iv) # 这里密钥key 长度必须为16(AES-128), 24(AES-192),或者32 (AES-256)Bytes 长度 # 目前AES-128 足够目前使用 count = len(data_bytes) add_count = 0 if count % AES.block_size > 0: add_count = AES.block_size - (count % AES.block_size) # 以补齐的字节个数作为填充 data_bytes = data_bytes + (bytes([add_count]) * add_count) # 调用加密 crypt_data_bytes = cipher.encrypt(data_bytes) # iv、填充个数和加密数据一起返回 return iv + bytes([add_count]) + crypt_data_bytesdef decrypt(key, crypt_bytes): """ 对加密数据crypt_bytes进行解密处理。 :param key: 解密时用到的key。 crypt_bytes: 被加密的数据。 :return 解密后的bytes类型的原始数据。 """ if len(crypt_bytes) <= AES.block_size: return '' key = add_16key(key) iv = crypt_bytes[:AES.block_size] cipher = AES.new(key, AES.MODE_CBC, iv) decrypt_bytes = cipher.decrypt(crypt_bytes[AES.block_size + 1:]) # 去掉填充的数据 add_count = crypt_bytes[AES.block_size] if add_count > 0: decrypt_bytes = decrypt_bytes[:-add_count] return decrypt_bytesclass CryptoFile: """ 使用AES对文件进行加解密 """ def __init__(self, key): self.flag = b'\xf6\x6f' self.version = b'1.1' self.key = key def encrypt_file(self, src_path, dst_dir): """ 对文件加密处理,加密后的文件保存在指定的目录。 :param src_path: 被加密的文件的路径 :param dst_dir: 加密后的文件保存的目录 :return: True=加密成功, False=加密失败 """ name = os.path.split(src_path)[1] # 获取文件的加密内容 suc, data_bytes = self.get_file_encrypt_data(src_path) if not suc: return False # 获取头部数据 header_bytes = self.get_header_bytes(name, data_bytes) # 写目标文件 if not os.path.exists(dst_dir): os.makedirs(dst_dir) name_ext = os.path.splitext(name) dst_path = os.path.join(dst_dir, name_ext[0] + '_1' + name_ext[1]) with open(dst_path, 'wb') as f: f.write(header_bytes) f.write(data_bytes) return True def decrypt_file(self, src_path, dst_dir): """ 对加密文件进行解密处理,并将解密后的文件保存在指定的目录 :param src_path: 加密文件的路径 :param dst_dir: 解密后的文件保存的目录 :return: True=解密成功, False=解密失败 """ suc, file_name, file_bytes = self.get_file_decrypt_bytes(src_path) if not suc: return False # 保存文件 if not os.path.exists(dst_dir): os.makedirs(dst_dir) dst_path = os.path.join(dst_dir, file_name) with open(dst_path, 'wb') as f: f.write(file_bytes) return True def get_file_decrypt_bytes(self, file_path): # 读取文件内容 data = None with open(file_path, 'rb') as f: data = f.read() if data is None: return False, None, None # 判断其实标志 if not data.startswith(self.flag): return False, None, None # 版本号 index = 0 version = data[2:5].decode(encoding='utf-8') index = 5 # 计算文件中的md5 md5 = hashlib.md5() md5.update(data[:5]) # 时间信息 field_byte_length = data[index] index += 1 create_times = struct.unpack(', data[index:index+field_byte_length])[0] md5.update(data[index:index+field_byte_length]) str_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(create_times)) index += field_byte_length # 文件名长度 field_byte_length = data[index] index += 1 src_name = data[index:index+field_byte_length].decode(encoding='utf-8') md5.update(data[index:index+field_byte_length]) index += field_byte_length # 校验码 field_byte_length = 32 md5_code = data[index:index+field_byte_length].decode(encoding='utf-8') index += field_byte_length # 加密数据的长度 field_byte_length = 4 encrypt_data_length = struct.unpack(', data[index:index+field_byte_length])[0] md5.update(data[index:index+field_byte_length]) index += field_byte_length # 加密数据 md5.update(data[index:index + encrypt_data_length]) # 计算新的md5,然后进行对比 new_md5_code = md5.hexdigest() if md5_code != new_md5_code: return False, None, None # 解密密文件内容 return True, src_name, decrypt(self.key, data[index:index + encrypt_data_length]) def get_file_encrypt_data(self, file_path): # 读取文件内容 data = None with open(file_path, 'rb') as f: data = f.read() if data is None: return False, None # 加密文件内容 return True, encrypt(self.key, data) def get_header_bytes(self, name, data_bytes): md5 = hashlib.md5() header_data = bytearray() header_data.extend(self.flag) header_data.extend(self.version) md5.update(header_data) now = time.time() now_bytes = struct.pack(', now) # 日期长度 header_data.extend(struct.pack(', len(now_bytes))) # 日期数据 header_data.extend(now_bytes) md5.update(now_bytes) # 文件名称 name_bytes = name.encode(encoding='utf-8') header_data.extend(struct.pack(', len(name_bytes))) header_data.extend(name_bytes) md5.update(name_bytes) # 数据长度(4个字节) data_length_bytes = struct.pack(', len(data_bytes)) md5.update(data_length_bytes) md5.update(data_bytes) # 计算校验码 md5_code = md5.hexdigest() header_data.extend(md5_code.encode(encoding='utf-8')) header_data.extend(data_length_bytes) return header_dataif __name__ == '__main__': # 生成对象时,要传入一个key(密码) crypto = CryptoFile("python is good") suc = crypto.encrypt_file("车展.jpg", "加密测试") print("加密结果:", suc) suc = crypto.decrypt_file("加密测试\\车展_1.jpg", "解密测试") print("解密结果:", suc) # encrypt_data = encrypt("123456", b'asfasdee') # print(encrypt_data) # decrypt_data = decrypt("123456", encrypt_data) # print(decrypt_data)
2、效果
废话不多说,直接看加解密效果图。
加密后的二进制文件,使用十六进制查看软件打开,可以看到文件开头的2个标记字节。标记后面的3个字节是版本号,在这一行的右侧可以看见字符1.1
好了,今天的内容就分享到这里了,后面的案例代码,小伙伴可以直接把代码拷贝到自己的PyCharm上运行。如有不清楚的或疑问,可以私信飞哥交流。
Python新手入门、进阶问题,欢迎私信交流,看到就回复。
END 扫码关注我们 专业提供 定制学习计划和职业规划服务 公众号:Python编程研习社