欢迎光临
我们一直在努力

你随手发的一张照片能泄露多少信息?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

本文定位:这是一篇隐私自查 / 数字取证 / 组织合规视角的技术文章,只讨论自己持有或已获书面授权的图片。文中所有示例图均为程序自造的测试图,坐标取自公开地标,不指向任何个人位置;所有命令均为读取、核查与清理,不含定位他人、跟踪、人肉或批量抓取社交平台图片做画像的方法。第八节单独列了法律与平台边界,请先读那一节再动手。
实测环境:Python 3.11.1 / Pillow 12.1.0 / piexif 1.1.3 / exifread 3.5.1 / exiftool 12.76 / Ubuntu 22.04。文中所有"输出"均为该环境真实运行结果,非示意数据。


一、先说结论:风险不在"能不能读",在"默认就写进去了"

先给结论,再拆解。

第一,读取门槛低到可以忽略。 一行 exiftool 或十行 Python 就能把一张照片的元数据全掏出来。所以任何"别人应该不会去看吧"的假设都不成立——防护不能建立在"读取很难"上。

第二,藏的不只是经纬度。 一张带完整 EXIF 的照片,通常同时写进去了设备厂商与型号、镜头、机身序列号、精确到秒的拍摄时间、编辑软件、作者与版权字段,以及一张裁剪前的完整缩略图。这些字段单独看不起眼,拼起来足以还原"谁、在什么时候、用什么设备、在哪个位置"。

第三,杠杆在"发出去之前",不在事后补救。 图片一旦以原文件形式离开你的设备(邮件附件、网盘分享、论坛附件、自建站、对象存储直链),元数据就跟着走了;事后再去删链接、改权限,副本已经不在你手里。真正有效的做法是把剥离元数据做成上传/发布流水线里的默认一步。

1.1 一张表看清"会泄露什么"

类别典型字段能推出什么风险
位置 GPSLatitude / GPSLongitude / GPSAltitude 拍摄点坐标,精度可达米级 高
时间 DateTimeOriginal / GPSTimeStamp / GPSDateStamp 精确到秒的拍摄时刻、作息规律 高
设备 Make / Model / LensModel / BodySerialNumber 机型、镜头,甚至机身唯一序列号 中高
归属 Artist / Copyright / UserComment 作者名、版权声明、自定义备注 中
软件 Software / ProcessingSoftware 手机固件版本、修图软件与版本 中
画面 ThumbnailImage(IFD1) 裁剪前的完整画面,可能含被裁掉的人脸/文字/屏幕 高(最易忽略)
参数 FNumber / ExposureTime / ISO / Flash 拍摄条件,辅助判断室内外、时间 低
编辑 XMP / IPTC / ICC 后期历史、关键词、标题、色彩配置 中

1.2 一个必须分清的前提:不是所有"发图"都会带 EXIF

这是最容易被自媒体文章写错的地方,先说准:

发出方式元数据通常是否保留说明
微信/朋友圈非原图、微博压缩图等 多数被剥离 平台会重编码、重建文件,EXIF 一般随之丢失
微信/邮件/网盘的「原图」 保留 传的是原始文件字节,元数据原样带走
论坛/博客附件、工单附件、对象存储直链 保留 服务端通常只做存储,不改写文件内容
自建站、CDN 直接托管 IMG_1234.jpg 保留 除非上传环节主动剥离
截图、屏幕录制 通常没有 GPS 系统截图一般不写相机元数据,但可能带软件/时间字段
HEIC / Live Photo / 视频 各自有独立容器 见第七节

所以准确的说法是:风险集中在「原图」和「文件原样对外」这两类场景,而不是"发张照片就会被定位"。搞清楚这一点,才不会要么过度恐慌、要么完全不管。


二、照片里到底藏了什么:EXIF 的结构

2.1 元数据不是一个段落,是几套互相独立的容器

很多人以为"EXIF 就是照片里的一小块信息",实际上一个图片文件里可能同时存在下面几套东西,它们各写各的、各删各的:

容器谁写的常见内容清理时容易被漏掉吗
EXIF(IFD0 / ExifIFD / GPS / IFD1) 相机、手机 设备、参数、时间、GPS、缩略图 一般会被清
XMP Adobe 系列、部分手机 编辑历史、标签、评级、裁剪记录 容易漏
IPTC 图片社、编辑软件 标题、作者、版权、关键词 容易漏
ICC Profile 相机 / 修图软件 色彩空间配置 常被误删(影响颜色)
JFIF / APP0 编码库 密度、版本 一般无害
文件本身 文件系统 文件名、mtime、路径 与图片内容无关但同样泄露

这也是为什么只删 GPS 是不够的:GPS 清了,XMP 里可能还留着一份位置,IPTC 里还留着作者名。

2.2 EXIF 内部的四个 IFD,用真实输出看最直观

用 exiftool -a -G1 -s 看分组(-G1 显示一级组名,-a 允许重复标签,-s 输出短名)。下面是对一张程序自造的测试图的真实输出节选:

