外链图片自动图床化:发文时抓回本地,以及两个必须避开的坑-第三维度MokIII

为什么外链图片不能长期用

写技术文章时,引用外部图片(别人博客的截图、GitHub 上的图)看起来很方便,但坑不少:

  • 对方一改动,你的文章就破图了(链接失效、目录改名);
  • 防盗链:很多站会对非本站 Referer 返回 403,读者看到的是碎图;
  • 对方能看到你所有访客的 IP(浏览器直接请求他们的服务器);
  • 每次都要额外建连:多一个域名 = DNS 查询 + TLS 握手,拖慢首屏;
  • https 页面里引用 http 图片会被浏览器判为混合内容,直接拦掉。

所以正确的做法是:发文时自动把外链图抓回自己的图床,并把地址换掉。

一、要处理两种位置

图片在正文里通常出现两次:

  1. <img src="外链图片"> —— 正文显示的小图;
  2. <a href="外链图片"><img …></a> —— 点击放大的灯箱链接,指向原图。

只处理第 1 种是常见疏漏:显示正常了,但读者点开大图,走的还是别人的服务器(而且很可能因为防盗链打不开)。

二、最大的坑:别用 wp_update_post() 写回

第一版我这么写:

// ❌ 会造成递归
add_action( 'transition_post_status', function ( $new, $old, $post ) {
    // ...下载、替换...
    wp_update_post( array( 'ID' => $post->ID, 'post_content' => $new_content ) );
}, 10, 3 );

wp_update_post() 会再次触发 transition_post_status,于是又进一次本函数 → 又更新一次 → 无限递归。

正确做法是直接更新数据库、并清理缓存,绕开钩子:

global $wpdb;
$wpdb->update( $wpdb->posts, array( 'post_content' => $new_content ), array( 'ID' => $post->ID ) );
clean_post_cache( $post->ID );

再加一把锁防重入:

function my_localize_external_images( $post_id ) {
    static $running = false;
    if ( $running ) { return false; }
    $running = true;
    // ...处理...
    $running = false;
    return $changed;
}

三、完整思路

add_action( 'transition_post_status', function ( $new_status, $old_status, $post ) {
    if ( ! $post instanceof WP_Post ) { return; }
    if ( 'publish' !== $new_status || 'publish' === $old_status ) { return; }  // 只在首次发布
    if ( wp_is_post_revision( $post->ID ) || wp_is_post_autosave( $post->ID ) ) { return; }

    my_localize_external_images( $post->ID );
}, 10, 3 );

处理函数里做四件事:

  1. 收集候选地址:<img src> 全部 + <a href> 里看起来是图片文件的那些(按扩展名判断,避免把普通网页链接也下载下来);
  2. 跳过本站地址(含图床域名),否则会把已经本地化的图再下载一遍;
  3. 下载:用 WordPress 自带的 media_sideload_image(),它会走媒体库、生成各尺寸缩略图,并落到你配置的图床路径;
  4. 替换地址:下载成功才替换,失败保留原地址——宁可是外链,也不要出现空 src(空 src 会让浏览器重新请求当前页面,非常糟糕)。
$path = (string) wp_parse_url( $url, PHP_URL_PATH );
if ( ! preg_match( '/\.(jpe?g|png|gif|webp|avif|bmp)$/i', $path ) ) { continue; }

$attachment_id = media_sideload_image( $url, $post_id, null, 'id' );
if ( is_wp_error( $attachment_id ) ) { continue; }   // 失败就跳过,保留原地址

$new_url = wp_get_attachment_url( $attachment_id );
$content = str_replace( $url, $new_url, $content );

四、和"链接本地化"配合

外链图还有第二种失败模式:<img src> 下载成功了,但外层 <a href> 因为防盗链下载失败——于是显示是本地的、点开还是外链。

所以我又加了一个兜底的插件,用更高的优先级在发布后执行:凡是 <a href> 指向外链图片、而它包着的 <img> 已经是本站地址的,就把 href 直接改成图片自身的地址。

两个插件的分工:

优先级干什么
10把 <img src> / <a href> 的外链图下载并替换成图床地址
20兜底:扫描剩下的"外链图片链接",回落到本地图片地址

五、怎么验证

# 发布一篇带外链图的测试文,然后查正文里还有没有外部域名

或者在数据库里直接看这篇的正文是否还残留外部图片域名。重点确认两件事:

  1. 正文和图床里都有这张图(下载成功);
  2. <a href> 和 <img src> 指向的是同一个(或同源)地址。

六、回滚

删掉插件文件即可。已经本地化过的图片不回滚——它们在媒体库里,删除插件不会让地址变回去(这通常是好事)。想彻底回到外链,只能手动改回。


一句话总结:外链图片的自动图床化本身不复杂,难在两处——写回数据库不能用 wp_update_post()(会递归),以及别忘了 <a href> 那一层(只换 src 会导致"显示正常、点开是外链")。