news 2026/10/20 18:26:30

Exif 信息读取与批量遍历实战:从底层结构到 Python 性能优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Exif 信息读取与批量遍历实战:从底层结构到 Python 性能优化

1. 从一张照片说起:为什么我们需要读取 Exif

你拍了一张照片,发到群里,朋友看了一眼就说:“这是在城东那个湿地公园拍的吧?下午四点多?”你一愣——他根本不在现场。秘密就藏在照片文件本身里。每一张由手机或相机拍摄的 JPEG 图片,除了你看到的画面像素,还附带了一包“行李”,里面记录了拍摄时间、设备型号、光圈快门、甚至经纬度坐标。这包行李就是Exif(Exchangeable Image File Format),中文一般叫“可交换图像文件格式”的元数据。

Exif 信息读取与遍历,说白了就是两件事:第一,把图片里这包元数据完整地“拆”出来,看看里面到底存了什么;第二,批量地对一堆图片做同样的操作,把结果整理成可用的表格或报告。这件事听起来简单,但实际做起来,坑比想象中多得多。不同厂商写入的字段名不一样,有的字段是 ASCII 有的却是 Rational 分数,有的时间戳带时区有的不带,GPS 坐标还分度分秒和十进制两套表示法。你要是不了解这些细节,读出来的数据很可能是一团乱麻。

这篇文章适合谁看?如果你是做图像管理工具的开发者、需要整理大量素材的摄影师、做数据取证或内容审核的技术人员,或者只是单纯好奇自己手机照片里藏了多少隐私信息,那这篇内容都能帮到你。我会从 Exif 的底层结构讲起,然后给出可直接运行的 Python 实操代码,再重点分享批量遍历时的性能优化和踩坑经验。全程不废话,都是我自己在项目里摸爬滚打总结出来的东西。

2. Exif 的底层结构:先搞懂它长什么样

2.1 Exif 在文件里的物理位置

要读取 Exif,得先知道它藏在哪。以最常见的 JPEG 为例,文件开头是FFD8(SOI,图像起始标记),结尾是FFD9(EOI,图像结束标记)。中间是一段段的“段”(Segment),每段以FF加一个标记字节开头。Exif 数据就藏在FFE1这个段里,学名叫APP1(Application Segment 1)。

APP1 段的结构是这样的:先是 2 字节的段长度,然后是 6 字节的固定标识Exif\0\0,接着才是真正的 TIFF 格式数据。这个 TIFF 头很关键,它决定了后面所有数据的字节序——开头两个字节如果是II(0x4949),就是小端序(Intel);如果是MM(0x4D4D),就是大端序(Motorola)。字节序搞错,读出来的数字全是乱的,这是新手最容易翻车的地方。

提示:PNG 图片的 Exif 存储方式完全不同,它放在eXIf数据块里,而且很多 PNG 压根没有 Exif。如果你要处理混合格式,得分别对待。

2.2 IFD:Exif 的目录索引机制

TIFF 头之后是一个 4 字节的偏移量,指向第一个IFD(Image File Directory,图像文件目录)。IFD 就像一本书的目录页,每个条目叫一个IFD Entry,固定 12 字节,结构如下:

字节偏移长度含义
0-12 字节Tag 编号(标识这个字段是什么)
2-32 字节数据类型(ASCII、Short、Long、Rational 等)
4-74 字节数据个数(Count)
8-114 字节值或值的偏移量

这里有个精妙的设计:如果数据不超过 4 字节,就直接塞在最后 4 个字节里;如果超过 4 字节,这 4 个字节存的是一个偏移量,指向数据真正存放的位置。我第一次手写解析器的时候没注意这点,读字符串字段全是乱码,排查了半天才发现是把偏移量当成了内容。

