图床外链检测与批量巡检:从防盗链到嵌入式加载

📅 发布时间:2026/9/30 1:18:42
图床外链检测与批量巡检:从防盗链到嵌入式加载
上周整理自己的文档仓库随手点开一篇半年前写的笔记配图全变成了浏览器里的裂图图标——一共十七篇文档、四十多张图全都指向同一个图床域名。更麻烦的是链接单独丢进地址栏能打开嵌在页面里就是不显示浏览器控制台里干干净净什么错都不报。折腾了大半天才定位到是对方加了 Referer 白名单。这件事之后我把「图床外链测试」当成一个正经的小项目来做重点不是评判某个图床服务好不好而是搞清楚我作为引用方能不能放心地把一条外链长期挂在文档、网页甚至是嵌入式设备的界面上。这套东西解决的其实是三个很具体的问题链接还活着吗、返回的还是我那张图吗、在被嵌入的那个环境里能被正常渲染吗。前两个问题决定了你的文档三个月后是不是一堆裂图第三个问题决定了你的页面或者设备界面会不会白屏。不管你是写博客的、做文档站的、用 Markdown 记笔记的还是在嵌入式 Linux 板子上做 HMI 界面的下面的检测维度、命令和脚本都能直接拿去改改就用。我把踩过的坑都写在后面省得你再花一个下午去猜。1. 先把测试对象说清楚图床外链到底会怎么失效1.1 外链失效远远不止 404 一种大部分人脑子里对链接坏了的定义就是打不开、返回 404。实际做过一轮批量巡检就知道这个认知太窄了。我把过去两年遇到的失效情况归了一下类最典型的是三种第一种是服务端状态异常包括 404文件被清理、403防盗链拦截或权限策略变更、429触发了限速、5xx图床侧故障。这一类最好查一条命令就能看出来。第二种是内容被替换也是最阴的一类。状态码返回 200Content-Type 也是 image/jpeg字节数看着也正常但图已经不是你上传的那张了——可能是审核拦截后换上的占位图、可能是被压缩降质后的版本、也可能是默认水印模板。这种情况你光看状态码永远发现不了必须比对内容指纹。第三种是能访问但不可嵌入链接本身没问题但被嵌到页面里就被浏览器或者目标环境拦掉比如 HTTPS 页面里引了 HTTP 图、跨域策略不匹配、目标环境不支持返回的图片格式WebP、AVIF 在老旧设备上大概率解不开。这三类失效的排查手段完全不同所以测试的第一件事就是把它们分开对待。再补一点这里的嵌入式其实有两个层次的意思。一个层次是把外链嵌进 Markdown、HTML、文档系统里这时候浏览器和渲染器就是你的运行环境另一个层次是把外链嵌进嵌入式设备的界面程序里运行环境变成了一块内存几十兆、根证书可能都不全的板子。后者对链接的要求比前者苛刻得多第五章单独讲。1.2 四个检测维度与判定标准我一般把检测拆成四个维度每个维度有独立的判定标准和失败表现。这张表是我自己巡检脚本里用的口径可以直接抄。检测维度具体测什么判定标准失败典型表现可达性DNS 解析、TCP 连接、TLS 握手、最终状态码最终状态码 2xx重定向跳数不超过 3403 防盗链、404 文件被清、DNS 解析超时内容正确性Content-Type 前缀、字节数、图像能否解码、内容指纹Content-Type 以 image/ 开头尺寸与指纹与源图一致200 返回占位图、返回 HTML 错误页、返回 WebP 而目标不支持引用兼容性协议一致性、Referer 策略、CORS 头、格式支持HTTPS 页面引用 HTTPS 链接防盗链白名单覆盖引用域名页面裂图但直开正常、控制台报混合内容错误性能与持久性首字节时间、总耗时、多节点一致性、签名有效期单图下载耗时在可接受区间内多节点返回内容一致首字节超过 2 秒、节点 A 新图节点 B 旧图、签名 URL 过期四个维度里可达性适合天天跑成本低、误报少内容正确性适合每周跑因为要完整下载图片、算指纹带宽开销大引用兼容性适合在改动页面结构或换图床时跑性能与持久性适合在发布前跑一次基线之后按周对比。分频次跑能省掉很多无谓的流量和时间这个节奏是我试过几个月之后固化下来的。2. 方案选型从手工点到脚本巡检的路线图2.1 几十张图命令行完全够用别一上来就写框架我见过不少人第一步就去搭一套带数据库的巡检系统结果图总共才三十张跑一次的心智负担比手工点还大。数量在几十张这个量级curl 加一个 while 循环就是最优解不改代码、不留依赖、结论直观。核心就是一行 format 参数把状态码、类型、字节数、耗时全打出来比一个个点开浏览器看快十倍不止。# urls.txt 每行一条图片外链 while read -r url; do curl -sS -o /dev/null -L --max-redirs 3 \ -A Mozilla/5.0 \ -w %{http_code}\t%{content_type}\t%{size_download}\t%{time_total}\t%{url_effective}\n \ $url done urls.txt这里的几个参数值得解释一下。-o /dev/null表示把响应体丢掉只保留统计信息能省掉大量磁盘写入-L --max-redirs 3是跟随重定向但限制跳数防止遇到跳转环时一直转下去-w里的%{url_effective}是跳转之后的最终地址排查为什么这条链接跳到了别处时特别有用。-A是伪装一个常见的浏览器 UA有些图床对空 UA 或者明显脚本 UA 会直接拒绝虽然这个行为不太友好但你得先绕过它才能拿到真实结论。2.2 上千张图Python 并发脚本是性价比最高的选择数量上到几百上千命令行的串行执行就撑不住了。按每张图平均往返 300 毫秒算一千张串行要跑五分钟而开 12 到 16 个并发之后能压到十几秒这个差距是数量级上的。并发方案里我更推荐 Python 的requests加ThreadPoolExecutor而不是一上来就写异步。原因很实在这类任务的瓶颈几乎全在网络 IO 等待线程池足够覆盖异步虽然理论吞吐更高但代码里一旦混进某个同步阻塞调用比如图像解码库调试成本会翻倍而线程池对这类混用天然容忍。选requests还有一个隐藏好处它的会话复用和连接池是开箱即用的同一域名下的几百个请求能省掉大量 TCP 和 TLS 握手开销。实测下来同一批图片用新连接逐个请求和复用会话总耗时能差出三成左右域名越多差距越明显。2.3 什么时候必须上无头浏览器纯 HTTP 层的检测有个天然盲区它看不到渲染结果。如果目标图床的防盗链是靠 JavaScript 注入 Cookie 实现的、或者页面侧有 CSP 策略拦截、又或者你测的是引用方的页面而不是链接本身那么 curl 拿到的结论就是片面的——链接返回 200页面照样裂图。这种情况下用 Playwright 或者类似工具补一次就够了做法很简单本地起一个 HTML 页面把所有待测外链用img标签塞进去用无头浏览器打开监听response和requestfailed事件把所有非 2xx 的图片请求、以及被浏览器拦截的请求全部收集起来再截一张整页图。这一趟跑下来你得到的就是用户真实看到的结论而不是服务器告诉你的结论。我一般只在发布前跑一次不做日常巡检因为它太重。3. 手工测试的六个关键动作3.1 状态码、响应头与重定向链最基础的动作但要注意 curl 的-I发的是 HEAD 请求而相当一部分图床和 CDN 对 HEAD 的支持是有问题的可能返回 200 但什么头都不给也可能直接 405。所以判断可用性时别用-I用-o /dev/null -D -发 GET 拿响应头。要关注的头有几个Content-Type决定浏览器会不会当图片渲染返回 text/html 基本就意味着这是个错误页Content-Length用来和实收字节数对比ETag和Last-Modified用来判断缓存版本Cache-Control决定你的链接会不会被中间层缓存很久导致换了图还显示旧的。重定向链也要看。有些图床会把请求 302 到你控制不了的地方链路上每多一跳就多一个故障点。我做过的判断是正常图床的跳转不该超过一跳超过两跳基本就是有中间层在做转发这种链接的可用性完全取决于中间层风险高能换就换。3.2 用 Referer 复现防盗链拦截防盗链导致的裂图是我遇到最多的一类问题也是最好复现的一类。核心就是控制 Referer 头看服务端在不同来源下的反应。URLhttps://img.example.com/2024/a1b2c3.jpg # 1) 带正常来源页 curl -sS -o /dev/null -w with-referer: %{http_code} %{size_download}\n \ -e https://my-docs.example.com/post/123 $URL # 2) 不带 Referer很多客户端默认行为 curl -sS -o /dev/null -w no-referer: %{http_code} %{size_download}\n \ -H Referer; $URL # 3) 带一个陌生来源 curl -sS -o /dev/null -w bad-referer: %{http_code} %{size_download}\n \ -e https://unknown.example.net/ $URL判读逻辑是这样的如果三种情况都返回 200 且字节数一致说明没有防盗链最省心如果只有第三种返回 403说明是标准白名单策略你需要把引用域名加进去如果第二种也返回 403问题就大了——很多客户端和渲染器在不跨域的场景下是不发 Referer 的这意味着你的链接在任何直开场景下都会失败只能在自己页面上用。这里有个细节要注意-H Referer;这个写法是 curl 提供的发送空值头语法和完全不写 Referer 头是两种不同的请求有些服务端会区别对待两种都要测。3.3 HTTPS 与混合内容检查如果你自己的页面是 HTTPS而图片外链还是 HTTP现代浏览器会直接把这张图拦掉控制台里会留下明确的提示。这个坑简单但致命因为它通常在你把站点切到 HTTPS 之后才批量暴露出来一次几十张图全裂。# 看看 http 链接是否会 301 到 https以及最终落到哪 curl -sS -o /dev/null -L -w %{http_code} %{url_effective}\n http://img.example.com/a.jpg如果它不跳转就必须手工把链接换成 https 版本。另外还有一种不那么明显的情况链接写的是 https但返回的图片内容里引用了 http 资源比如 SVG 内嵌外链这类问题 curl 看不出来得在浏览器里看控制台。所以我在做站点 HTTPS 迁移时会额外跑一次浏览器渲染检查。3.4 图片内容完整性校验这一步专门对付200 但不是我的图的情况。思路是下载完整内容比对三样东西字节数是否和源文件一致、MD5 是否一致、能不能被图像库正常解码。# 下载后看真实文件类型不要相信扩展名 curl -sS -o /tmp/check.img $URL file /tmp/check.img md5sum /tmp/check.img # 用 ImageMagick 看真实尺寸与格式 identify -format %m %wx%h %b\n /tmp/check.imgfile命令的作用是读文件头部的魔数这比看扩展名可靠得多。我遇到过一次图床在拦截之后返回了一张 200 的占位图扩展名 .jpgfile一看是 PNG尺寸也从 1920×1080 变成了 400×300这就有结论了。另外要留意identify报出来的实际字节数和Content-Length是否对得上两者不一致说明传输被截断通常和代理层或者超时有关。内容校验的代价是要下载完整文件所以适合每周跑一次而不是每天跑。3.5 多节点与缓存一致性外链指向的域名背后通常是一组 CDN 节点不同节点的缓存刷新时间可能不一致。你在本地看到的图和另一个城市的同事看到的图有可能不是同一张。排查方法是指定 IP 去请求同一个域名对比返回内容。# 列出该域名解析出的所有 IPv4 地址 dig short img.example.com | grep -E ^[0-9]\.[0-9]\.[0-9]\.[0-9]$ ips.txt while read -r ip; do echo node $ip curl -sS -o /dev/null --resolve img.example.com:443:$ip \ -w %{http_code} %{size_download} %{time_total} %{remote_ip}\n $URL done ips.txt--resolve的作用是强制把域名解析到指定 IP绕过本地 DNS这样你就能精确地逐节点比对。我判断一致性的口径是所有节点的状态码、字节数完全一致如果字节数有差异说明存在版本不一致这时候最省事的做法是换一个带版本号或者带时间戳的文件名重新上传而不是等它慢慢刷缓存。这个方法我在换图床、改图片内容之后必跑一次。3.6 时效、限速与签名过期有一类图床返回的是带签名的临时链接链接里会带过期时间参数。这种链接在文档里是绝对不能用的因为它可能几小时后就失效了。判断方法是看 URL 参数里有没有类似Expires、X-Amz-Expires、signature、token这样的字段有的话基本可以判定为临时链接。限速则是另一个方向的问题短时间内连续请求同一图床可能触发 429 或者被降速。我在批量巡检时遇到过一次前两百张飞快后面开始大面积超时隔了十分钟再跑又恢复正常这就是典型的服务端限速。应对办法是控制并发数、请求之间加一点随机延迟、遇到 429 时指数退避重试而不是硬刚。4. 自动化批量巡检脚本参数推导与完整实现4.1 并发、超时、重试三个参数怎么定这三个参数直接决定脚本是跑得快还是结论准不能拍脑袋。并发数我固定在 12 到 16 之间。再往上加本机的文件句柄和带宽先成为瓶颈而目标端在感知到异常流量后有可能直接限速反而让整体变慢、结论失真。目标是稳定拿到可信结论不是把目标打满。超时拆成两段连接超时 5 秒读取超时 15 秒。连接超时给 5 秒是因为 DNS 或者 TCP 层面出问题基本是毫秒级就该有结果5 秒还不通就是不通读取超时给 15 秒是给大图留余量一张 3MB 的图在弱网下确实可能要十几秒卡太死会误报。重试定为 2 次退避时间 0.5 秒、1 秒。关键点在于只有重试全部失败才判定为失败避免一次网络抖动就给你报一堆假问题。反过来如果一条链接三次都失败那基本可以确认是链路或者服务端的真实问题值得你去查。这套参数我在几千条链接上跑过误报率控制得很低。4.2 从 Markdown 里抽链接并检查完整代码下面脚本直接读 Markdown 文件把标准图片语法和 HTML img 标签里的外链都抽出来去重然后并发检查并输出报告。可以按需改输入源。#!/usr/bin/env python3 # -*- coding: utf-8 -*- import csv import hashlib import io import re import time from concurrent.futures import ThreadPoolExecutor, as_completed import requests from PIL import Image TIMEOUT (5, 15) # (连接超时, 读取超时) RETRY 2 # 重试次数 WORKERS 12 # 并发数 MAX_BYTES 8 * 1024 * 1024 # 单图最多下载 8MB防止异常大文件拖垮巡检 UA Mozilla/5.0 (compatible; ImgLinkChecker/1.0) REFERER https://my-docs.example.com/ # 按你的实际引用页填写 IMG_EXTS rpng|jpe?g|gif|webp|avif|bmp|svg def extract_urls(path): text open(path, encodingutf-8).read() urls set() # Markdown 图片语法 ![alt](url) urls | set(re.findall(r!\[[^\]]*\]\((https?://[^\s)\]), text)) # HTML img 标签 urls | set(re.findall(rimg[^]src[\](https?://[^\])[\], text)) # 独立的图片链接行 urls | set(re.findall(r^\s*(https?://\S\.(?:%s))\s*$ % IMG_EXTS, text, re.M)) return sorted(urls) def verify_image(data): 返回 (尺寸, 格式) 或 (None, 错误信息) try: Image.open(io.BytesIO(data)).verify() im Image.open(io.BytesIO(data)) return im.size, im.format except Exception as exc: return None, str(exc) def check_once(url): headers { User-Agent: UA, Accept: image/*,*/*;q0.8, Referer: REFERER, } t0 time.time() with requests.get(url, headersheaders, timeoutTIMEOUT, streamTrue, allow_redirectsTrue) as resp: ctype resp.headers.get(Content-Type, ).split(;)[0].strip().lower() declared int(resp.headers.get(Content-Length) or 0) buf io.BytesIO() for chunk in resp.iter_content(8192): buf.write(chunk) if buf.tell() MAX_BYTES: break data buf.getvalue() return { status: resp.status_code, ctype: ctype, declared: declared, bytes: len(data), redirects: len(resp.history), final_url: resp.url, elapsed: round(time.time() - t0, 3), md5: hashlib.md5(data).hexdigest(), _data: data, } def check(url): last_err for attempt in range(RETRY 1): if attempt: time.sleep(0.5 * (2 ** (attempt - 1))) try: info check_once(url) break except Exception as exc: last_err %s: %s % (type(exc).__name__, exc) else: return {url: url, ok: False, reason: request-failed last_err} reasons [] if not (200 info[status] 300): reasons.append(http-%d % info[status]) if not info[ctype].startswith(image/): reasons.append(bad-ctype:%s % (info[ctype] or empty)) if info[declared] and abs(info[declared] - info[bytes]) 1024: reasons.append(size-mismatch:%d/%d % (info[declared], info[bytes])) if info[redirects] 2: reasons.append(too-many-redirects:%d % info[redirects]) size, fmt verify_image(info[_data]) if size is None: reasons.append(decode-failed:%s % fmt) elif min(size) 16: reasons.append(suspicious-size:%dx%d % size) info.pop(_data, None) info[url] url info[ok] not reasons info[reason] ,.join(reasons) info[image_size] %dx%d % size if size else return info def main(srcpost.md, outreport.csv): urls extract_urls(src) print(待检链接: %d 条 % len(urls)) rows [] with ThreadPoolExecutor(max_workersWORKERS) as pool: futures {pool.submit(check, u): u for u in urls} for idx, fut in enumerate(as_completed(futures), 1): rows.append(fut.result()) if idx % 50 0: print(已完成 %d/%d % (idx, len(urls))) ok sum(1 for r in rows if r.get(ok)) print(通过 %d / 失败 %d % (ok, len(rows) - ok)) keys [url, ok, status, ctype, bytes, declared, image_size, redirects, elapsed, final_url, md5, reason] with open(out, w, newline, encodingutf-8-sig) as fh: writer csv.DictWriter(fh, fieldnameskeys, extrasactionignore) writer.writeheader() writer.writerows(sorted(rows, keylambda r: r.get(ok, True))) if __name__ __main__: main()几个实现上的取舍说一下。没有用 HEAD 请求前面提过很多图床对 HEAD 支持不可靠直接 GET 流式下载更稳。只下载前 8MB是为了防止某条链接指向一个异常大的文件把整个巡检拖死超过就截断后面靠解码校验兜底。Referer 写死在头部因为大多数引用场景都是带来源页的这样测出来的结论最接近真实情况如果你想测无 Referer的场景把这一行去掉再跑一遍对比即可。md5 字段是留给你做跨次比对的第一次跑出一份基线以后每次跑完和基线对比就能发现内容被悄悄换了的情况。4.3 报告怎么读失败分类与优先级脚本输出的 CSV 按ok字段排序失败的排在前面看的时候按reason分组比逐条看高效得多。我的处理优先级是这样排的request-failed和http-404最高这类链接是确定已经挂了的必须马上换http-403次之通常是防盗链需要去图床侧加白名单或者换服务bad-ctype和decode-failed排第三意味着返回的不是有效图片多半是错误页或者被替换成了别的格式size-mismatch排第四可能是传输截断也可能是缓存不一致需要复测确认suspicious-size和too-many-redirects排最后属于暂时能用但值得关注的观察项。这个优先级背后的逻辑很简单先解决确定坏掉的再处理可能坏掉的。把 200 多条失败链接按这个顺序处理比从头到尾一条条点开看能省掉一多半时间。5. 嵌入式设备端加载外链图片的额外测试点5.1 设备端的四个硬约束前面讲的都是浏览器和渲染器环境如果把同样的外链搬到嵌入式设备的界面上情况会复杂一个量级。第一个约束是根证书。设备上的 TLS 实现通常只带了一小部分根证书图床域名换了一张新证书之后设备侧就直接握手失败而你在电脑上测一切正常。这类问题排起来很快在设备上执行一次openssl s_client -connect img.example.com:443 -servername img.example.com看证书链能不能验通就行了。第二个约束是内存。一张 4000×3000 的 JPEG 在内存里解码成位图大约要 48MB这个数字很多开发板根本吃不消表现就是加载到一半崩掉或者界面卡死。第三个约束是网络超时与重试。设备端代码往往没有重试逻辑网络抖一下就是一张白图而浏览器会自己重试。第四个约束是格式支持。图床现在越来越喜欢自动返回 WebP如果你的设备端只有一个 JPEG 解码器那就是 200 拿到手也解不开。所以给设备用的外链最好在服务端侧先做一次格式和尺寸归一化别直接把原始链接丢给板子。5.2 最小联调页面与错误判读排查设备端问题时我习惯先做一个最小验证在设备上用 curl 直接请求那条外链把详细过程打出来。# 在设备端执行观察完整链路 curl -v -o /tmp/t.img -m 20 \ -w \ncode%{http_code} type%{content_type} size%{size_download} time%{time_total}\n \ https://img.example.com/2024/a1b2c3.jpg # 确认设备端解码器能不能吃下这个文件 file /tmp/t.img判读的口径卡在SSL certificate problem或者unable to get local issuer certificate就是根证书问题报Connection timed out且-m 20到点退出是网络或者 DNS 问题能下载但file命令显示 WebP、设备代码却报解码失败就是格式不支持。这套判断我在几块不同的板子上都用过基本一次就能定位到是哪一类。如果设备端连 curl 都没有退而求其次的做法是搭一个最小 HTML 页面用设备上的浏览器或者 WebView 打开同时把请求日志打到串口或者系统日志里看。6. 常见问题速查表与踩坑记录6.1 故障现象对照表下面这张表是我自己攒的遇到问题先查表能省不少时间。现象可能原因快速验证方式处理方向浏览器直开正常页面里裂图Referer 防盗链curl -e换不同来源测试加白名单或换图床电脑正常设备白屏缺根证书或不支持 TLS 版本设备上跑openssl s_client更新证书库或换协议报 200 但图不是原来的审核拦截替换成占位图比对 md5 与图像尺寸换图床或改上传策略时好时坏CDN 多节点缓存不一致--resolve逐节点对比字节数换带版本号的新文件名一段时间后全部失效用了签名临时链接检查 URL 里有无过期参数换永久链接批量过半后大面积超时触发服务端限速隔十分钟重跑降并发、加随机延迟下载字节数少于 Content-Length传输被截断重复下载对比字节数排查中间层或换链路设备报解码失败返回了 WebP 等不支持的格式设备端file看真实格式服务端统一转 JPEG6.2 几条用返工换来的经验第一条不要相信 HEAD 请求。我一开始图省事用curl -I批量测结果漏掉了好几条实际已经坏掉的链接因为那个图床对 HEAD 一律返回 200。后来全部改成 GET 才把问题暴露出来。这个教训的代价是我以为巡检已经做完了实际上漏了三分之一。第二条空 Referer 和没有 Referer 头是两回事。有些服务端的策略是允许无 Referer拒绝空 Referer这两种请求在代码里长得几乎一样结论却相反。测试的时候两种都跑一遍别只跑一种就下结论。第三条并发不要开太大。我曾经为了快把并发开到 64结果一半的请求返回超时我拿着这份报告去怀疑图床有问题查了半天才发现是自己把对方打限速了。降到 12 之后同样一批链接全部通过。这件事说明一个道理测试工具本身也会成为被测环境的一部分参数没控好测出来的结论是无效的。第四条链接地址里的中文和空格一定要先编码。有一次一批链接在浏览器里能打开脚本里全部请求失败最后发现是文件名里带了中文浏览器会自动编码而我在列表里存的是裸字符串。用urllib.parse.quote处理一遍就能解决但不知道的人能在这一步卡很久。第五条换图床的时候必须全量复测不能抽样。抽样只能告诉你大概率没问题而文档站里坏一条链接就是一个读者看到一个裂图。全量跑一次的成本用第 4 节的脚本也就几分钟远比事后一篇篇修要划算。7. 让巡检长期跑下去的几点安排7.1 定时任务与增量策略巡检这东西跑一次没意义得让它定期跑。我的安排是每天一次短巡检只测可达性不下载完整内容每周一次长巡检完整下载、校验指纹、多节点比对发布新文章之前额外跑一次针对新链接的检查。短巡检的脚本可以复用第 4 节的代码把MAX_BYTES调小、跳过verify_image就行这样一次跑完只要几十秒。定时任务本身用最朴素的方式就好系统自带的计划任务跑一个脚本输出 CSV 和一份只有失败项的摘要。摘要我建议直接推送或者写进一个固定文件里原因是巡检报告里通过的部分永远是大多数你只需要看到失败的那几条。另外报告文件按日期保留最近 30 份就够了再多的历史数据其实没人看真要溯源就看基线那份。7.2 换图床或批量改链时的全量复测清单迁移这件事我做过多轮把清单固定下来之后每次照着走就行顺序很重要先导出全部现有链接并跑一次基线巡检把已经坏掉的先排除免得后面分不清是新问题还是老问题然后在新图床按同样的目录结构上传拿到新链接之后先只替换一小批比如 10 条跑一次完整校验确认格式、尺寸、防盗链策略都符合预期确认无误后再全量替换替换完成后跑一次全量巡检重点看有无size-mismatch和bad-ctype最后把报告和旧基线对比确认没有新增失败项。跳掉中间先换 10 条这一步的代价我付过一次当时我一把梭全量替换了四百多条链接结果新图床默认把图片转成了 WebP我自己电脑上看没问题但设备端和一部分老浏览器全部显示异常最后又倒回去改了一遍配置。所以这一小步千万别省。我个人在实际操作中的体会是图床外链这事儿最大的风险从来不是链接突然打不开而是链接看起来好好的实际上已经变了。前者你会立刻发现后者可能要等几个月后读者截图来问你你才知道问题早就存在了。把基线指纹存下来、定期比对这套流程麻烦的地方只有第一次搭之后基本就是无感的。