EXIF:藏在图片里的秘密
我发现了一个隐私问题
上线第二周,一个用户给我发了一条消息:
我在你们平台上上传了一张在家门口拍的照片。结果发现,点击图片的”查看信息”后,显示了经纬度坐标。这是不是意味着别人能知道我住在哪里?
我的心一紧。我从来没想过这个问题。
我打开一张测试照片,用 Python 解析了它的 EXIF 数据:
from PIL.ExifTags import TAGS, GPSTAGS
from PIL import Image
def extract_exif(image_path):
"""提取图片的 EXIF 元数据"""
img = Image.open(image_path)
exif_data = img._getexif()
if not exif_data:
return "无 EXIF 数据"
result = {}
for tag_id, value in exif_data.items():
tag_name = TAGS.get(tag_id, tag_id)
result[tag_name] = value
return result
exif = extract_exif('/var/www/photos/night_photo.jpg')
# 输出:
# {
# 'Make': 'SONY', # 相机品牌
# 'Model': 'ILCE-7RM5', # 相机型号
# 'LensModel': 'FE 24-70mm F2.8 GM II', # 镜头型号
# 'DateTimeOriginal': '2024:06:15 20:30:15', # 拍摄时间
# 'FocalLength': 35.0, # 焦距
# 'FNumber': 2.8, # 光圈
# 'ExposureTime': 0.5, # 快门速度
# 'ISO': 400, # ISO 感光度
# 'GPSInfo': { # GPS 定位!
# 1: 'N',
# 2: (39.0, 54, 34.56), # 纬度
# 3: 'E',
# 4: (116.0, 23, 45.67), # 经度
# },
# 'Software': 'Adobe Lightroom 7.0', # 后期软件
# 'ImageWidth': 6000,
# 'ImageHeight': 4000,
# }GPS 定位数据就在图片里。 精确到经纬度的小数点后两位——足够定位到一栋楼。
EXIF 到底包含什么?
我系统地研究了一下 EXIF(Exchangeable Image File Format)标准:
# EXIF 数据分类
EXIF_CATEGORIES = {
'拍摄参数': {
'ExposureTime': '快门速度(如 1/200s)',
'FNumber': '光圈(如 f/2.8)',
'ISO': '感光度(如 400)',
'FocalLength': '焦距(如 35mm)',
'WhiteBalance': '白平衡模式',
'Flash': '闪光灯是否开启',
'MeteringMode': '测光模式',
'ExposureProgram': '曝光程序(光圈优先/快门优先等)',
},
'设备信息': {
'Make': '制造商(如 SONY)',
'Model': '型号(如 ILCE-7RM5)',
'LensModel': '镜头型号',
'Software': '处理软件',
},
'时间信息': {
'DateTimeOriginal': '拍摄时间',
'DateTimeDigitized': '数字化时间',
'DateTime': '修改时间',
},
'位置信息': {
'GPSLatitude': '纬度',
'GPSLongitude': '经度',
'GPSAltitude': '海拔',
'GPSDestBearing': '拍摄方向',
},
'图片信息': {
'ImageWidth': '宽度',
'ImageHeight': '高度',
'Orientation': '拍摄方向(横/竖)',
'XResolution': '水平分辨率',
'YResolution': '垂直分辨率',
'ColorSpace': '色彩空间(sRGB/AdobeRGB)',
},
}这些信息对摄影师来说很有价值——他们可以在平台上展示拍摄参数,其他用户可以学习。
但 GPS 坐标是另一个故事。
我做了一个决定:剥离敏感 EXIF
我需要在”保留有价值的拍摄参数”和”保护用户隐私”之间找到平衡。
from PIL import Image
import io
# 需要保留的 EXIF 字段
SAFE_EXIF_TAGS = {
'Make', 'Model', 'LensModel',
'ExposureTime', 'FNumber', 'ISO', 'FocalLength',
'DateTimeOriginal',
'ImageWidth', 'ImageHeight', 'Orientation',
}
# 需要删除的 EXIF 字段(隐私敏感)
SENSITIVE_EXIF_TAGS = {
'GPSInfo', # GPS 定位
'GPSTimeStamp', # GPS 时间戳
'GPSDateStamp', # GPS 日期
'UserComment', # 用户注释
'XPComment', # Windows 注释
'XPTitle', # Windows 标题
'XPAuthor', # Windows 作者
'XPKeywords', # Windows 关键词
'XPSubject', # Windows 主题
}
def strip_sensitive_exif(input_path, output_path):
"""剥离敏感 EXIF 数据,保留安全的拍摄参数"""
img = Image.open(input_path)
# 获取原始 EXIF
original_exif = img._getexif()
if not original_exif:
img.save(output_path)
return
# 过滤 EXIF:只保留安全的字段
from PIL.ExifTags import TAGS
safe_exif = {}
for tag_id, value in original_exif.items():
tag_name = TAGS.get(tag_id, tag_id)
if tag_name in SAFE_EXIF_TAGS:
safe_exif[tag_id] = value
# 创建新图片,写入过滤后的 EXIF
if safe_exif:
from PIL.Image import Exif
exif = Exif()
exif_data = img.info.get('exif', b'')
img.save(output_path, exif=exif.tobytes() if safe_exif else None)
else:
img.save(output_path)
return {
'original_tags': len(original_exif),
'stripped_tags': len(original_exif) - len(safe_exif),
'kept_tags': list(safe_exif.keys()),
}更简单的方案:缩略图直接不保留任何 EXIF。
def generate_thumbnail_no_exif(image_path, output_path, max_width=800, quality=80):
"""生成缩略图,完全不保留 EXIF"""
img = Image.open(image_path)
img.thumbnail((max_width, max_width), Image.LANCZOS)
# 保存时不传入 exif 参数 = 不包含 EXIF
img.save(output_path, 'WebP', quality=quality)
# 缩略图文件大小对比:
# 带 EXIF:82 KB
# 不带 EXIF:78 KB
# EXIF 数据通常很小(几 KB),对缩略图影响不大我的最终策略:
EXIF_STRATEGY = {
'original': '保留全部 EXIF', # 原图原样保存
'thumbnail': '不保留任何 EXIF', # 缩略图不需要 EXIF
'detail': '保留安全字段', # 详情页展示拍摄参数
'download': '保留全部 EXIF', # 下载原图时保留
}EXIF 的另一个用途:智能旋转
处理 EXIF 时,我发现了一个有趣的问题。
小李上传的竖拍照片,在我的网站上显示成了横的——头是歪的。
# 问题原因:EXIF 中的 Orientation 字段
# 相机拍摄时传感器是横的,竖拍时只是记录了 Orientation=6(旋转 90°)
# 浏览器如果不读 EXIF,就不知道该旋转
ORIENTATION_VALUES = {
1: '正常',
2: '水平翻转',
3: '旋转 180°',
4: '垂直翻转',
5: '旋转 90° + 水平翻转',
6: '旋转 90°', # ← 小李的竖拍照片是这个
7: '旋转 270° + 水平翻转',
8: '旋转 270°',
}
# 解决方案:在生成缩略图时自动旋转
def auto_rotate_image(img):
"""根据 EXIF Orientation 自动旋转图片"""
from PIL import Image
try:
exif = img._getexif()
if not exif:
return img
from PIL.ExifTags import TAGS
orientation = None
for tag_id, value in exif.items():
if TAGS.get(tag_id) == 'Orientation':
orientation = value
break
if orientation == 3:
return img.rotate(180, expand=True)
elif orientation == 6:
return img.rotate(270, expand=True)
elif orientation == 8:
return img.rotate(90, expand=True)
except Exception:
pass
return img这个问题很隐蔽——在本地电脑上图片显示正常(因为系统会读 EXIF 自动旋转),上传到网站后就歪了。
我的思考
思考 1
社交媒体平台(如微信朋友圈)为什么不对上传的图片保留 EXIF?这对摄影师社区来说是损失吗?
社交媒体剥离 EXIF 的原因:
隐私保护:EXIF 中包含 GPS 坐标,用户在家拍照上传后,可能暴露家庭住址。微信有 10 亿用户,一旦出现位置泄露事件,后果不堪设想。
存储成本:EXIF 数据虽然每张只有几 KB,但乘以每天几十亿张图片,就是几十 TB 的额外存储。CDN 传输这些数据也要花钱。
安全考虑:EXIF 中的设备信息可能被用于社会工程攻击——知道你用什么相机、在哪里拍的,可以推断你的消费水平、活动范围。
对摄影师社区的影响:
摄影师确实需要 EXIF 数据来展示拍摄参数、交流技术。但这两个需求可以分离处理:
方案 1:原样保存,展示时选择性显示
- 存储:保留完整 EXIF
- 展示:只显示拍摄参数(光圈、快门、ISO)
- 隐藏:GPS 坐标、设备序列号
方案 2:提取后单独存储
- 上传时提取 EXIF 到数据库
- 图片文件本身剥离 EXIF(减少文件大小)
- 展示时从数据库读取方案 2 更好:图片文件更小,数据结构化后可以搜索和统计。
# 方案 2 实现
def process_upload(image_path):
# 提取 EXIF 存入数据库
exif_data = extract_exif(image_path)
safe_exif = filter_safe_exif(exif_data)
db.insert('photo_metadata', {
'photo_id': photo_id,
'camera': safe_exif.get('Model'),
'lens': safe_exif.get('LensModel'),
'aperture': safe_exif.get('FNumber'),
'shutter_speed': safe_exif.get('ExposureTime'),
'iso': safe_exif.get('ISO'),
'focal_length': safe_exif.get('FocalLength'),
'taken_at': safe_exif.get('DateTimeOriginal'),
})
# 生成不带 EXIF 的缩略图
generate_thumbnails_no_exif(image_path)
# 原图也剥离 GPS 后保存
strip_gps_exif(image_path, original_storage_path)