Exif 通常有多个 IFD,按层级组织:IFD0(主图像信息,如厂商、型号)、Exif SubIFD(拍摄参数,如光圈、快门、ISO)、GPS IFD(地理位置)、IFD1(缩略图信息)。每个 IFD 结尾有一个 4 字节指针,指向下一个 IFD,形成链表。遍历的本质,就是顺着这条链把每个 IFD 的每个 Entry 都读一遍。

2.3 常见 Tag 与数据类型的对应关系

不是所有字段都值得关注。实际项目里,我通常重点抓这几类:

  • 时间类:DateTime(0x0132)、DateTimeOriginal(0x9003)、DateTimeDigitized(0x9004)
  • 设备类:Make(0x010F)、Model(0x0110)、Software(0x0131)
  • 拍摄参数:ExposureTime(0x829A)、FNumber(0x829D)、ISOSpeedRatings(0x8827)、FocalLength(0x920A)
  • GPS 类:GPSLatitude(0x0002)、GPSLatitudeRef(0x0001)、GPSLongitude(0x0004)、GPSLongitudeRef(0x0003)、GPSAltitude(0x0006)

数据类型尤其要注意Rational,它是两个 Long 组成的分数,分子在前分母在后。比如 FNumber 读出来是28/10,你得自己算成 2.8。GPS 坐标更麻烦,是三个 Rational 组成的度、分、秒,还要结合 Ref 字段判断南北纬和东西经。

3. 工具选型:为什么我不建议你从零手写解析器

3.1 手写解析的诱惑与陷阱

刚接触 Exif 的时候,很多人(包括我)会想:不就是按字节读吗,我自己写一个多酷。我确实写过,用 Python 的struct模块一个字节一个字节地 unpack。结果呢?处理了 200 张图,有 30 多张报错,剩下的读出来字段缺失严重。问题出在:不同厂商的 Exif 写入规范差异极大,有的字段长度不对,有的偏移量指向了文件外,有的干脆是损坏的。手写解析器要处理所有这些边界情况,工作量远超预期。

手写唯一的优势是“零依赖”,适合嵌入式或极端受限环境。但如果你是在做正常的桌面或服务端应用,用成熟库是明智得多。省下来的时间拿去优化业务逻辑不香吗。

3.2 主流库横向对比

我实际用过的几个库,各有脾气:

库名语言优点缺点
PillowPython安装简单,API 友好Exif 支持不完整,GPS 解析弱
exifreadPython纯 Python,字段全,支持遍历速度偏慢,维护不活跃
ExifToolPerl/命令行支持格式最全,字段最权威需外部进程调用,有性能开销
piexifPython读写都支持,轻量只处理 JPEG/TIFF,API 略原始
metadata-extractorJava稳定,字段丰富仅 Java 生态

我的选择策略是:快速原型用 Pillow,批量生产用 exifread 或 ExifTool。如果对字段完整性要求极高(比如取证场景),ExifTool 是事实标准,没有之一。它的字段命名体系(比如EXIF:DateTimeOriginal、GPS:GPSLatitude)已经成为行业通用语言。

3.3 安装与环境准备

以 Python 为例,我推荐这套组合:

pip install exifread Pillow

exifread 负责深度解析,Pillow 负责打开图片和做兜底。如果你要用 ExifTool,去官网下载可执行文件,放到 PATH 里即可,Python 侧用subprocess调用。

注意:exifread 对 HEIC 格式支持有限,如果你要处理苹果设备导出的 HEIC,建议先用pillow-heif转成 JPEG,或者直接用 ExifTool。

4. 单张图片读取:从能跑到跑对

4.1 最小可用代码

先上一个能跑的基础版本,用 exifread 读取单张图片:

import exifread def read_exif_basic(path): with open(path, 'rb') as f: tags = exifread.process_file(f, details=False) for tag, value in tags.items(): print(f"{tag}: {value}") return tags read_exif_basic("sample.jpg")

details=False表示只读基础字段,跳过 MakerNote 等厂商私有数据,速度快很多。如果你需要厂商特定信息(比如快门次数),改成details=True,但解析时间可能翻好几倍。