[File] FileType : JPEG
[File] ExifByteOrder : Big-endian (Motorola, MM)
[JFIF] JFIFVersion : 1.01
[IFD0] Make : DemoCam
[IFD0] Model : DC-1000
[IFD0] Software : DemoFW 1.2
[IFD0] ModifyDate : 2026:02:11 09:32:18
[IFD0] Artist : luosiqiang
[IFD0] Copyright : demo only
[ExifIFD] ExposureTime : 1/125
[ExifIFD] FNumber : 1.8
[ExifIFD] ISO : 200
[ExifIFD] DateTimeOriginal : 2026:02:11 09:32:18
[ExifIFD] UserComment : self-made test image
[ExifIFD] SerialNumber : SN-DEMO-0001
[ExifIFD] LensModel : 24mm f/1.8
[GPS] GPSLatitudeRef : North
[GPS] GPSLatitude : 39 deg 54' 15.12"
[GPS] GPSLongitudeRef : East
[GPS] GPSLongitude : 116 deg 24' 26.64"
[GPS] GPSAltitude : 44 m
[GPS] GPSTimeStamp : 09:32:18
[GPS] GPSDateStamp : 2026:02:11
[IFD1] ThumbnailOffset : 693
[IFD1] ThumbnailLength : 1485
[IFD1] ThumbnailImage : (Binary data 1485 bytes, use -b option to extract)
[Composite] GPSPosition : 39 deg 54' 15.12" N, 116 deg 24' 26.64" E

四个 IFD 的分工:

IFD作用关键字段
IFD0(0th) 图像本身与设备基本信息 Make Model Software Artist Copyright ModifyDate
ExifIFD 拍摄参数与原始时间 DateTimeOriginal ExposureTime ISO LensModel SerialNumber
GPS IFD 定位信息 GPSLatitude GPSLongitude GPSAltitude GPSTimeStamp GPSDateStamp
IFD1(1st) 缩略图 ThumbnailOffset ThumbnailLength ThumbnailImage

[Composite] 是 exiftool 自己算出来的派生标签(比如把度分秒拼成 GPSPosition),文件里并不实际存在,别把它当成"又多存了一份"。

【图 1】

2.3 哪些格式能带 EXIF

格式能否带 EXIF备注
JPEG / TIFF ✅ 原生 最常见,APP1 段
HEIC / HEIF ✅ iPhone 默认格式,需支持 HEIF 的工具
PNG ✅(eXIf 块) 部分老工具读不到,容易误判"没有元数据"
WebP ✅(RIFF/EXIF) 支持程度依工具版本
RAW(CR2/NEF/ARW/DNG) ✅ 且字段极多 专业相机,常含机身序列号
GIF / BMP ❌ 基本不带 别在这类格式上白费劲
经过重编码的压缩图 ⚠️ 视实现 有的清空、有的复制部分字段,不要赌

2.4 最容易被忽略的一项:IFD1 缩略图

很多"隐私清理"教程只讲 GPS,但 IFD1 里那张缩略图才是隐蔽大户。它的原始用途是给相机屏幕和文件管理器快速预览用,所以往往是裁剪、修图之前的原始画面。

我实测了一个典型场景:把一张 1200×800 的图(角落写着一行文字)裁成 600×400 的"发布版",但保留带 IFD1 缩略图的 EXIF:

主图(已裁剪): (600, 400), 9360 bytes
导出缩略图 : (320, 213), 4669 bytes

用一行命令就能把缩略图抠出来:

exiftool -b -ThumbnailImage cropped.jpg > thumbnail.jpg

导出的 320×213 缩略图里,完整画面连同角落那行被"裁掉"的文字都还在。也就是说,你以为裁掉了敏感内容,只要 EXIF 没清,原画面还挂在文件里。这一条在第六节的清理方案里必须覆盖。


三、四条读取路径:从零配置到工程化

按投入从低到高排,四条路径各有适用场景。

路径上手成本适合能看到全貌吗
A 图形界面 零 临时看一两张 ❌ 只显示部分字段
B exiftool 装一个包 日常核查、批量、导出缩略图 ✅ 最全
C Python(Pillow / piexif / exifread) 装库 集成进脚本、自动化 ✅ 可编程
D 在线查看网站 零 —— ⚠️ 不推荐,见 3.4

3.1 路径 A:系统自带,先看个大概

系统操作能看到
Windows 右键文件 → 属性 → 详细信息 相机型号、拍摄时间、部分 GPS(Win10/11 常显示为度分秒)
macOS 预览 → 工具 → 显示检查器 → GPS 标签页 经纬度、海拔,可直接跳地图
iOS / Android 相册 → 详情 / 信息 拍摄时间、位置(部分机型)
Linux file、identify -verbose(ImageMagick) 基础信息

局限:图形界面通常只渲染一部分标准字段,XMP、IPTC、MakerNote、IFD1 缩略图这些基本看不到。所以它适合"确认有没有",不适合"确认清没清干净"。

3.2 路径 B:exiftool,事实标准

安装:

# Debian / Ubuntu
sudo apt-get install -y libimage-exiftool-perl
# macOS
brew install exiftool
# 验证
exiftool -ver # 本文实测:12.76

常用命令表(建议直接收藏):

