2026/10/18 13:51:28

Python批量提取照片EXIF信息:拍摄时间、机型与GPS坐标实战

Python批量提取照片EXIF信息:拍摄时间、机型与GPS坐标实战 简介这是一份面向摄影爱好者、图像处理初学者及需要批量读取照片元数据用户的EXIF信息提取工具源码包基于C#与.NET开发可解决单张或批量图片拍摄参数读取、导出与分析的常见需求。压缩包共46个文件约1.6MB包含cs源码、dll依赖库、xml配置、config设置、exe可执行程序及sln解决方案等其中MetadataExtractor与XmpCore组件负责解析EXIF与XMP数据Form1等窗体代码展示界面交互逻辑整体结构完整便于二次开发或直接运行。已有800人学习下载。通过该资源读者可获得一套可直接编译运行的EXIF读取方案理解拍摄时间、光圈、快门、ISO、GPS经纬度等字段的提取流程并参考其工程组织方式快速集成到自己的图像处理项目中适合作为工具脚本或课程设计参考。1. 照片 EXIF 信息提取从一张“干净”的图里挖出拍摄时间、机型和 GPS你拿到一批照片想按拍摄时间归档结果发现文件修改时间全被拷贝过程重置了或者你想确认某张图是不是手机原图而不是被二次压缩过的截图。这时候照片里藏着的 EXIF 信息就是唯一的线索。EXIFExchangeable Image File Format是数码相机和手机在生成 JPEG、TIFF 时写入的一组元数据包含拍摄时间、相机厂商、光圈快门、ISO、焦距甚至 GPS 坐标。它不依赖文件系统只要图片没被“清洗”过信息就还在。这个资源包解决的正是“批量、稳定、可编程地读取这些字段”的问题适合做影像归档、内容审核、取证辅助或自动化素材管理的开发者。下面我从实际拆包的角度把提取流程、参数边界和踩坑点一次讲透。2. 提取 EXIF 的三种技术路线为什么我最终选了 Pillow exifread 组合2.1 纯 Python 标准库为什么不够用很多人第一反应是用 Python 的imghdr或直接读二进制。imghdr只能判断图片类型拿不到任何元数据。手动解析 JPEG 的 APP1 段虽然可行但 EXIF 本身是一个嵌套的 TIFF 结构包含 IFD0、ExifIFD、GPSIFD 等多个目录每个目录里的标签类型ASCII、SHORT、RATIONAL解析规则不同。自己写解析器光是处理字节序大端/小端和偏移量计算就能耗掉一整天而且遇到厂商自定义标签直接抓瞎。常见做法是站在成熟库的肩膀上把精力放在业务逻辑上。2.2 Pillow 的_getexif()与 exifread 的分工Pillow 是图像处理的事实标准它的Image._getexif()能返回一个字典键是 EXIF 标签编号值是原始数据。优点是快、依赖少缺点是标签编号不直观比如 36867 是拍摄时间34853 是 GPS 信息而且部分厂商的私有标签会被忽略。exifread 则把标签编号映射成了可读名称比如EXIF DateTimeOriginal、GPS GPSLatitude还支持直接读取文件流不用先把图片加载进内存。我的组合策略是用 Pillow 做快速筛选和缩略图生成用 exifread 做完整的元数据提取和可读化输出。两者互补覆盖 95% 以上的消费级相机和手机照片。2.3 环境准备与依赖安装资源包里的脚本基于 Python 3.8核心依赖只有两个。建议在虚拟环境里操作避免和系统包冲突。# 创建虚拟环境并激活 python -m venv exif_env source exif_env/bin/activate # Windows 用 exif_env\Scripts\activate # 安装核心依赖 pip install Pillow exifread逻辑说明Pillow负责图像对象创建和基础 EXIF 字典获取exifread负责可读标签解析。参数上没有特殊要求但注意exifread在读取某些 HEIC 格式时需要额外安装pillow-heif这个后面避坑章节会细说。安装完成后用python -c import PIL, exifread; print(ok)验证没报错就说明环境通了。3. 动手写一个批量提取脚本从单张调试到目录遍历3.1 单张照片的字段提取与打印先写一个最小可用函数把一张照片的关键字段拎出来。这里用 exifread 做主解析因为它输出的键名直接可读。import exifread def extract_exif_single(file_path): 提取单张照片的 EXIF 关键字段 with open(file_path, rb) as f: tags exifread.process_file(f, detailsFalse) # detailsFalse 跳过缩略图等大字段 # 定义我们关心的字段映射 fields { DateTimeOriginal: 拍摄时间, Make: 相机厂商, Model: 相机型号, FNumber: 光圈值, ExposureTime: 曝光时间, ISOSpeedRatings: ISO, FocalLength: 焦距, GPS GPSLatitude: GPS纬度, GPS GPSLongitude: GPS经度 } result {} for tag_key, cn_name in fields.items(): if tag_key in tags: result[cn_name] str(tags[tag_key]) else: result[cn_name] None return result # 调试单张 if __name__ __main__: info extract_exif_single(test.jpg) for k, v in info.items(): print(f{k}: {v})逻辑说明exifread.process_file的detailsFalse参数很关键它告诉库跳过 MakerNote 和缩略图数据只解析标准 IFD速度能快 3 到 5 倍。字段映射表里DateTimeOriginal是拍摄时间和文件修改时间不是一回事GPS GPSLatitude返回的是分数形式的列表比如[39, 54, 1234/100]需要额外转换才能变成十进制度数。参数上如果你需要厂商私有标签把details设为True但解析时间会明显增加。3.2 批量遍历目录与结果导出 CSV单张跑通后扩展到目录遍历。这里要处理子目录、非图片文件过滤和异常捕获。import os import csv from pathlib import Path def batch_extract(root_dir, output_csv): 遍历目录提取所有 JPEG/TIFF 的 EXIF 并写入 CSV supported_ext {.jpg, .jpeg, .tif, .tiff} all_records [] for dirpath, _, filenames in os.walk(root_dir): for fname in filenames: ext Path(fname).suffix.lower() if ext not in supported_ext: continue full_path os.path.join(dirpath, fname) try: info extract_exif_single(full_path) info[文件路径] full_path all_records.append(info) except Exception as e: # 记录失败文件不中断整体流程 all_records.append({文件路径: full_path, 错误: str(e)}) # 写入 CSV用 utf-8-sig 避免 Excel 打开乱码 if all_records: keys all_records[0].keys() with open(output_csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnameskeys) writer.writeheader() writer.writerows(all_records) print(f处理完成共 {len(all_records)} 条记录) # 使用示例 batch_extract(./photos, ./exif_output.csv)逻辑说明os.walk递归遍历所有子目录supported_ext集合过滤掉 PNG、GIF 等通常不含 EXIF 的格式。异常捕获放在单文件级别一张图损坏不会导致整个批次失败。CSV 编码用utf-8-sig这是血泪经验——用普通utf-8写中文Excel 打开会乱码。参数上root_dir建议用绝对路径避免相对路径在不同工作目录下解析错误。输出 CSV 的列顺序由第一条记录的键决定如果不同照片字段不一致后续记录可能缺列这是正常现象。3.3 GPS 坐标的十进制转换GPS 字段原始值是度、分、秒的分数形式直接存 CSV 没法用。需要写一个转换函数。def gps_to_decimal(gps_value): 将 EXIF GPS 分数列表转为十进制度数 if not gps_value or len(gps_value) ! 3: return None # exifread 返回的是 Ratio 对象需要转 float degrees float(gps_value[0].num) / float(gps_value[0].den) minutes float(gps_value[1].num) / float(gps_value[1].den) seconds float(gps_value[2].num) / float(gps_value[2].den) return degrees minutes / 60 seconds / 3600 # 在 extract_exif_single 中调用 # 注意GPSLatitude 需要配合 GPSLatitudeRef 判断南北逻辑说明EXIF 的 GPS 坐标是三个分数分别代表度、分、秒。Ratio对象有num和den属性相除得到浮点数。转换公式是度 分/60 秒/3600。但这里有个隐藏坑GPSLatitudeRef和GPSLongitudeRef决定正负号N 和 E 为正S 和 W 为负。很多脚本只转数值不读 Ref导致南半球和西半球的坐标全错。参数上如果gps_value不是三个元素直接返回 None避免索引越界。4. 避坑与排查EXIF 提取中五个让人翻车的细节4.1 现象读取某些手机照片返回空字典原因iPhone 默认用 HEIC 格式存储Pillow 和 exifread 原生都不支持。解决安装pillow-heif并在代码开头注册。from pillow_heif import register_heif_opener register_heif_opener()注册后 Pillow 就能打开 HEICexifread 也能通过 Pillow 的桥接读取。注意pillow-heif需要单独pip install不包含在 Pillow 里。4.2 现象拍摄时间字段是空的但相机明明有日期原因部分厂商把时间写在DateTime而不是DateTimeOriginal或者写在 MakerNote 里。解决按优先级回退读取。def get_best_datetime(tags): for key in [EXIF DateTimeOriginal, Image DateTime, EXIF DateTimeDigitized]: if key in tags: return str(tags[key]) return NoneDateTimeOriginal是原始拍摄时间DateTimeDigitized是数字化时间Image DateTime是文件修改时间。优先用第一个没有就往后找。4.3 现象批量处理几百张图后内存暴涨原因exifread.process_file默认会加载缩略图数据大图缩略图可能几 MB。解决始终传detailsFalse或者用stop_tag参数限制只读到 ExifIFD。tags exifread.process_file(f, detailsFalse, stop_tagExifIFD)stop_tag告诉解析器读到指定标签就停减少不必要的遍历。实测在 1000 张 12MP 照片上内存占用从 2GB 降到 200MB 以内。4.4 现象CSV 里中文列名在 Excel 中乱码原因Excel 默认用 GBK 打开 CSV而文件是 UTF-8。解决写文件时用encodingutf-8-sig这个编码会在文件头加 BOMExcel 能正确识别。如果已经生成了乱码文件用记事本打开另存为 ANSI 也能救回来但批量场景下不如一开始就写对。4.5 现象GPS 坐标转换后位置偏移几百米原因EXIF 的 GPS 坐标基于 WGS84 坐标系而国内地图服务用的是 GCJ02 或 BD09。解决如果只是归档存 WGS84 原始值即可如果要在地图上展示需要做坐标系转换。常见做法是调用现成的坐标转换库比如coord-convert但注意转换只针对国内区域国外坐标不需要转。这个坑在取证场景下尤其致命差几百米可能就指向完全不同的地点。5. 进阶技巧用 Pillow 做 EXIF 完整性校验与批量清洗5.1 校验照片是否被二次编辑过EXIF 里有一个Software标签如果值包含 “Photoshop”、“Snapseed”、“美图秀秀” 等说明图片被编辑过。但更可靠的判断是看DateTimeOriginal和DateTime是否一致以及Make/Model是否存在。我一般会写一个校验函数给每张图打一个“可信度”标签。def check_integrity(tags): 返回图片 EXIF 完整性评分 score 0 if EXIF DateTimeOriginal in tags: score 40 if Image Make in tags and Image Model in tags: score 30 if EXIF FNumber in tags and EXIF ExposureTime in tags: score 20 if GPS GPSLatitude in tags: score 10 return score # 评分低于 50 的图片大概率是截图或经过重度压缩逻辑说明原始相机照片通常包含完整的拍摄参数评分会接近 100。截图或社交平台下载的图片EXIF 被剥离评分很低。这个评分不是绝对标准但能快速筛选出可疑文件。参数上各字段权重可以根据业务调整比如做地理信息分析就把 GPS 权重调高。5.2 批量清洗 EXIF 并保留关键字段有时候你需要把照片发给别人但不想暴露 GPS 和相机序列号。Pillow 可以重写 EXIF但直接删除所有元数据会丢失拍摄时间。我的做法是读取原始 EXIF只保留时间和相机型号其余清空再写回新文件。from PIL import Image def clean_exif(input_path, output_path): 清洗 EXIF只保留拍摄时间和相机型号 img Image.open(input_path) exif img.getexif() # 新建一个空 EXIF只写入指定标签 new_exif Image.Exif() # 36867 是 DateTimeOriginal272 是 Model if 36867 in exif: new_exif[36867] exif[36867] if 272 in exif: new_exif[272] exif[272] img.save(output_path, exifnew_exif) print(f已清洗: {output_path}) # 批量处理 for f in Path(./photos).glob(*.jpg): clean_exif(f, f./cleaned/{f.name})逻辑说明Image.Exif()创建一个空 EXIF 对象通过标签编号写入。36867 对应拍摄时间272 对应相机型号。保存时传exifnew_exifPillow 会替换原始元数据。注意这个操作会重新编码 JPEG画质有轻微损失如果原图是高质量存档建议先备份。参数上quality默认是 75可以显式设为 95 减少损失。5.3 用exiftool做交叉验证Python 库偶尔会漏掉一些厂商私有标签我习惯用exiftool做交叉验证。它不是 Python 库是独立的命令行工具支持几乎所有元数据格式。# 安装后用 JSON 格式输出所有标签 exiftool -j -G -a photo.jpg exif_dump.json # 只看 GPS 相关 exiftool -gps:all photo.jpg-j输出 JSON-G显示标签分组-a显示重复标签。这个命令在排查“为什么 Python 读不到某个字段”时特别有用能确认字段到底存不存在。如果 exiftool 也读不到那就是图片本身没有这个信息不是代码问题。从那以后我每次做批量提取都会先用 exiftool 抽三张样本做基准再跑 Python 脚本对比结果确认一致才全量处理。这个习惯帮我省掉了至少两次因为库版本差异导致的数据错漏。希望帮到你。本文还有配套的精品资源点击获取