4.2 关键字段的提取与格式化

直接打印出来的值很多是 exifread 的封装对象,不能直接用。我通常写一个规范化函数,把常用字段转成 Python 原生类型:

from datetime import datetime def normalize_exif(tags): result = {} # 时间字段 for key in ['EXIF DateTimeOriginal', 'Image DateTime']: if key in tags: try: result['datetime'] = datetime.strptime( str(tags[key]), '%Y:%m:%d %H:%M:%S' ) break except ValueError: pass # 设备字段 result['make'] = str(tags.get('Image Make', '')).strip() result['model'] = str(tags.get('Image Model', '')).strip() # 拍摄参数 if 'EXIF FNumber' in tags: result['f_number'] = tags['EXIF FNumber'].values[0].num / \ tags['EXIF FNumber'].values[0].den if 'EXIF ExposureTime' in tags: result['exposure'] = tags['EXIF ExposureTime'].values[0].num / \ tags['EXIF ExposureTime'].values[0].den if 'EXIF ISOSpeedRatings' in tags: result['iso'] = tags['EXIF ISOSpeedRatings'].values[0] return result

这里有个细节:tags['EXIF FNumber'].values[0]返回的是一个Ratio对象,有.num和.den属性。直接float()转换在某些版本会报错,老老实实做除法最稳。

4.3 GPS 坐标的解析:最容易出错的地方

GPS 是 Exif 里最复杂也最有价值的部分。原始数据是三个 Rational(度、分、秒),加上一个 Ref 字段表示方向。转换公式是:

十进制度数 = 度 + 分/60 + 秒/3600

如果 Ref 是 'S'(南纬)或 'W'(西经),结果取负。完整代码:

def parse_gps(tags): def to_deg(values): d = values[0].num / values[0].den m = values[1].num / values[1].den s = values[2].num / values[2].den return d + m / 60 + s / 3600 lat = lon = None if 'GPS GPSLatitude' in tags and 'GPS GPSLatitudeRef' in tags: lat = to_deg(tags['GPS GPSLatitude'].values) if str(tags['GPS GPSLatitudeRef']) == 'S': lat = -lat if 'GPS GPSLongitude' in tags and 'GPS GPSLongitudeRef' in tags: lon = to_deg(tags['GPS GPSLongitude'].values) if str(tags['GPS GPSLongitudeRef']) == 'W': lon = -lon return lat, lon

我踩过的坑:有些手机写入的 GPS 秒数是0/0,除零会崩。所以to_deg里最好加个判断,分母为 0 时按 0 处理。另外,部分安卓机型的 Ref 字段是空字符串,这时候默认按北纬东经处理。

5. 批量遍历:性能与健壮性的双重考验

5.1 遍历策略设计

单张读取跑通后,真正的挑战来了:几千上万张图片怎么高效处理。我的基本策略是:

  1. 先扫描后解析:用os.walk或pathlib先收集所有图片路径,过滤掉非图片文件,再统一处理。
  2. 并发加速:Exif 解析是 IO 密集加 CPU 密集的混合任务,用concurrent.futures的进程池比线程池更有效,因为 Python 的 GIL 会限制线程的 CPU 利用率。
  3. 增量处理:记录已处理的文件(用文件路径加修改时间做 key),下次运行跳过未变化的文件。
from concurrent.futures import ProcessPoolExecutor from pathlib import Path def collect_images(root): exts = {'.jpg', '.jpeg', '.tif', '.tiff', '.png'} return [p for p in Path(root).rglob('*') if p.suffix.lower() in exts] def process_batch(root, workers=4): files = collect_images(root) results = [] with ProcessPoolExecutor(max_workers=workers) as executor: for res in executor.map(process_one, files, chunksize=20): if res: results.append(res) return results

chunksize=20是个经验值。太小了进程间通信开销大,太大了单个进程内存压力大。我实测下来,处理 1 万张 3MB 左右的 JPEG,4 进程加 chunksize 20,比单进程快 3.5 倍左右。

