如何仅用Python不依赖第三方库读取JPG文件的EXIF元数据?
JPG EXIF元数据手动解析方案
规范说明
你参考的JFIF文档是JPEG文件的基础结构规范,EXIF元数据存储在JPEG的APP1(0xFFE1)标记段中,内部结构遵循TIFF 6.0的子集规范,可按以下逻辑直接解析,无需依赖第三方库。
核心解析步骤
- 确认字节序
你输出中Exif\x00\x00后跟随的2字节是字节序标记:
MM代表大端序(Motorola字节序)II代表小端序(Intel字节序)
后续所有多字节数值的读取都需要遵循该字节序。
定位图像文件目录(IFD)
字节序之后的2字节是固定TIFF标记0x002A,再往后4个字节是第一个IFD的偏移地址,按字节序转换为整数后,就是IFD相对于TIFF起始位置(即Exif\x00\x00后第一个字节)的偏移量。解析IFD条目
IFD开头的2字节是当前IFD的条目总数,每个条目固定12字节,结构如下:
| 字节长度 | 含义 |
|---|---|
| 2字节 | 标签ID(对应不同元数据字段,比如0x010F对应相机制造商、0x0110对应相机型号) |
| 2字节 | 数据类型(1=字节、2=ASCII字符串、3=短整数、4=长整数等) |
| 4字节 | 数据计数 |
| 4字节 | 数据值/数据偏移:如果数据总长度≤4字节,这里直接存储值,否则存储数据相对于TIFF起始位置的偏移地址 |
可运行的原生Python实现
def read_int(bytes_data, byteorder, length=2): return int.from_bytes(bytes_data, byteorder=byteorder, signed=False) with open("./image.jpg", "rb") as f: # 常用EXIF标签映射表,可根据需求自行补充 tag_map = { 0x010F: "制造商", 0x0110: "相机型号", 0x0131: "编辑软件", 0x0132: "修改时间", 0x829A: "曝光时间", 0x8827: "ISO感光度", 0x920A: "焦距" } while True: marker = f.read(2) # 碰到图像起始标记或者文件结束就退出 if not marker or marker == b'\xff\xda': break # 匹配EXIF存储的APP1段 if marker == b'\xff\xe1': seg_len = read_int(f.read(2), 'big') exif_header = f.read(6) if exif_header[:4] == b'Exif': byteorder_mark = f.read(2) byteorder = 'big' if byteorder_mark == b'MM' else 'little' # 校验TIFF标记合法性 if read_int(f.read(2), byteorder) != 0x002A: continue # 读取IFD偏移并跳转 ifd_offset = read_int(f.read(4), byteorder) f.seek(f.tell() - 8 + ifd_offset) # 读取IFD条目总数 entry_count = read_int(f.read(2), byteorder) for _ in range(entry_count): tag_id = read_int(f.read(2), byteorder) data_type = read_int(f.read(2), byteorder) data_count = read_int(f.read(4), byteorder) data_field = f.read(4) if tag_id in tag_map: tag_name = tag_map[tag_id] # 此处以最常用的ASCII类型为例,其他类型可按TIFF规范补充解析逻辑 if data_type == 2: if data_count <= 4: val = data_field[:data_count-1].decode('utf-8', errors='ignore') else: data_offset = read_int(data_field, byteorder) current_pos = f.tell() f.seek(f.tell() - 8 + data_offset) val = f.read(data_count-1).decode('utf-8', errors='ignore') f.seek(current_pos) print(f"{tag_name}: {val}") else: # 跳过非EXIF的APP1段剩余内容 f.seek(seg_len - 6, 1) else: # 跳过其他无关JPEG段 if marker.startswith(b'\xff') and marker not in [b'\xff\xd8', b'\xff\xd9']: seg_len = read_int(f.read(2), 'big') f.seek(seg_len - 2, 1)
注意事项
- 不要硬编码读取214字节,每个APP1段的长度由段开头的2字节动态指定,硬编码会导致解析失败
- 如果需要解析更多非字符串类型的EXIF字段,参照TIFF规范补充对应数据类型的解析逻辑即可
- 缩略图对应的IFD解析逻辑和主IFD完全一致,有需求可自行扩展
内容的提问来源于stack exchange,提问作者code0x00
相关产品推荐
相关产品推荐