目的命令
看全部 exiftool photo.jpg
看分组 + 短名(排查最常用) exiftool -a -G1 -s photo.jpg
只看 GPS exiftool -GPS* -s photo.jpg
GPS 输出十进制度 exiftool -GPSLatitude -GPSLongitude -n -s photo.jpg
自定义坐标格式 exiftool -c "%.6f" -GPSPosition photo.jpg
时间格式化 exiftool -d "%Y-%m-%d %H:%M:%S" -DateTimeOriginal photo.jpg
导出缩略图 exiftool -b -ThumbnailImage photo.jpg > thumb.jpg
批量导出成 CSV exiftool -csv -r ./photos > report.csv
批量按时间重命名 exiftool "-FileName<DateTimeOriginal" -d "%Y%m%d_%H%M%S%%-c.%%e" ./photos
只删 GPS exiftool -gps:all= -overwrite_original photo.jpg
全清 exiftool -all= -overwrite_original photo.jpg

小技巧:-n 会关闭"友好格式化",直接输出数值,写脚本时必加。实测:

$ exiftool -a -G1 -s -n -GPSLatitude -GPSLongitude -GPSAltitude exif_demo.jpg
[GPS] GPSLatitude : 39.9042
[Composite] GPSLatitude : 39.9042
[GPS] GPSLongitude : 116.4074
[Composite] GPSLongitude : 116.4074
[GPS] GPSAltitude : 44
[Composite] GPSAltitude : 44

不加 -n 时是 39 deg 54' 15.12" 这种度分秒文本,程序不好解析——这是新手写批处理时最常踩的坑。

3.3 路径 C:Python 三件套

先装库:

python3 -m pip install Pillow piexif exifread

三个库定位不同,别混着用:

库强项弱项适合
Pillow 零额外依赖、能同时处理像素 GPS 需自己按 tag 号解析 快速读、边读边处理图像
piexif 读写 EXIF 结构最完整,能无损删除 只管 EXIF 清理、构造、精确修改
exifread 容错好、字段友好、读 RAW 方便 只读、不擅长写 批量扫目录、脏文件容错
C1 · Pillow:十行拿到 GPS 原始值

from PIL import Image, ExifTags

GPS_IFD = 34853 # ExifTags.IFD.GPSInfo 的数值

with Image.open("photo.jpg") as im:
exif = im.getexif()
print("== IFD0 / 基础 ==")
for tag_id, value in exif.items():
name = ExifTags.TAGS.get(tag_id, tag_id)
print(f" {name:<20}: {value}")

gps = exif.get_ifd(GPS_IFD) # Pillow ≥ 8.2 支持
print("== GPS IFD ==")
for tag_id, value in gps.items():
name = ExifTags.GPSTAGS.get(tag_id, tag_id)
print(f" {name:<20}: {value}")

对同一张测试图的真实输出:

== IFD0 / 基础 ==
Make : DemoCam
Model : DC-1000
Software : DemoFW 1.2
DateTime : 2026:02:11 09:32:18
Artist : luosiqiang
Copyright : demo only
GPSInfo : 392
ExifOffset : 218
== GPS IFD ==
GPSLatitudeRef : N
GPSLatitude : (39.0, 54.0, 15.12)
GPSLongitudeRef : E
GPSLongitude : (116.0, 24.0, 26.64)
GPSAltitudeRef : b'\\x00'
GPSAltitude : 44.0
GPSTimeStamp : (9.0, 32.0, 18.0)
GPSDateStamp : 2026:02:11

注意 GPSInfo : 392 是个偏移量而不是数据本身,必须 get_ifd() 进去才拿得到真的标签——这个数字骗过不少人。

C2 · piexif:能读能删,结构最清楚

import piexif

d = piexif.load("photo.jpg")
for ifd in ("0th", "Exif", "GPS", "1st"):
print(f"[{ifd}] {len(d[ifd])} tags")
print("缩略图字节数:", len(d.get("thumbnail") or b""))
print("厂商:", d["0th"].get(piexif.ImageIFD.Make))
print("原始时间:", d["Exif"].get(piexif.ExifIFD.DateTimeOriginal))
print("GPS 原始:", d["GPS"].get(piexif.GPSIFD.GPSLatitude))

真实输出:

[0th] 8 tags
[Exif] 7 tags
[GPS] 8 tags
[1st] 3 tags
缩略图字节数: 1485
厂商: b'DemoCam'
原始时间: b'2026:02:11 09:32:18'
GPS 原始: ((39, 1), (54, 1), (378, 25))

注意 GPS 是三个有理数(分子, 分母),不是浮点数。(378, 25) 表示 15.12 秒。下一节专门讲怎么换算。

C3 · exifread:批量扫目录最省事

import exifread

with open("photo.jpg", "rb") as fh:
tags = exifread.process_file(fh, details=False)

for k in ("Image Make", "Image Model", "EXIF DateTimeOriginal",
"GPS GPSLatitude", "GPS GPSLatitudeRef", "GPS GPSLongitude"):
print(f"{k:<26}: {tags.get(k)}")

details=False 会跳过缩略图等大块内容,扫几千张图时明显更快;文件损坏时它也比另外两个更不容易抛异常。

C4 · 一个可以直接抄走的"发图前自查"函数

把上面的能力收成一个函数,按风险分级打印,发图前跑一遍就知道这张能不能发:

from PIL import Image, ExifTags
from pathlib import Path