5.2 异常处理:坏图、无 Exif、编码错误

批量处理最怕的就是一张坏图让整个任务崩掉。我的原则是:任何单张图片的失败都不能影响整体流程。process_one函数必须包一层 try-except:

def process_one(path): try: with open(path, 'rb') as f: tags = exifread.process_file(f, details=False) if not tags: return {'path': str(path), 'status': 'no_exif'} data = normalize_exif(tags) lat, lon = parse_gps(tags) data.update({'path': str(path), 'lat': lat, 'lon': lon, 'status': 'ok'}) return data except Exception as e: return {'path': str(path), 'status': 'error', 'error': str(e)}

常见的异常类型我整理成了一张速查表:

异常现象可能原因处理方式
KeyError字段不存在用.get()或in判断
ZeroDivisionErrorRational 分母为 0除法前判断分母
UnicodeDecodeError字符串字段编码非 UTF-8用errors='ignore'解码
读取超时文件损坏或超大设置超时,跳过
内存暴涨一次性加载所有结果分批写入磁盘

5.3 结果输出:CSV 还是数据库

小规模(几千张以内)用 CSV 就够了,方便用 Excel 打开检查。大规模建议直接写 SQLite,支持索引和查询。我通常用 pandas 做中转:

import pandas as pd df = pd.DataFrame(results) df.to_csv('exif_report.csv', index=False, encoding='utf-8-sig')

utf-8-sig这个编码很关键,不加的话 Excel 打开中文会乱码。这是我被同事问过无数次的问题。

6. 实战中的坑与排查技巧

6.1 时间字段的时区陷阱

Exif 的 DateTimeOriginal 字段不带时区信息。它记录的是相机本地时间。如果你在北京拍的照片,字段值是2024:05:01 14:30:00,但相机可能设置的是 UTC,也可能是任意时区。做时间分析时,千万别假设它是 UTC。我的做法是:如果图片同时有 GPS 信息,可以根据经纬度推断时区;否则就原样保留,不做转换。

另外,有些设备写入的时间格式不规范,比如用-而不是:分隔,或者月份是单个数字。解析时最好用正则先做一次清洗。

6.2 厂商私有字段的读取

MakerNote 是厂商存放私有数据的区域,佳能、尼康、索尼各有各的格式,没有统一标准。exifread 对部分厂商有支持,但覆盖不全。如果你需要读取快门次数、镜头序列号这类信息,ExifTool 是唯一靠谱的选择。调用方式:

import subprocess, json def read_with_exiftool(path): result = subprocess.run( ['exiftool', '-j', '-G', str(path)], capture_output=True, text=True ) return json.loads(result.stdout)[0]

-j输出 JSON,-G给字段加组名前缀(如EXIF:、GPS:),避免字段名冲突。ExifTool 启动有固定开销(约 100ms),批量处理时建议一次传多个文件,而不是循环调用。

6.3 隐私清理与合规提醒

读取 Exif 的反面就是清理 Exif。很多社交平台上传时会自动剥离,但通过邮件、网盘分享原图时,GPS 坐标就暴露了。如果你在做图片分享类产品,默认剥离 GPS 字段应该是基本设计。清理用 piexif 很简单:

import piexif def strip_gps(path, output): exif_dict = piexif.load(path) exif_dict['GPS'] = {} piexif.insert(piexif.dump(exif_dict), path, output)

注意:清理 Exif 会改变文件内容,如果图片有数字签名或哈希校验,清理后校验会失败。做取证类应用时要特别小心。

6.4 常见问题速查

问题排查方向解决
读出来全是 None文件是否真的是 JPEG检查文件头 FFD8
GPS 坐标偏差几公里度分秒转换公式错核对 60 和 3600
批量处理卡死某张图超大或损坏加超时和文件大小限制
字段名对不上库版本差异打印所有 tag 名对照
中文乱码编码问题用 utf-8-sig 输出

