照片EXIF信息提取实战:拍摄时间、机型与GPS解析
简介这是一份面向摄影爱好者、图像处理初学者及需要批量读取照片元数据用户的EXIF信息提取工具源码包基于C#与.NET开发解决手动逐张查看拍摄参数效率低、难以批量导出分析的问题。压缩包共46个文件约1.6MB包含cs源码、dll依赖库、xml与config配置、exe可执行程序、nupkg包及sln解决方案等其中MetadataExtractor与XmpCore组件负责解析EXIF与XMP数据Form1等窗体代码构成图形界面便于二次开发或直接运行。已有800人学习下载。通过该资源可掌握EXIF中拍摄时间、光圈、快门、ISO、GPS经纬度及镜头型号等字段的读取与导出思路理解批量处理文件夹、结果保存为文本的完整流程并借助项目结构快速定位界面逻辑与解析库调用方式适合作为图像元数据处理的入门实践与工具改造基础。1. 照片 EXIF 信息提取从一张“干净”的图里挖出拍摄时间、机型和 GPS你拿到一张照片右键属性里只看到“修改日期”但客户坚持说这张图是上周三下午在城东拍的。这时候能救你的不是肉眼比对而是照片里那层看不见的元数据——EXIF。它记录了拍摄时间、设备型号、光圈快门、甚至经纬度。做影像归档、二手设备验机、内容审核、保险定损都会碰到“照片会说话”的需求。这份资源就是围绕“提取照片 EXIF 信息”整理的一套可复现方案适合需要批量处理照片元数据的开发者、运维和数字取证入门者。下面从原理到代码把能踩的坑一次讲透。2. EXIF 到底藏在哪JPEG 段结构、TIFF 标签与解析选型2.1 为什么改后缀名骗不过 EXIF 读取很多人以为把.jpg改成.png就能抹掉拍摄信息实际上 EXIF 是嵌在 JPEG 文件内部的 APP1 段里跟扩展名无关。JPEG 由多个标记段组成每个段以0xFF加标记码开头APP1 段标记码0xE1专门存放 EXIF 数据。这段数据遵循 TIFF 格式组织开头是字节序标记II表示小端MM表示大端接着是 IFD图像文件目录偏移量每个 IFD 条目包含标签 ID、数据类型、数量、值或值偏移。常见标签如0x0110是相机型号0x9003是原始拍摄时间0x8825是 GPS 信息 IFD 指针。理解这个结构的意义在于当你用某些“一键去元数据”工具处理后APP1 段可能被整体删除但文件里其他段如 APP0 的 JFIF还在所以不能靠“有没有 APP1”来判断是否处理过。更稳妥的做法是直接解析 IFD 条目看关键标签是否存在。2.2 选型Pillow、ExifRead 还是 exiftoolPython 生态里常见三条路Pillow安装简单Image.getexif()能拿到大部分基础标签但对 GPS 嵌套 IFD 和厂商私有标签支持有限遇到损坏文件容易抛异常。ExifRead纯 Python 实现对 TIFF/JPEG 兼容性好能直接输出标签字典GPS 经纬度会转成度分秒格式适合快速脚本。exiftoolPerl 写的命令行工具标签库最全支持 RAW、HEIC、视频但需要额外安装二进制批量调用有进程开销。我一般这样选临时抽查用 ExifRead 写十行脚本生产环境批量归档用 exiftool 配合-json输出再用 Python 消费如果项目不允许装外部二进制就用 Pillow 加自定义 IFD 解析兜底。下面给出 ExifRead 和 Pillow 两种可抄作业的实现。2.3 用 ExifRead 提取基础标签与 GPS先安装依赖pip install exifread然后写一个提取脚本import exifread def read_exif_basic(path): with open(path, rb) as f: tags exifread.process_file(f, detailsFalse) # 只取常用字段避免输出过长 keys [ Image Make, Image Model, EXIF DateTimeOriginal, EXIF ExposureTime, EXIF FNumber, EXIF ISOSpeedRatings, GPS GPSLatitude, GPS GPSLatitudeRef, GPS GPSLongitude, GPS GPSLongitudeRef ] result {} for k in keys: if k in tags: result[k] str(tags[k]) return result if __name__ __main__: info read_exif_basic(sample.jpg) for k, v in info.items(): print(f{k}: {v})逻辑说明process_file的detailsFalse会跳过 MakerNote 等大块私有数据加快速度GPS GPSLatitude输出格式是[度, 分, 秒]的 Ratio 列表需要自己转成十进制。参数上如果照片没有 GPS 标签字典里就不会出现对应键不要用tags[GPS GPSLatitude]直接取否则 KeyError。2.4 用 Pillow 解析嵌套 IFD 与 GPS 转换Pillow 从 6.0 开始提供getexif()但 GPS 信息在GPSInfo子 IFD 里需要二次读取from PIL import Image from PIL.ExifTags import TAGS, GPSTAGS def convert_to_degrees(value): # value 是 (度, 分, 秒) 的 IFDRational 元组 d float(value[0]) m float(value[1]) s float(value[2]) return d (m / 60.0) (s / 3600.0) def read_exif_pillow(path): img Image.open(path) exif img.getexif() result {} for tag_id, val in exif.items(): tag TAGS.get(tag_id, tag_id) result[tag] val # 读取 GPS 子 IFD gps_ifd exif.get_ifd(0x8825) if gps_ifd: gps_data {} for tag_id, val in gps_ifd.items(): tag GPSTAGS.get(tag_id, tag_id) gps_data[tag] val if GPSLatitude in gps_data and GPSLatitudeRef in gps_data: lat convert_to_degrees(gps_data[GPSLatitude]) if gps_data[GPSLatitudeRef] S: lat -lat result[GPSLatitudeDecimal] lat if GPSLongitude in gps_data and GPSLongitudeRef in gps_data: lon convert_to_degrees(gps_data[GPSLongitude]) if gps_data[GPSLongitudeRef] W: lon -lon result[GPSLongitudeDecimal] lon return result逻辑说明get_ifd(0x8825)是 Pillow 提供的 GPS IFD 入口0x8825就是 GPSInfo 标签 ID。convert_to_degrees把度分秒转成十进制注意南纬和西经要取负值。参数上如果照片是 PNG 或经过社交平台压缩getexif()可能返回空字典这时候不要继续调get_ifd先判断exif是否为空。2.5 批量处理与输出格式选择单张读取只是玩具实际场景往往是几百张照片。批量脚本要处理三件事遍历目录、跳过非图片文件、把结果落成 CSV 或 JSON。下面是一个 CSV 输出示例import csv import os from exifread import process_file def batch_extract(root_dir, out_csv): with open(out_csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([filename, datetime, make, model, gps_lat, gps_lon]) for dirpath, _, files in os.walk(root_dir): for name in files: if not name.lower().endswith((.jpg, .jpeg, .tiff)): continue full os.path.join(dirpath, name) try: with open(full, rb) as img: tags process_file(img, detailsFalse) row [ full, str(tags.get(EXIF DateTimeOriginal, )), str(tags.get(Image Make, )), str(tags.get(Image Model, )), str(tags.get(GPS GPSLatitude, )), str(tags.get(GPS GPSLongitude, )) ] writer.writerow(row) except Exception as e: # 损坏文件记录到单独日志不中断批量 print(f[skip] {full}: {e}) if __name__ __main__: batch_extract(./photos, ./exif_report.csv)逻辑说明os.walk递归遍历endswith过滤扩展名tags.get避免 KeyError异常捕获保证单张损坏不影响整体。参数上如果照片量极大可以把detailsFalse改成detailsTrue但只取特定标签不过 ExifRead 没有按标签过滤的选项更高效的方式是换 exiftool 的-fast模式。3. 避坑与排查为什么你读不到时间、GPS 和机型3.1 现象DateTimeOriginal 为空但文件属性里有拍摄时间原因Windows 资源管理器显示的“拍摄日期”可能来自文件系统时间或 XMP 字段不是 EXIF 的DateTimeOriginal。社交平台导出、截图工具、部分手机“优化存储”会剥离 EXIF 或只保留 XMP。解决先确认文件是否真的含 APP1 段。用十六进制查看器搜0xFFE1如果没有说明 EXIF 已被删除只能从 XMP 侧信道读取或者接受数据丢失。不要用文件属性时间冒充 EXIF 时间。3.2 现象GPS 经纬度读出来是[0, 0, 0]或极大值原因部分设备在关闭定位时仍写入 GPS IFD但值为零另一些设备写入的是 Rational 类型分母为 0 导致除零异常。解决在convert_to_degrees里加分母判断遇到s.denominator 0直接返回 None。同时检查GPSLatitudeRef是否存在没有 Ref 的经纬度不可信。3.3 现象Pillow 报KeyError: 34665或get_ifd返回空原因34665是 ExifIFD 指针Pillow 在某些版本里对嵌套 IFD 支持不完整尤其是经过无损旋转的 JPEG。解决升级 Pillow 到 9.0 以上如果仍失败改用 ExifRead 或 exiftool。不要硬解_getexif()私有方法版本间行为不一致。3.4 现象批量处理时内存暴涨或速度极慢原因process_file默认会读取 MakerNote某些厂商的 MakerNote 有几十 KB几千张照片累积起来很可观。解决始终传detailsFalse如果确实需要 MakerNote单独用 exiftool 按需提取。另外批量脚本不要一次性把所有结果放内存边读边写 CSV。3.5 现象同一张照片在手机和电脑上读出的 EXIF 不一致原因手机相册应用可能动态注入或隐藏部分标签电脑端读的是原始文件。解决以原始文件字节为准不要信任任何“预览”层。做取证或归档时先对文件做哈希再解析 EXIF确保解析的是同一份数据。4. 进阶用 exiftool 做全格式覆盖与校验4.1 为什么还需要 exiftoolExifRead 和 Pillow 对 JPEG/TIFF 够用但遇到 HEIC、RAW、PNG 内嵌 eXIf 块、视频元数据就力不从心。exiftool 支持超过 200 种格式标签库更新快还能写回元数据。安装方式# Ubuntu/Debian sudo apt install libimage-exiftool-perl # macOS brew install exiftool4.2 用 JSON 输出对接现有流水线exiftool -j -DateTimeOriginal -Make -Model -GPSLatitude -GPSLongitude ./photos exif.json参数说明-j输出 JSON 数组后面跟标签名表示只取这些字段-GPSLatitude会自动带上 Ref 并转成十进制。如果目录里有非图片文件加-ext jpg -ext heic限定扩展名。输出 JSON 可以直接被 Pythonjson.load消费比解析文本更稳。4.3 校验 EXIF 是否被篡改一个实用技巧用 exiftool 的-validate选项检查文件结构一致性。exiftool -validate -warning -a ./suspect.jpg如果输出里有Warning: Invalid EXIF或Bad IFD说明元数据可能被手工修改过。注意-validate不能证明内容真实只能发现结构异常。我一般会结合文件哈希和拍摄时间做交叉验证如果 EXIF 时间早于文件系统创建时间且 GPS 与已知地点矛盾就标记为待人工复核。4.4 一个我常犯的错早期做批量归档时我直接用exiftool -all清空元数据结果把色彩配置文件也删了导致部分照片颜色发灰。后来每次清理前都先exiftool -ColorSpaceTags -o cleaned/保留必要标签。从那以后我每次处理照片元数据都强制走一遍“先备份、再校验、后写入”的流程希望帮到你。本文还有配套的精品资源点击获取