HIGH = {"GPSInfo", "GPSLatitude", "GPSLongitude", "GPSDateStamp", "GPSTimeStamp",
"DateTimeOriginal", "DateTime", "Artist", "Copyright",
"BodySerialNumber", "SerialNumber", "UserComment"}
MID = {"Make", "Model", "Software", "LensModel", "ProcessingSoftware"}

def audit(path):
p = Path(path)
out = {"高危": [], "中危": [], "其他": []}
with Image.open(p) as im:
ex = im.getexif()
named = {ExifTags.TAGS.get(k, str(k)): v for k, v in ex.items()}
gps = ex.get_ifd(34853) if 34853 in ex else {}
# Pillow 用 GPSTAGS 映射 GPS IFD 的标签号
named.update({ExifTags.GPSTAGS.get(k, str(k)): v
for k, v in gps.items()})
# 注意:IFD1 缩略图 Pillow 读不到,需按 6.2 用 exiftool 单独复核

for k, v in named.items():
if k in HIGH:
out["高危"].append((k, v))
elif k in MID:
out["中危"].append((k, v))
else:
out["其他"].append((k, v))

print(f"📄 {p.name}")
for level in ("高危", "中危", "其他"):
print(f" [{level}] {len(out[level])} 项")
for k, v in out[level][:6]:
print(f" – {k}: {str(v)[:60]}")
return out

if __name__ == "__main__":
import sys
for f in sys.argv[1:]:
audit(f)

对同一张测试图的真实输出(节选):

📄 exif_demo.jpg
[高危] 4 项
– GPSInfo: 392
– DateTime: 2026:02:11 09:32:18
– Copyright: demo only
– Artist: luosiqiang
[中危] 3 项
– Make: DemoCam
– Model: DC-1000
– Software: DemoFW 1.2
[其他] 11 项
– GPSLatitude: (39.0, 54.0, 15.12)
– GPSLongitudeRef: E

GPSInfo: 392 里的 392 是 GPS IFD 在文件中的偏移量,不是坐标值;函数内部用 get_ifd(34853) 已经把真实标签取出来并合并了,所以看到这一项就可以判定"这张图带定位信息"。

这段是审查骨架,生产环境建议再加上 XMP/IPTC 检查(见 3.2 的 -a -G1 全量输出)和统一的清理动作,别只靠 Pillow。

3.4 路径 D:在线 EXIF 查看网站——本文不推荐

搜"EXIF 查看"能搜到一堆网页工具,点一下确实方便,但有个绕不过去的逻辑问题:

你把一张可能含敏感信息的照片,上传到了一个第三方服务器,就为了看看它含不含敏感信息。

这本身就是一次数据外发。对方是否留存、是否用于训练、传输是否加密,你都无法验证。所以:

  • 自查用本地 exiftool 或本地脚本,零外发;
  • 确需用在线工具,只传已经清理过、不含任何敏感内容的样图;
  • 涉及工作素材、客户资料、证件、未公开现场照片时,一律本地处理。

四、GPS 怎么从"三个分数"变成地图坐标

这是全文最常被搜到的一段,也是最容易算错的一段。

【图 2】

4.1 存储格式:三个 rational + 两个方向参考

EXIF 里纬度/经度不是浮点数,而是度、分、秒各一个有理数,再加一个方向字母:

GPSLatitudeRef : b'N'
GPSLatitude : ((39, 1), (54, 1), (378, 25)) # 39° 54' 15.12"
GPSLongitudeRef : b'E'
GPSLongitude : ((116, 1), (24, 1), (666, 25)) # 116° 24' 26.64"

(378, 25) = 378/25 = 15.12 秒。方向由 GPSLatitudeRef(N/S)和 GPSLongitudeRef(E/W)决定,南纬、西经要取负——这一步漏了,坐标会跑到地球另一边。

4.2 换算代码(可直接跑)

import piexif

def rational_to_float(r):
"""piexif 的 (numerator, denominator) → float"""
return r[0] / r[1] if isinstance(r, tuple) else float(r)

def dms_to_decimal(dms, ref):
"""[(度), (分), (秒)] + 方向 → 十进制度"""
deg = rational_to_float(dms[0])
minute = rational_to_float(dms[1])
second = rational_to_float(dms[2])
val = deg + minute / 60 + second / 3600
return -val if ref in (b"S", b"W", "S", "W") else val

def read_gps(path):
g = piexif.load(path)["GPS"]
if piexif.GPSIFD.GPSLatitude not in g:
return None
lat = dms_to_decimal(g[piexif.GPSIFD.GPSLatitude],
g.get(piexif.GPSIFD.GPSLatitudeRef, b"N"))
lon = dms_to_decimal(g[piexif.GPSIFD.GPSLongitude],
g.get(piexif.GPSIFD.GPSLongitudeRef, b"E"))
alt = None
if piexif.GPSIFD.GPSAltitude in g:
alt = rational_to_float(g[piexif.GPSIFD.GPSAltitude])
if g.get(piexif.GPSIFD.GPSAltitudeRef) == 1:
alt = -alt
return round(lat, 6), round(lon, 6), alt

if __name__ == "__main__":
print(read_gps("photo.jpg"))

对测试图的真实输出(print(read_gps(…)) 返回 (纬度, 经度, 海拔)):