7. 进阶方向:从读取到应用

Exif 读取本身不是目的,它通常是更大流程的一环。我做过几个有意思的应用场景,可以给你一些启发。

第一个是照片自动归档。读取 DateTimeOriginal,按年月建立文件夹,再根据 GPS 反查城市名(调用逆地理编码服务),归档路径就是2024/05/北京/。这个需求在摄影工作室特别常见,几千张照片手动分类能累死人。

第二个是设备统计报告。批量读取 Make 和 Model,统计团队里谁用什么设备拍得最多,哪个焦段用得最频繁。这个数据对器材采购决策很有参考价值。

第三个是异常检测。比如检测图片的修改时间(FileModifyDate)和拍摄时间(DateTimeOriginal)差异过大,可能说明图片被后期处理过。或者检测 GPS 坐标落在水域、无人区等不合理位置,用于内容审核。

这些应用的共同点是:Exif 读取只是数据入口,真正的价值在于后续的分析和决策。所以你在设计读取模块时,就要考虑输出格式是否方便下游使用。我的建议是统一输出成扁平化的字典或 DataFrame,字段名用英文小写下划线,时间统一成 ISO 8601 格式,坐标统一成十进制度数。这样下游无论是做可视化还是入库,都不用再做转换。

最后分享一个我个人的习惯:每次做 Exif 相关的项目,我都会先拿 20 张不同类型的图片(不同品牌手机、不同相机、截图、网图、修过的图)跑一遍,把结果打印出来人工核对。这一步能提前发现 80% 的字段解析问题,比写完整个流程再调试高效得多。Exif 这东西,规范是一回事,实际数据是另一回事,只有拿真实样本喂出来的代码才靠得住。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 18:25:57

AI写作降AIGC实战:检测原理、改写逻辑与避坑指南

最近大半年,我用AI写东西的频率越来越高,随之而来的一个问题也变得越来越扎眼——内容“AI味”太重。你辛辛苦苦用AI列好框架、写好初稿,发出去朋友第一反应:“这不会是AI写的吧?”这话听着轻巧,但放在公众…

作者头像 李华
网站建设 2026/10/20 18:25:50

context-mode机制设计:从参数爆炸到模式化上下文管理

1. 从"context-mode"这个词说起:它到底指什么第一次看到"context-mode"这个标题,很多人会愣一下——它不像"XX管理系统"或"XX爬虫"那样一眼能看出用途。我最初接触这个词是在做对话式应用的时候,当时…

作者头像 李华
网站建设 2026/10/20 18:23:17

前端面试题PDF高效用法:盘点、拆解、模拟面试闭环

简介:一份面向前端求职者和初中级开发者的面试题汇总PDF,聚焦HTML与CSS核心知识点,覆盖从页面构建基础到浏览器兼容、性能优化、SEO策略等高频考点,可帮助读者系统梳理知识体系、应对技术面试。整包共1个PDF文件,大小2…

作者头像 李华
网站建设 2026/10/20 18:22:29

阿里云OOS ChatOps AI助手:用自然语言驱动运维编排实践

早上还在群里被同事问:“能不能把这次变更的执行记录也同步到群里?”下午就看到阿里云OOS出了ChatOps AI助手。这个功能我盯了有一阵子,简单说就是你在钉钉或者企微群里,用自然语言对机器人说一句“帮我把这三台机器的应用日志采集…

作者头像 李华
网站建设 2026/10/20 18:22:29

雨流计数法原理与Python实现:从载荷循环到疲劳寿命分析

简介:雨流计数法是疲劳寿命分析与载荷谱处理中的经典统计方法,适用于机械工程、航空航天及结构工程领域的研究人员和工程师。这份介绍文档系统梳理了雨流计数法的基本原理,包括塔顶法这一别名、与变程对—均值法在计数结果上的等价关系&#…

作者头像 李华