You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何仅用Python不依赖第三方库读取JPG文件的EXIF元数据?

JPG EXIF元数据手动解析方案

规范说明

你参考的JFIF文档是JPEG文件的基础结构规范,EXIF元数据存储在JPEG的APP1(0xFFE1)标记段中,内部结构遵循TIFF 6.0的子集规范,可按以下逻辑直接解析,无需依赖第三方库。

核心解析步骤

  1. 确认字节序
    你输出中Exif\x00\x00后跟随的2字节是字节序标记:
  • MM 代表大端序(Motorola字节序)
  • II 代表小端序(Intel字节序)
    后续所有多字节数值的读取都需要遵循该字节序。
  1. 定位图像文件目录(IFD)
    字节序之后的2字节是固定TIFF标记0x002A,再往后4个字节是第一个IFD的偏移地址,按字节序转换为整数后,就是IFD相对于TIFF起始位置(即Exif\x00\x00后第一个字节)的偏移量。

  2. 解析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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.24 08:24:05