read_gps() = (39.9042, 116.4074, 44.0)

对照一下换算过程,输入是 4.1 那三个有理数:

GPSLatitude 原始 : ((39, 1), (54, 1), (378, 25)) # 39° 54' 15.12"
GPSLatitudeRef : b'N'
deg=39 minute=54 second=378/25=15.12
→ 39 + 54/60 + 15.12/3600 = 39.904200

把 return 里的 round(…, 6) 去掉就是完整精度;做取证记录时建议保留原始 rational 值,避免反复舍入。

4.3 一个国内读者必踩的坑:WGS84 ≠ GCJ-02

EXIF 里的坐标是设备按 WGS84(GPS 原始坐标系)写入的。而国内主流地图服务用的是加密偏移后的坐标系:

地图 / 服务坐标系直接用 WGS84 会怎样
Google 地图(境外)、OSM WGS84 正确
高德、腾讯地图 GCJ-02("火星坐标") 偏移几百米
百度地图 BD-09(在 GCJ-02 上再偏一次) 偏移更大
设备 GPS 原始值 WGS84 基准

我实测了北京公开地标这一点(WGS84 116.407400, 39.904200)的转换结果:

EXIF 里存的是 WGS84 : lng=116.407400, lat=39.904200
转 GCJ-02(高德/腾讯) : lng=116.413642, lat=39.905603
不转换直接贴国内地图,约偏移: 东 533 m / 北 155 m / 直线约 555 m

555 米——足够让一个"定位到某栋楼"的结论变成"定位到隔壁街区"。写取证报告或做位置还原时,这个偏差必须说明,否则结论站不住。

标准转换函数(WGS84 → GCJ-02):

import math

def wgs84_to_gcj02(lng, lat):
a, ee = 6378245.0, 0.00669342162296594323

def tlat(x, y):
r = -100.0 + 2.0*x + 3.0*y + 0.2*y*y + 0.1*x*y + 0.2*math.sqrt(abs(x))
r += (20.0*math.sin(6.0*x*math.pi) + 20.0*math.sin(2.0*x*math.pi)) * 2.0/3.0
r += (20.0*math.sin(y*math.pi) + 40.0*math.sin(y/3.0*math.pi)) * 2.0/3.0
r += (160.0*math.sin(y/12.0*math.pi) + 320*math.sin(y*math.pi/30.0)) * 2.0/3.0
return r

def tlng(x, y):
r = 300.0 + x + 2.0*y + 0.1*x*x + 0.1*x*y + 0.1*math.sqrt(abs(x))
r += (20.0*math.sin(6.0*x*math.pi) + 20.0*math.sin(2.0*x*math.pi)) * 2.0/3.0
r += (20.0*math.sin(x*math.pi) + 40.0*math.sin(x/3.0*math.pi)) * 2.0/3.0
r += (150.0*math.sin(x/12.0*math.pi) + 300.0*math.sin(x/30.0*math.pi)) * 2.0/3.0
return r

dlat, dlng = tlat(lng – 105.0, lat – 35.0), tlng(lng – 105.0, lat – 35.0)
radlat = lat / 180.0 * math.pi
magic = 1 – ee * math.sin(radlat) ** 2
sqrtmagic = math.sqrt(magic)
dlat = (dlat * 180.0) / ((a * (1 – ee)) / (magic * sqrtmagic) * math.pi)
dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * math.pi)
return round(lng + dlng, 6), round(lat + dlat, 6)

生成地图链接时,按目标服务选坐标系:

lat, lon, _ = read_gps("photo.jpg")
g_lng, g_lat = wgs84_to_gcj02(lon, lat)
print("高德:", f"https://uri.amap.com/marker?position={g_lng},{g_lat}")
print("OSM :", f"https://www.openstreetmap.org/?mlat={lat}&mlon={lon}#map=16/{lat}/{lon}")

4.4 三个配套的注意点

  • GPSTimeStamp 是 UTC,DateTimeOriginal 通常是设备本地时间,两者可能差 8 小时。做时间线时先统一时区,别把"早上 9 点"读成"下午 5 点"。
  • GPSAltitudeRef:0 表示海平面以上,1 表示以下,别忘了取负。
  • 精度不是无限的:民用 GPS 常见几米到十几米误差,室内/高楼区更差;GPSHPositioningError 等字段(部分新机型有)会给估计误差。写结论时不要写成"精确到门牌号"。

五、三个实战场景(均为自查 / 授权取证)

5.1 场景一:发图前自查——十秒钟决定"这张能不能发"

最容易落地的用法。把 3.3 的 audit() 挂成一个 shell 别名或文件右键动作,发原图之前跑一下:

python3 exif_audit.py IMG_20260211_093218.jpg

输出会直接告诉你有没有高危字段。判定规则建议这样定:

输出动作
有 GPS* / Artist / Copyright / SerialNumber 先清理再发
只有 Make / Model / 曝光参数 视发布范围决定,对外建议一并清
有 IFD1 缩略图 必须清,见 2.4
什么都没有 可发,但仍建议确认是"真没有"而不是"工具没读到"

提醒:这里的"什么都没有"要用 exiftool -a -G1 -s 复核一次。PNG 的 eXIf 块、XMP 段,Pillow 不一定读得到(见第七节)。

