图片里的 GPS 正在暴露你的位置:EXIF 隐私清理与自动化缩略图流水线实战

你上传的那张照片,正在暴露你家门口的位置

个人站长往网站上放图片是家常便饭:文章配图、产品图、生活照、活动照片。绝大多数人只关心一件事——图清晰不清晰、加载快不快。很少有人会想到,一张从手机直接导出、原样上传的 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)就能一次性解决,搭好之后基本不用再维护。对个人站长来说,这种「搭一次、永久受益」的改造,优先级应该排在很前面。

Last modification:September 28th, 2026 at 07:25 pm

Leave a Comment