你上传的那张照片,正在暴露你家门口的位置
个人站长往网站上放图片是家常便饭:文章配图、产品图、生活照、活动照片。绝大多数人只关心一件事——图清晰不清晰、加载快不快。很少有人会想到,一张从手机直接导出、原样上传的 JPG,里面可能藏着你拍照时的精确 GPS 坐标、拍摄时间、设备型号和序列号。
这些信息存在文件头部的 EXIF 区域。相机和手机拍照时默认会写入它们,本意是方便日后管理相册。但当你把这张原图上传到公网,任何访客只要右键保存、用一条命令就能读出来。有人靠一张「在家门口拍的猫」定位到具体楼栋;有人靠设备序列号把同一批照片关联到同一个人。
这篇文章讲两件事,都是围绕图片这一环的真实工程问题:第一,怎么批量、彻底地清掉图片里的隐私元数据;第二,怎么搭一条自动化的缩略图流水线,让图片上传后自动生成合适尺寸、合适格式的版本,既省流量又不用手工处理。这两件事用同一套工具链就能做完,一次搭好,长期受益。
先搞清楚:EXIF 里到底有什么
在动手清理之前,先要知道你看的是什么。三个命令行工具足够:exiftool 读得最全,identify(ImageMagick)能看基本属性,exiftool -GPS* 专门看定位。
# 看一张图的全部元数据(信息会很多)
exiftool photo.jpg
# 只看 GPS 相关字段
exiftool -GPS:all photo.jpg
# 只看拍摄时间、设备、软件
exiftool -DateTimeOriginal -Make -Model -Software -SerialNumber photo.jpg重点关注这几类字段,它们对隐私的杀伤力最大:
- GPSLatitude / GPSLongitude:精确到米级的拍照位置。这是最危险的,直接暴露你的居住地、常去场所。有些手机还同时写入 GPSAltitude(海拔),进一步缩小范围。
- DateTimeOriginal:精确到秒的拍摄时间。单张不致命,但和社交媒体发帖时间交叉比对,可以推断作息规律。
- Make / Model / SerialNumber / LensSerialNumber:设备型号甚至机身序列号。序列号最能定位到「同一台设备拍的所有照片」,即使用不同账号发布也能关联起来。
- Software / HostComputer:处理软件和电脑名,有时会带上你的用户名。
- Artist / Copyright / ImageDescription:很多人不知道,相机设置里填的署名也会一直跟着照片走。
还要注意一个容易被忽略的点:有些信息不在 EXIF 里,而在 IPTC 或 XMP 区域。IPTC 常见于图片编辑软件写入的标题、作者、版权;XMP 是 Adobe 系的元数据容器。所以清理时不能只针对 EXIF,要覆盖全部元数据块。
清理元数据:一行命令,但要小心两个陷阱
最常用的清理命令是 exiftool 的「保留图像、丢弃元数据」模式:
exiftool -all= -overwrite_original photo.jpg但这条命令有两个坑,直接用会出问题。
陷阱一:颜色配置(ICC Profile)也会被一起删掉。对于普通 sRGB 的图,删掉 ICC 通常没影响;但如果你的图用了 Adobe RGB 或窄色域配置,删掉之后在浏览器里可能整体偏色、发灰。-all= 会连 ICC 一起清。稳妥的做法是保留 ICC:
exiftool -all= -icc_profile:all= -overwrite_original photo.jpg等等,上面这段需要澄清——-all= 已经把 ICC 删了,想保留就不能用这个写法。正确做法是显式声明要保留的标签:
# 清空所有元数据,但把 ICC Profile 显式保留下来
exiftool -all= -tagsfromfile @ -icc_profile -overwrite_original photo.jpg-tagsfromfile @ 的意思是从原文件(@)复制指定标签到处理后的文件,这里只复制 ICC Profile。一定要先在副本上测试,用 identify -verbose 或再跑一次 exiftool 确认颜色信息还在、GPS 已经没了。
陷阱二:PNG 和 WebP 的元数据在别的位置。JPEG 的元数据在 APP1 段,PNG 则存在 tEXt / iTXt 块里,WebP 又是另一种容器格式。exiftool 都能处理,但如果你用的是别的工具(比如某些在线工具),可能只清了 JPEG 的 EXIF 而漏掉 PNG 的文本块。用 exiftool 统一处理是最省心的。
批量处理一个目录(递归):
# 先备份原始文件,再批量清理
cp -r /var/www/site/uploads /backup/uploads_orig
exiftool -r -all= -tagsfromfile @ -icc_profile \
-overwrite_original /var/www/site/uploads-r 递归子目录,-overwrite_original 直接覆盖不自建备份文件(exiftool 默认会保留一个 _original 副本,批量时会生成一堆垃圾文件,所以这个参数在生产环境是必要的)。
清理完之后验证一下,这一步别省:
# 应该没有任何输出,说明 GPS 全清了
exiftool -GPS:all -r /var/www/site/uploads | grep -i gps把清理做成「上传即自动」,而不是事后补救
手工清理的问题在于:你今天清完了,明天又传了十张新图。真正可靠的做法是把清理接进上传流程,让每一张进入服务器的图都自动过一遍。有两条路。
路线一:在 PHP 上传处理里剥元数据。PHP 的 GD 库重写图像时天然不保留元数据,只要你用 GD 重新编码,EXIF 就没了。这是最省事的方案:
<?php
function strip_meta($src, $dst) {
$info = getimagesize($src);
switch ($info[2]) {
case IMAGETYPE_JPEG:
$img = imagecreatefromjpeg($src);
imagejpeg($img, $dst, 88);
break;
case IMAGETYPE_PNG:
$img = imagecreatefrompng($src);
imagesavealpha($img, true);
imagepng($img, $dst, 6);
break;
default:
return false;
}
imagedestroy($img);
return true;
}注意 imagesavealpha 那行:PNG 默认重编码会丢掉透明通道,导致原本透明的背景变成黑色,这是用 GD 处理 PNG 最常见的翻车点。
路线二:用 exiftool 做目录级守护。如果你的站有多个上传入口、改不动每一处的代码,那就用 cron 定期扫目录:
# 每 10 分钟清理一次新上传的文件(用 -m 允许修改时间判断)
*/10 * * * * find /var/www/site/uploads -type f \
\( -name '*.jpg' -o -name '*.jpeg' -o -name '*.png' \) \
-mmin -11 -print0 | xargs -0 -r exiftool -all= \
-tagsfromfile @ -icc_profile -overwrite_original >>/var/log/strip_meta.log 2>&1这条 cron 的逻辑是「只处理最近 11 分钟内被修改过的图片」,好处是不会每次都把全站图片重新扫一遍(图片多了这很费 IO)。时间窗口 11 分钟比执行间隔 10 分钟稍大,避免因为任务执行耗时刚好错过文件。
搭一条自动化缩略图流水线
清理元数据解决的是隐私问题,接下来解决体验问题:访客在文章列表页看到的图,不需要原图那么大。一张手机拍的 4000x3000、4MB 的照片,在列表页只显示 300px 宽,却让访客下载了 4MB。这就是典型的「图省事,流量和速度都亏了」。
缩略图流水线要解决三件事:生成多个尺寸、转成更省流量的格式、以及「列表页用缩略图、详情页用大图」的正确引用。用 ImageMagick 的 convert(新版叫 magick)可以一次搞定。
#!/bin/bash
# gen_thumbs.sh —— 为一张图生成多档尺寸 + WebP
SRC="$1"
OUTDIR="/var/www/site/cache/thumbs"
NAME=$(basename "$SRC")
BASE="${NAME%.*}"
# 三档常用尺寸:列表缩略图 / 正文中图 / 大图预览
for SIZE in 320 768 1280; do
magick "$SRC" \
-auto-orient \
-resize "${SIZE}x${SIZE}>" \
-strip \
-quality 82 \
"$OUTDIR/${BASE}-${SIZE}.jpg"
magick "$SRC" \
-auto-orient \
-resize "${SIZE}x${SIZE}>" \
-strip \
-quality 78 \
"$OUTDIR/${BASE}-${SIZE}.webp"
done
echo "done: $SRC"这段脚本里有几个参数值得逐一解释,它们直接决定了输出质量:
-auto-orient:必加。手机拍照时会用 EXIF 里的 Orientation 标记表示「需要旋转 90 度显示」,很多看图软件会读取它自动转正。但如果你把 EXIF 删了(前面刚做的事)却没在删除前旋转,图片就会歪掉。所以顺序必须是「先 auto-orient 应用旋转,再 -strip 删除元数据」。这两个参数一起用,才能既正又干净。-resize "320x320>":注意结尾的>,它的意思是「只在原图比目标尺寸大时才缩小,绝不放大」。没有这个符号,一张 200px 的小图会被强行放大到 320px,糊成一片。-strip:等价于前面说的清理,去掉全部元数据。放在流水线里,等于缩略图天然就是干净的。-quality 82(JPEG) /78(WebP):JPEG 82 是清晰度和体积的平衡点,低于 75 开始出现明显块状伪影;WebP 因为压缩效率更高,可以更低一点。
响应式图片:让浏览器自己挑该下载哪一档
生成了多档尺寸,还得让浏览器知道「有哪些可选、该选哪个」,否则它还是只会下载你 src 里写的那一张。这就是 srcset 和 sizes 的用途。
<img
src="/cache/thumbs/photo-768.jpg"
srcset="/cache/thumbs/photo-320.jpg 320w,
/cache/thumbs/photo-768.jpg 768w,
/cache/thumbs/photo-1280.jpg 1280w"
sizes="(max-width: 600px) 100vw, 768px"
width="1280" height="960"
loading="lazy" decoding="async"
alt="示例配图">浏览器的决策逻辑是:先根据 sizes 算出「当前布局下这张图要占多宽」,再结合设备像素比(DPR)从 srcset 里挑一个刚好够用的。手机上宽度 100vw、DPR 2,那么 320px 的屏幕就需要约 640 物理像素,浏览器会选 768w 那档;桌面端布局宽 768px、DPR 1,也选 768w。这样一来,手机访客永远不会去下载 1280 的大图。
几个细节:
width和height属性一定要写,且是原图真实的宽高比。这不是为了控制显示大小(那由 CSS 决定),而是为了让浏览器在图片加载前就预留正确高度的空间,避免图片加载时页面内容跳动(CLS,这是搜索引擎性能评分的一项)。loading="lazy"让首屏之外的图延迟加载。但首屏的第一张图不要加 lazy,否则它会被推迟到布局计算之后才开始下载,反而拖慢首屏最大内容绘制(LCP)。首屏图应该用fetchpriority="high"。- WebP 的引用需要
<picture>配合<source type="image/webp">,或者干脆用 Nginx 的map按Accept头自动返回 WebP,这样 HTML 里只写一个 JPG 路径就行。
用 Nginx 按 Accept 头自动投递 WebP
如果你想做到「HTML 里只写 JPG 路径,但支持 WebP 的浏览器自动拿到 WebP」,可以在 Nginx 层做内容协商。前提是你的缩略图流水线已经同时生成了同名的 .webp 文件。
# http 块内:根据 Accept 头判断浏览器是否支持 webp
map $http_accept $webp_suffix {
default "";
"~*image/webp" ".webp";
}
server {
location ~* ^/cache/thumbs/(.+)\.(jpe?g|png)$ {
add_header Vary Accept;
try_files /cache/thumbs/$1$webp_suffix /cache/thumbs/$1.$2 =404;
}
}try_files 会先尝试带 .webp 后缀的文件,不存在就回退到原始 JPG。这里有一个必须加的东西:add_header Vary Accept;。因为同一个 URL 会根据 Accept 头返回不同内容,如果响应里不声明 Vary: Accept,CDN 或中间代理可能把「给支持 WebP 浏览器的 WebP 响应」缓存下来,然后发给一个不支持 WebP 的老浏览器,导致图裂。这是内容协商类改造最经典的静默失效点。
一次真实的排查:图片清了 EXIF 反而变歪了
有位站长按教程批量执行了 exiftool -all=,清完之后发现所有竖拍的手机照片全躺倒了——人像变成横躺。他一开始以为是清理破坏了图像数据,回滚了备份,结果 GPS 又回来了。
真实原因是:手机竖拍时,感光元件实际记录的仍是横向像素,只是写了一个 Orientation 标记告诉查看器「请旋转 90 度显示」。他之前之所以看着正常,是因为浏览器和看图软件都读了这个标记。-all= 把标记删了,像素本身却没被旋转,于是显示器失去了「需要转正」的提示,图片就呈现原始横向。
排查过程很简单,两步就定位了:
- 先对比两张图(清理前后的)实际像素尺寸,发现宽高没变、像素数据几乎一致,说明图像没被破坏,问题只在方向标记。
- 再
exiftool -Orientation查原图,看到值是Rotate 90 CW,确认了症结。
修复也简单:把顺序改成「先应用旋转,再删元数据」。exiftool 自己的做法是加 -Orientation#=1 之前先转像素,更通用的做法是交给流水线里的 ImageMagick —— 它的 -auto-orient 会读取 Orientation 并真正旋转像素,然后 -strip 才删标记。改造后重新处理这批图,方向正确且 GPS 全无。
这个案例的教训值得记住:EXIF 不只有 GPS 这类「纯隐私」字段,还有一些(Orientation、ICC Profile)是被渲染流程真正依赖的。清理元数据不是「无脑删光」,而是要区分「可安全删除的隐私字段」和「必须保留或先应用再删的功能字段」。把清理动作放在图像处理流水线里(先 auto-orient 再 strip),就天然避免了这类问题。
小结与落地清单
- 上传前先
exiftool -GPS:all看一眼,确认你的原图到底带了多少隐私信息,很多站长第一次看到精确坐标都会吓一跳。 - 清理用
exiftool -all= -tagsfromfile @ -icc_profile -overwrite_original,保留 ICC 避免偏色;生产环境加-r和-overwrite_original。 - 不要把 EXIF 清理和方向修正搞反顺序:先 auto-orient,再 strip,否则竖拍照片会全部躺倒。
- 缩略图流水线一次生成 320/768/1280 三档,同时输出 WebP,用
-resize "NxN>"防止放大小图。 - 用
srcset+sizes让浏览器挑尺寸,width/height必写以避免布局跳动,首屏图不要 lazy。 - Nginx 做 WebP 内容协商时,务必加
Vary: Accept,否则 CDN 缓存会串味导致图裂。 - 把清理做成「上传即自动」:能在上传代码里剥就剥(GD 重编码),剥不了就用 cron 扫新文件。
图片这个环节很容易被当成「非技术活」忽略掉,但它同时踩中两个真实的坑:一个是隐私泄露(不可逆,且你自己不知道),一个是性能浪费(访客替你买单)。这两件事用同一套工具链(exiftool + ImageMagick + Nginx)就能一次性解决,搭好之后基本不用再维护。对个人站长来说,这种「搭一次、永久受益」的改造,优先级应该排在很前面。