源码解读③:不依赖 PIL 手写 EXIF 解析——前任.skill 照片时间线提取原理 源码解读③不依赖 PIL 手写 EXIF 解析——前任.skill 照片时间线提取原理【免费下载链接】ex-skill前任 skill项目地址: https://gitcode.com/gh_mirrors/exsk/ex-skill前任.skillex-skill是一个把前任蒸馏成可对话 AI Skill 的开源项目而 photo_analyzer.py 负责从照片的 EXIF 元数据中提取拍摄时间、生成一张照片时间线。本篇源码解读带你读懂它如何不依赖 PIL / Pillow仅用 Python 标准库手写 EXIF 解析几十行代码就从 JPEG 的原始字节里钓出了拍摄日期 为什么要手写 EXIF 解析大多数同学提取照片拍摄时间第一反应是from PIL import Image或装个piexif。但前任.skill 选择了另一条路只 import 标准库os、struct、re、datetime、pathlib。这样设计带来三个好处零安装用户 clone 下来就能跑photo_analyzer.py不需要任何pip install。跨平台稳不挑 Python 版本也不受 Pillow 编译、架构兼容的影响。职责单一它只关心这张照片是什么时候拍的不解析照片像素内容内容分析交给 Claude 的Read工具所以能保持极轻量。隐私说明该工具只提取 EXIF 中的日期等元数据不读取、不上传照片内容所有处理都在本地完成。requirements.txt里其实把 Pillow 作为可选依赖注释掉了仅在需要更好支持 HEIC / TIFF 时才启用# Optional: Better EXIF support for more image formats (HEIC, TIFF, etc.) # Pillow10.0.0 参见 requirements.txt先看懂 JPEG 结构SOI、APP1 与 EOI手写 EXIF 解析的前提是理解 JPEG 文件的分段布局。一个 JPEG 由若干**段segment**拼接而成每段都以0xFF开头的标记marker开始标记字节名称含义FF D8SOIStart Of Image文件起始标记FF E1APP1应用段EXIF 信息通常藏在这里FF DASOSStart Of Scan真正的图像数据从这里开始FF D9EOIEnd Of Image文件结束标记每段标记之后紧跟一个2 字节大端长度它包含自身这 2 个字节。所以一段的实际载荷长度是length - 2。理解了这个结构整个解析逻辑就清晰了从 SOI 开始一段一段往后跳直到跳进 APP1把里面的字节读出来找日期。核心逐段扫描的 while 循环整个找 EXIF的过程浓缩在 extract_exif_date() 里。先看它如何校验文件头并逐段游走with open(file_path, rb) as f: # 1) 校验 JPEG 起始标记不是 JPEG 直接放弃 if f.read(2) ! b\xff\xd8: return None while True: marker f.read(2) if marker[0] ! 0xFF: break if marker[1] 0xE1: # 2) 命中 APP1EXIF 在这 length struct.unpack(H, f.read(2))[0] exif_data f.read(length - 2) ... # 3) 在 exif_data 里找日期 elif marker[1] in (0xD9, 0xDA): # 4) 到 EOI / SOS结束 break else: # 5) 其他段读长度并跳过 length struct.unpack(H, f.read(2))[0] f.seek(length - 2, 1)三个关键细节struct.unpack(H, ...)表示大端网络字节序H表示无符号短整型2 字节正好读出段的长度。f.seek(length - 2, 1)1代表相对当前位置把文件指针一次性跳过无关段效率很高。外层try/except直接pass遇到任何异常比如非 JPEG、文件损坏都优雅地返回None绝不中断整个扫描。这段逻辑是全文的骨架对应 photo_analyzer.py。用正则从原始字节里钓出拍摄日期有意思的地方来了。拿到exif_data后代码并没有老老实实去解析 TIFF 的 IFD 目录结构而是直接在原始字节里正则匹配日期串# DateTimeOriginal 在 EXIF 里就是 YYYY:MM:DD HH:MM:SS date_pattern rb\d{4}:\d{2}:\d{2} \d{2}:\d{2}:\d{2} matches re.findall(date_pattern, exif_data) if matches: return matches[0].decode(ascii) 参见 photo_analyzer.py为什么这样能成因为 EXIF 里的拍摄时间标签DateTimeOriginaltag0x9003本身就是按YYYY:MM:DD HH:MM:SS这个 ASCII 格式存储的。与其费劲解析二进制结构不如用一行正则钓出第一处匹配——简单、鲁棒、零依赖。这是典型的够用就好工程取舍。EXIF 拿不到时的兜底文件修改时间现实里很多照片会被压缩、转发EXIF 信息被抹掉。get_photo_date() 为此设计了一个降级兜底EXIF 优先拿不到就退回文件的修改时间。exif_date extract_exif_date(str(file_path)) if exif_date: return datetime.strptime(exif_date, %Y:%m:%d %H:%M:%S).strftime(%Y-%m-%d) # 兜底文件修改时间 mtime file_path.stat().st_mtime return datetime.fromtimestamp(mtime).strftime(%Y-%m-%d)这一层保证了时间线永远不会空——即便所有 EXIF 都丢了照片依然能按修改时间排进时间轴里。按日期分组生成 Markdown 时间线日期拿到后剩下的就是整理成人类能读的表格scan_photos() 递归遍历目录按扩展名过滤出图片支持的格式 覆盖 jpg / heic / webp / tiff 等。group_by_date() 用defaultdict按日期聚合并排序。format_output() 输出 Markdown每天最多列 10 张避免一天几百张刷屏。跑完你得到的时间线长这样# 照片时间线 目录~/Photos/with_her 总照片数128 跨度2022-03-14 ~ 2024-09-02 --- ## 2022-03-145 张 - IMG_0021.jpg3102 KB - IMG_0022.jpg2871 KB ... ## 2022-08-0212 张 - ... 还有 2 张时间线本身不含任何照片内容只回答一个问题这些照片分别属于哪一天。后续再由 AI 按日期回看具体照片补全记忆。三条命令跑通照片时间线 想亲眼看看它工作克隆仓库后对着一个照片文件夹执行即可# 克隆仓库本地只读使用 git clone https://gitcode.com/gh_mirrors/exsk/ex-skill # 扫描照片目录把时间线写到文件 python3 ex-skill/tools/photo_analyzer.py --dir ~/Photos/with_her --output /tmp/photo_timeline.txt # 直接打印到终端 python3 ex-skill/tools/photo_analyzer.py --dir ~/Photos/with_her在 Claude Code 里它被 SKILL.md 的方式 C照片流程调用时间线生成后再由Read工具读回来。相关源码地图photo_analyzer.py 只是 tools/ 下七把瑞士军刀之一它们共同把各类原材料解析成 AI 可用的文本模块作用tools/photo_analyzer.py照片 EXIF 元数据 → 时间线本篇主角tools/wechat_parser.py微信聊天记录解析tools/imessage_parser.pyiMessage / chat.db 解析tools/sms_parser.py短信解析tools/social_media_parser.py微博 / 豆瓣 / 小红书 / Instagram 解析tools/skill_writer.py生成与管理 Skill 文件tools/version_manager.py版本存档与回滚时间线最终会汇入 prompts/memories_analyzer.md 的关系时间线维度成为共同记忆的一部分示例见 exes/example_xiaomei/memories.md。小结不依赖 PIL 的手写 EXIF 解析靠的是一条清晰思路——按 JPEG 标记逐段游走 → 在 APP1 里正则钓日期 → 拿不到就用修改时间兜底 → 按日期分组出时间线。代码很短却把零依赖、鲁棒、够用三件事都做到了这正是它值得被单独解读的原因 ✨【免费下载链接】ex-skill前任 skill项目地址: https://gitcode.com/gh_mirrors/exsk/ex-skill创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考