5.2 场景二:授权取证里的批量时间线还原

这是合法取证里非常标准的一类用途:手上有一批已获授权的图片(比如自家监控导出、企业内部调查中依法取得的材料),需要还原事件先后顺序。

# 递归导出全部元数据到 CSV,时间统一格式
exiftool -csv -r -d "%Y-%m-%d %H:%M:%S" \\
-FileName -DateTimeOriginal -GPSLatitude -GPSLongitude \\
./evidence > timeline.csv

再用几行 Python 排序:

import csv
from datetime import datetime

rows = []
with open("timeline.csv", encoding="utf-8-sig") as fh:
for r in csv.DictReader(fh):
t = r.get("DateTimeOriginal", "").strip()
if not t:
continue
try:
dt = datetime.strptime(t, "%Y-%m-%d %H:%M:%S")
except ValueError:
continue
rows.append((dt, r["SourceFile"], r.get("GPSLatitude", ""),
r.get("GPSLongitude", "")))

for dt, name, lat, lon in sorted(rows)[:20]:
print(f"{dt} {name} ({lat}, {lon})")

取证场景的三条硬要求(比代码重要):

  • 授权链完整:谁授权、授权范围、授权期限要有书面记录;超出范围的材料不要碰。
  • 原件只读 + 哈希固定:先对原目录算 sha256sum,复制一份工作副本,所有分析在副本上做,原件不改动。
  • 过程可复现:命令、工具版本、时区设置、转换算法全部记进笔记,否则结论在复核时会被质疑。
  • 5.3 场景三:缩略图复核——你以为裁掉的,其实还在

    2.4 讲了原理,这里给完整复核流程:

    # 1) 看有没有缩略图
    exiftool -a -G1 -s photo.jpg | grep -i thumbnail
    # [IFD1] ThumbnailLength : 1485
    # [IFD1] ThumbnailImage : (Binary data 1485 bytes, use -b option to extract)

    # 2) 导出来
    exiftool -b -ThumbnailImage photo.jpg > thumb.jpg

    # 3) 看一眼(人眼判断有没有被裁掉的敏感内容)
    file thumb.jpg

    实测案例数据:

    主图(已裁剪): (600, 400), 9360 bytes
    导出缩略图 : (320, 213), 4669 bytes
    缩略图里仍能看到完整画面(含角落文字)

    结论:裁剪 ≠ 清除。任何"我把敏感区域裁掉就安全了"的判断都不成立,除非同时剥离 EXIF。

    5.4 顺带澄清一个反例

    不是所有图片都带 GPS。系统截图、大部分压缩后的聊天图、某些编辑软件导出的图通常没有定位信息。所以看到"照片能定位"不要反向推成"任何一张图都能定位"——先读,再判断,别凭印象下结论。这也是自查脚本存在的意义。


    六、清理与防护:把剥离做成默认动作

    6.1 三种清理方式,实测对比

    用同一张 26151 字节的测试图,三种方式各跑一遍,再用 exiftool 复核残留敏感标签数:

    方式命令 / 代码清理后体积残留敏感标签是否重编码保留 ICC
    原图 —— 26151 B 17 —— ✅
    piexif.remove() 见下 23993 B 0 ❌ 无损 ✅
    exiftool -all= 见下 23975 B 0 ❌ 无损 ⚠️ 默认会删,可加 -icc_profile:all 保留
    Pillow 重存 见下 23884 B 0 ✅ 有损 ❌ 丢失

    真实复核输出:

    原图 26151 bytes 残留敏感标签: 17
    piexif.remove() 23993 bytes 残留敏感标签: 0 无 ✅
    exiftool -all= 23975 bytes 残留敏感标签: 0 无 ✅
    Pillow 重存 23884 bytes 残留敏感标签: 0 无 ✅

    【图 3】

    方式 1 · piexif.remove()(无损,Python 内首选)

    import piexif

    piexif.remove("photo.jpg", "photo_clean.jpg") # 只删 EXIF 段,不改像素

    优点:不动像素、不重编码、画质零损失,适合脚本里逐张处理。

    局限:只管 EXIF,XMP / IPTC 需要另外处理。

    方式 2 · exiftool -all=(最彻底,批量首选)

    # 单张(原地覆盖,先备份!)
    exiftool -all= -overwrite_original photo.jpg

    # 批量,同时保留色彩配置避免偏色
    exiftool -all= -tagsfromfile @ -icc_profile:all \\
    -overwrite_original -r ./photos

    # 只删 GPS,保留其余参数(修图工作流常用)
    exiftool -gps:all= -overwrite_original photo.jpg

    -all= 会连 XMP、IPTC、缩略图一起清掉,是最不容易漏的一项。注意 -overwrite_original 会直接覆盖原文件,不加这个参数时 exiftool 会生成 photo.jpg_original 备份——生产环境建议先不加,确认没问题再删备份。

    方式 3 · Pillow 重存(不推荐,除非你本来就要转码)

    from PIL import Image

    im = Image.open("photo.jpg")
    im.save("photo_clean.jpg", "JPEG", quality=92) # 不传 exif= 即丢弃

    会重新编码,有画质损失,且 ICC 一并丢失可能偏色。只在你本来就要缩放/转格式时顺带用,不要专门拿它当清理手段。

    6.2 清理之后必须复核

    清理动作本身也可能失败(文件被占用、格式不支持、只读),所以清理 + 复核要成对出现:

    exiftool -a -G1 -s photo_clean.jpg | grep -Ei "GPS|Artist|Serial|Thumbnail|XMP|IPTC"
    # 期望:无输出

    把它写进脚本,失败就告警,别靠人眼。

    6.3 服务端:把剥离做成上传网关的默认一步

    个人自查靠自觉,组织级防护靠流程。一个最小可用的处理函数:

    from PIL import Image
    import piexif, io, hashlib

    def sanitize_upload(raw: bytes, *, max_side: int = 2048) -> bytes:
    """接收上传字节,返回已剥离元数据 + 可选缩放的 JPEG 字节。
    设计要点:原始字节不落盘;只把清理后的版本交给存储/CDN。"""
    im = Image.open(io.BytesIO(raw))
    im = im.convert("RGB") # 去掉 alpha / 调色板等边角情况

    if max(im.size) > max_side: # 顺带缩图,缩小攻击面
    im.thumbnail((max_side, max_side), Image.LANCZOS)

    buf = io.BytesIO()
    im.save(buf, "JPEG", quality=88, optimize=True) # 不写 exif / ICC 之外的元数据
    clean = buf.getvalue()

    # 复核:输出不含任何 EXIF / GPS 段,失败即拒绝入库(实测通过)
    leftover = piexif.load(clean)
    assert not leftover["GPS"] and not leftover["0th"] and not leftover["Exif"], \\
    "元数据清理失败,禁止入库"
    return clean

    实测:26151 bytes 输入 → 11348 bytes 输出,piexif.load() 六个 IFD 全为 0,复核断言通过。注意这个函数为了重编码会损失画质,且会丢 ICC;业务若必须保真,改用 6.1 的 exiftool -all=(无损)并在网关层调用,而不是在 Python 里转码。

    工程上四条建议:

  • 先剥离、再落盘:原始上传字节只存在于内存,对象存储里永远只有清理版。
  • CDN 只出清理版:不要同时留一个"原图直链",那等于防护白做。
  • 保留 ICC 要显式处理:一律丢弃可能让设计/摄影类业务偏色,需要就在转码后重新写入标准 sRGB。
  • 定期抽查:每周随机抽 N 张线上图片跑 exiftool,确认流水线没有被后来的改动绕过。
  • 6.4 拍摄侧:从源头关掉定位

    端操作
    iOS 设置 → 隐私与安全性 → 定位服务 → 相机 → 设为「永不」或「使用期间」按需
    Android 相机 App 设置里关「位置标签 / GPS 标记」;系统层面可收回相机定位权限
    相机 菜单里关「GPS 记录 / 地理标记」,外接 GPS 模块同理
    微信 / 邮件 / 网盘 发送时不选「原图」;确需原图,先清理再传
    截图 / 录屏 一般无 GPS,但注意画面内容本身,以及文件名的可识别信息

    6.5 发布侧清单(可直接抄进 SOP)

    • ☐ 发图前跑一次本地自查脚本,高危字段清零
    • ☐ 需要发原图时,先 exiftool -all= 再传
    • ☐ 复核一次清理结果(-a -G1 -s | grep,期望无输出)
    • ☐ 确认没有 IFD1 缩略图残留
    • ☐ 文件名不含姓名、工号、地址、日期等可识别信息(IMG_张三_20260211.jpg 这类要改)
    • ☐ 组织场景:确认上传网关已默认剥离,且线上抽查通过

    七、十个常见坑

    #坑正确做法
    1 「社交平台会帮我清,不用管」 不要赌。不同平台、不同版本、原图/非原图行为不一致,一律自己清
    2 清完发现颜色变了 -all= 会连带删 ICC;需要时用 -tagsfromfile @ -icc_profile:all 保留或重写 sRGB
    3 换 HEIC / PNG / WebP 后工具读不到 换支持该容器的工具(exiftool 覆盖最全),别误判成"没有元数据"
    4 只处理了 .jpg,漏了视频 / Live Photo MOV/MP4 有独立元数据容器,Live Photo 是"图+视频"成对,要单独处理
    5 以为截图一定有 GPS 多数截图没有定位,先读再判断
    6 时间差 8 小时 GPSTimeStamp 是 UTC,DateTimeOriginal 常为本地时间,统一时区再排序
    7 云端同步 / 修图软件又写回元数据 清理放在最后一步(发布前),而不是流程开头
    8 用 Windows「属性」确认"已清空" 图形界面看不到 XMP/IPTC/缩略图,用 exiftool -a -G1 -s 复核
    9 只删 GPS,忘了 XMP/IPTC 用 -all=,或逐项确认三类容器都清了
    10 以为裁剪、压缩就等于清除 有些工具会复制元数据到新文件;裁剪对 IFD1 缩略图尤其无效(见 2.4)

    八、合规与边界:这一节比代码重要

    技术是中性的,但用在哪、对谁用,决定了它合不合规。这一节请当硬性约束读。

    8.1 法律层面:GPS 属于敏感个人信息

    按《中华人民共和国个人信息保护法》,行踪轨迹属于敏感个人信息,处理需满足特定目的、充分必要、单独同意与严格保护措施。一张带 GPS + 精确时间戳的照片,结合设备序列号、作者字段,具备识别特定自然人的能力,不能当普通"技术参数"看待。

    要点:

    • 处理他人照片的元数据,需有合法基础(本人同意、合同履行、法定义务或依法授权的调查程序);
    • 组织内部做合规审查,要有制度依据与授权范围,并把处理记录留痕;
    • 本文不提供法律意见,涉及具体案件请咨询专业律师。

    8.2 本文支持与不支持的用法

    ✅ 支持❌ 不支持
    检查自己照片发出去会带什么 用他人照片定位其住址、单位、常去地点
    组织内对自有素材做合规审查 跟踪、蹲守、骚扰、人肉
    合法授权下的取证与证据固定 批量抓取社交平台公开图片做人物画像/轨迹还原
    在上传流水线里默认剥离元数据 绕过平台限制获取他人原图
    科普、教学、防御性研究 把提取结果公开发布或用于交易

    一句话:读取能力只用于"看清自己暴露了什么"和"有授权的调查",不用于"看清别人在哪"。

    8.3 取证场景的额外要求

    • 授权链书面化:谁授权、范围、期限、用途;
    • 证据保全:原件只读、先算 SHA-256、工作副本分析、全程记录工具版本与命令;
    • 最小必要:只提取与案件相关的字段,不做无关扩散;
    • 结论表述:注明坐标系、误差范围、时区,避免"精确定位到某人"这类超出证据能力的断言。

    8.4 内容层面的合规(发这篇文章本身也要守)

    项要求本文落地
    AI 参与程度声明 如实标注,不标「纯原创」 front matter ai_assisted: true,正文说明程序生成测试图
    AIGC 标识 2025-09-01 起,AI 生成的文本/图片/音视频须显式标识 + 隐式元数据,不得删改 本文插图为程序绘制示意图,非 AI 生成写实内容;如后续替换为 AI 图须显式标注
    信息来源标注 引用标准/法规/工具版本须标来源 已标注工具版本、法规名称、实测环境
    虚构 / 演绎标识 演示数据须说明「示例/自造」 测试图、坐标、设备型号均为自造,开头已声明
    不提供可复用攻击方法 平台专项红线 全文仅读取、核查、清理与加固,无入侵/绕过步骤
    广告标识 带货/测评附链接须标「广告」 本文无带货、无购物链接,不涉及

    平台规则与法规会更新(例如 AIGC 标识细则、各平台对元数据处理的口径),以官方最新发布为准。


    九、写在最后

    把全文收成一句话:

    元数据是「默认写入、默认不显眼、默认跟着文件走」的东西。所以防护不能靠提醒,得靠流程。

    三件最值得先做的事:

  • 个人:把 exiftool 装上,发原图前跑一次自查 + 清理 + 复核,三步做成肌肉记忆;
  • 组织:在上传网关把「剥离元数据」做成默认动作,原始字节不落盘,线上定期抽查;
  • 认知:别把"社交平台会处理"当保障,也别把"任何照片都能定位"当常识——先读,再判断。
  • 顺带回应一个常被问到的问题:清理元数据会不会让照片失去价值? 对绝大多数发布场景,不会。EXIF 里的曝光参数、机身序列号、缩略图,对读者没有任何意义;真正需要保留拍摄参数的场景(摄影作品归档、取证原件),把元数据单独导出成结构化记录保存即可,不需要让它跟着对外发布的图片一起走。把「展示用副本」和「归档用原件」分开,是最干净的解法。

    落地清单

    优先级动作类型预期效果
    P0 本地安装 exiftool,对最近要发的图跑一次 -a -G1 -s 自查 先看清现状
    P0 发原图一律 exiftool -all= + 复核 发布 高危字段归零
    P0 检查 IFD1 缩略图,裁剪图也要清 发布 堵住最易漏的口子
    P1 把 3.3 的自查脚本挂进日常发布流程 工具 从靠自觉变成靠流程
    P1 拍摄端关闭相机定位权限 源头 少写进去一份
    P1 文件名去掉姓名/工号/地址 发布 降低关联识别
    P2 上传网关默认剥离元数据,原件不落盘 工程 组织级收敛
    P2 线上图片每周随机抽查 + 告警 运营 防绕过、防回归
    P2 归档原件与展示副本分离,元数据单独存档 治理 兼顾取证与发布

    延伸方向:这一篇讲的是图片。同一套思路可以平移到 PDF / Office 文档元数据(作者、修订记录、隐藏批注、打印路径)、音视频容器元数据(MOV/MP4 的 moov 地理标签)、以及日志与截图里的隐性信息(时间戳、主机名、内网 IP)。如果要做成系列,下一篇适合写「文档文件里的隐藏信息」。
    合规说明:本文所有命令均为读取、核查、清理与加固操作,不含可对他人实施定位、跟踪或绕过平台限制的方法;示例数据与坐标均为自造或公开地标,不指向任何个人。涉及个人信息处理与取证的具体场景,请以现行法律法规与官方最新规则为准,本文不构成法律意见。

    赞(0)
    未经允许不得转载:171主机测评 » 你随手发的一张照片能泄露多少信息?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址