WordPress删除文章同步删除图片:20步完整实战代码方案
本文目录
- 为什么 WordPress 默认不删除文章里的图片
- 核心代码:把图片清理挂在 before_delete_post 钩子上
- 把代码放进 functions.php 之前,先做这几件事
- 备份当前主题
- 用子主题接管修改
- 测试环境先跑一遍
- 准备回滚预案
- 保哥踩过的坑:这几种情况要谨慎
- 进阶:把功能做成插件而不是塞进主题
- 清理历史孤儿附件的一次性脚本
- 监控与告警
- 常见问题解答
- 执行后图片只是从媒体库消失,还是物理文件也被删了?
- 用回收站恢复文章时,图片还能找回来吗?
- 能不能让管理员删文章时清图片,普通编辑不清?
- 装了WP Smush或ShortPixel这类图片优化插件会冲突吗?
- 删除文章时如何只清理图片,不清理PDF或视频?
- 这套代码会不会拖慢删除文章的响应?
- 如果删除文章后媒体库的图片仍然存在,怎么排查?
- 删除文章后能否同时清理对应的Yoast SEO或Rank Math元数据?
- 权威参考资料
WordPress删文章时,图片附件和缩略图往往留在服务器变成孤儿。本文给完整方案——用before_delete_post钩子配合wp_delete_attachment,附prepare安全加固、ACF自定义字段兼容、WooCommerce产品图共用判断、几种要谨慎处理的情况,再讲把功能做成插件、WP-CLI一次性清理历史孤儿附件和监控告警。
保哥运营WordPress站点这些年,见过太多博主忽略一个细节:在后台删掉一篇文章后,文章里上传过的图片、特色图(缩略图)以及附件记录,会原封不动地留在媒体库和服务器磁盘里。短期看不出问题,时间一长,主机空间被几千张早已用不到的图片塞满,备份越来越慢,迁站时光是图片打包就要好几个小时。
这篇文章把保哥自己用了多年的解决方案完整整理一遍:通过一段挂在 before_delete_post 钩子上的PHP代码,让WordPress在删除文章的同时,把这篇文章关联的特色图、附件、postmeta记录一并清掉。下面会从原理、代码、风险、扩展用法、孤儿附件清理脚本五个角度展开,附带踩坑案例和WP-CLI批量清理命令。
为什么 WordPress 默认不删除文章里的图片
这是一个常被误解的设计。很多人觉得WordPress漏了一个功能,但实际上保留是有意为之。
在WordPress的数据模型里,图片本身是一种 post_type='attachment' 的独立内容,它和文章是父子关系而不是从属关系。一张图片可以被多篇文章引用,也可以被插入到页面、产品、自定义文章类型里。如果删文章就连带删图,对于复用图片的站点会造成大面积丢图。
所以官方默认策略是保守:只删 wp_posts 里那一条文章记录,附件和媒体库里的图片文件保持不动,由站长自己决定是否清理。
但是对于绝大多数个人博客、企业站来说,一张图只对应一篇文章,根本不会跨文章复用。这种场景下默认行为反而成了负担:
- 媒体库越来越乱,找历史图片像在大海捞针。
- 服务器磁盘被孤儿图片吃满,云主机扩容要花真金白银——保哥见过一个10GB磁盘的VPS被3年的孤儿图片占满到98%。
- 备份文件臃肿,小水管VPS经常因为tar打包过大失败。
- SEO上还可能留下一堆指向404文章的图片URL,被搜索引擎反复抓取,浪费爬虫预算。
- WordPress媒体库自身的列表性能也会因为附件表数据膨胀而变慢,后台编辑效率下降。
搞清楚这个背景,下面的代码方案才不会用得糊里糊涂。
核心代码:把图片清理挂在 before_delete_post 钩子上
下面这段代码是保哥目前在用的版本,放进当前主题的 functions.php(建议放在子主题里,避免主题更新被覆盖)即可生效:
/**
* 删除文章时同步删除关联的特色图与图片附件
* 挂载点:before_delete_post(在文章被永久删除前触发)
*/
function zwb_delete_post_and_attachments( $post_ID ) {
global $wpdb;
// 1. 删除特色图(_thumbnail_id 指向的 attachment)
$thumbnails = $wpdb->get_results(
$wpdb->prepare(
"SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_thumbnail_id' AND post_id = %d",
$post_ID
)
);
foreach ( $thumbnails as $thumbnail ) {
wp_delete_attachment( (int) $thumbnail->meta_value, true );
}
// 2. 删除以本文为父级的所有附件(图片、PDF、视频等)
$attachments = $wpdb->get_results(
$wpdb->prepare(
"SELECT ID FROM {$wpdb->posts} WHERE post_parent = %d AND post_type = 'attachment'",
$post_ID
)
);
foreach ( $attachments as $attachment ) {
wp_delete_attachment( (int) $attachment->ID, true );
}
// 3. 兜底清掉残留的 _thumbnail_id 记录
$wpdb->delete(
$wpdb->postmeta,
array(
'post_id' => $post_ID,
'meta_key' => '_thumbnail_id',
),
array( '%d', '%s' )
);
}
add_action( 'before_delete_post', 'zwb_delete_post_and_attachments' );相比网上流传的老版本,保哥做了三处加固:
- 使用
$wpdb->prepare()拼接SQL:原始写法直接把$post_ID拼到字符串里,遇到不规范的调用方(比如自定义插件传入字符串)就有SQL注入风险,prepare是WordPress官方推荐的安全做法。 - 强制类型转换
(int):wp_delete_attachment期望整型ID,转一次能避免一些边角情况下的warning。 - 改用
$wpdb->delete()替代手写DELETE:可读性更好,参数化也更稳。
钩子选择 before_delete_post 而不是 delete_post 的原因是:前者在文章数据被清前触发,此时还能查到postmeta;如果用 after_delete_post,部分元数据已经被WordPress清掉了,会出现找得到附件但找不到关系的尴尬情况。
把代码放进 functions.php 之前,先做这几件事
动 functions.php 是有风险的,少一个分号都可能让整站白屏。保哥自己每次改之前都按这个流程走,从没翻过车:
备份当前主题
通过SSH或者宝塔面板,把整个主题目录打包:
# 假设主题路径如下,按实际情况替换
cd /www/wwwroot/example.com/wp-content/themes/
tar -czvf my-theme-backup-$(date +%Y%m%d).tar.gz my-theme/用子主题接管修改
直接改父主题的 functions.php,下次主题更新就全没了。保哥一直推荐建一个子主题,子主题的 functions.php 是叠加而不是覆盖,更新主题不会动它。子主题的最小骨架只需要两个文件:style.css(声明Template为父主题slug)和 functions.php。
测试环境先跑一遍
在本地用Local by Flywheel或Docker起一个WordPress,把代码贴进去,新建一篇带特色图和多张内文图的文章,再删掉,去媒体库确认图片是否消失,去服务器对应的 wp-content/uploads/年/月/ 目录确认物理文件是否真的没了。一定要测试"删特色图但保留正文图"和"删正文图但保留特色图"两种边界,确保你的清理逻辑没有误删。
准备回滚预案
万一改完出错,能立刻回滚。保哥的标准做法是改之前用 cp functions.php functions.php.bak 备份原文件,万一白屏就用FTP把bak文件覆盖回去,5秒钟恢复。如果连后台都进不去,用SSH直接 mv functions.php.bak functions.php 也行。
保哥踩过的坑:这几种情况要谨慎
这套代码在一图一文场景下非常稳,但下面三种情况要格外小心,否则会误删:
情况一:同一张图被多篇文章引用
如果你习惯在不同文章里复用同一张配图,这段代码不会判断这张图是否还在被别人用,它会直接删。后果是:A文章删了,B文章里的同一张图变成404。解决思路是在 wp_delete_attachment 之前加一段查询,看 post_content 里是否还有别的文章引用了这个URL,没有再删。保哥在这部分会推荐用 WP_Query 加 s 参数搜索,不要用LIKE直接扫数据库。
// 检查附件URL是否还被其它文章引用
function zwb_is_attachment_in_use( $attachment_id, $excluded_post_id ) {
$url = wp_get_attachment_url( $attachment_id );
if ( ! $url ) return false;
global $wpdb;
$count = $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status='publish' AND ID != %d AND post_content LIKE %s",
$excluded_post_id,
'%' . $wpdb->esc_like( $url ) . '%'
));
return $count > 0;
}情况二:附件被ACF自定义字段引用
用ACF(Advanced Custom Fields)的站点,图片往往不是父子关系,而是通过字段ID关联,post_parent 可能是0。这种情况下上面代码的第二段查询根本扫不到这些附件,要额外写逻辑去 wp_postmeta 里查ACF字段对应的附件ID。
情况三:CDN或对象存储独立存储图片
如果你把媒体库挂载到了七牛、阿里云OSS、又拍云这类对象存储,本地其实没有图片文件,只有 wp_posts 里的附件记录。wp_delete_attachment 触发的时候,对应插件(比如WP Stateless、阿里云OSS for WordPress)通常会监听 delete_attachment 钩子,连带把云端的对象一起删掉。建议先在云存储后台开启版本控制或回收站,万一删错可以恢复。
情况四:WooCommerce产品图共用
WooCommerce的产品图册经常多个产品共用同一张图。如果你的站点跑WooCommerce,要在删除前增加一层产品引用判断,或者干脆把这套清理逻辑限定为只对 post 类型文章生效,不动 product:
function zwb_delete_post_and_attachments( $post_ID ) {
if ( get_post_type( $post_ID ) !== 'post' ) {
return; // 只清理普通文章,跳过 product/page 等
}
// 后续清理逻辑...
}进阶:把功能做成插件而不是塞进主题
等代码经过一段时间稳定运行,保哥会建议把它从 functions.php 里挪出来,做成一个独立的mu-plugin(必装插件)。原因有三个:
- 换主题的时候功能不会丢。
- 在
wp-content/mu-plugins/下的插件不能在后台被禁用,更可靠。 - 跟主题逻辑解耦,便于后续维护和迁移。
做法很简单。在 wp-content/ 下新建 mu-plugins/ 目录,里面放一个 zwb-clean-attachments.php 文件:
<?php
/**
* Plugin Name: ZWB Clean Attachments On Post Delete
* Description: 删除文章时同步清理特色图与附件,防止媒体库堆积孤儿图片。
* Version: 1.0
* Author: 保哥
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// 直接复用前面 zwb_delete_post_and_attachments 函数体
add_action( 'before_delete_post', 'zwb_delete_post_and_attachments' );WordPress启动时会自动扫描 mu-plugins/ 目录加载,无需在后台激活。如果未来站点托管到WordPress.com这种受限平台,mu-plugin可能不被支持,需要回退到主题层方案。
清理历史孤儿附件的一次性脚本
上面的代码只能管以后删的文章,对历史上已经堆积的孤儿附件无能为力。这种情况保哥一般推荐用一次性WP-CLI脚本扫一遍:
# 列出所有 post_parent = 0 且未在任何文章 content 中出现的图片附件
wp post list --post_type=attachment --post_parent=0 --format=ids
# 拿到ID列表后,逐个判断是否为孤儿
for id in $(wp post list --post_type=attachment --post_parent=0 --format=ids); do
url=$(wp post meta get $id _wp_attached_file)
count=$(wp db query "SELECT COUNT(*) FROM wp_posts WHERE post_content LIKE '%$url%'" --skip-column-names)
if [ "$count" = "0" ]; then
echo "Orphan: $id ($url)"
# 取消下面这行注释才会真删
# wp post delete $id --force
fi
done这个清理操作非常重,一定要先做整站数据库加uploads目录的备份,跑完用 du -sh wp-content/uploads/ 对比清理前后的体积,心里就有数了。保哥曾经帮一个客户清理了 8000+ 孤儿附件,uploads目录从 28GB 降到 9GB,磁盘瞬间宽敞。
监控与告警
清理脚本上线后,保哥习惯加一段监控代码,在每次清理时记录日志:
function zwb_log_attachment_deletion( $post_ID, $count ) {
$log = sprintf(
"[%s] Post #%d deleted, %d attachments removed.\n",
current_time( 'Y-m-d H:i:s' ),
$post_ID,
$count
);
error_log( $log, 3, WP_CONTENT_DIR . '/zwb-cleanup.log' );
}把这段加到主清理函数里,所有删除动作都会留档,万一未来发现误删,能快速定位是哪个用户在哪一刻删了哪篇文章。日志放在 wp-content/ 而不是 uploads/,避免被WP的媒体库扫描收录。
更进阶的做法是配合 钉钉/飞书机器人 在每次大量删除时发告警。比如一次清理超过 50 张图片就推送通知,避免被自动化任务或被入侵者批量删文章导致媒体库大规模消失。
常见问题解答
执行后图片只是从媒体库消失,还是物理文件也被删了?
wp_delete_attachment( $id, true ) 第二个参数为true,表示强制删除,会调用 wp_delete_attachment_files() 把对应的原图、各种尺寸的缩略图以及EXIF中间文件都从磁盘上一并删除。如果只想删数据库记录、保留物理文件,把第二个参数改成false即可,但不推荐这样做,会留下更多孤儿文件。
用回收站恢复文章时,图片还能找回来吗?
找不回。before_delete_post 是文章永久删除(也就是从回收站再次点删除)时才触发的钩子,文章在被丢进回收站时不会触发,所以图片还在;但一旦从回收站清空,图片就跟文章一起永久消失了。如果你担心误操作,可以把删除前的图片先复制到一个备份目录,等观察一段时间再清理。
能不能让管理员删文章时清图片,普通编辑不清?
可以,在函数开头加一层权限判断:if ( ! current_user_can( 'manage_options' ) ) { return; } 这样只有管理员角色触发删除时才会同步清图,避免编辑误删。如果想做得更细,可以基于自定义角色或自定义能力做判断,比如 current_user_can( 'delete_attached_media' ) 这种自定义cap。
装了WP Smush或ShortPixel这类图片优化插件会冲突吗?
不会。这类插件的工作时机在上传图片时或图片被请求时,跟 before_delete_post 钩子完全不在一个生命周期。保哥自己同时跑着ShortPixel和这段清理代码,长期使用没出过任何问题。但要注意:如果你用了WP Offload Media S3这类插件把图片同步到S3,删除时会同步删除S3对象,不会留下云端孤儿。
删除文章时如何只清理图片,不清理PDF或视频?
在循环 attachments 时增加MIME类型判断:在 SQL 里加 AND post_mime_type LIKE 'image/%' 即可。这样PDF、ZIP、视频都不会被一并删除,适合那些把视频和文档作为长期资产复用的站点。
这套代码会不会拖慢删除文章的响应?
极少会。一篇文章关联的附件通常不超过20个,循环调用 wp_delete_attachment 总耗时一般在 200 到 500 毫秒之间。如果你的文章关联附件数量极大(比如图集站每篇1000+张图),建议改用WP Cron异步删除:把附件ID列表存到 transient,然后调度一个 wp_schedule_single_event 在后台慢慢删,避免阻塞用户请求。
如果删除文章后媒体库的图片仍然存在,怎么排查?
按顺序排查:1)确认 before_delete_post 钩子真的被触发了(在函数开头加 error_log('hook fired') 测试);2)确认 $post_ID 取值正确;3)确认 SQL 查询能查到附件记录(直接在phpMyAdmin跑一遍);4)确认 wp_delete_attachment 没被其他插件拦截(hook优先级冲突)。最常见的原因是用了 wp_trash_post 而不是 wp_delete_post——前者只是丢回收站,不触发本钩子。
删除文章后能否同时清理对应的Yoast SEO或Rank Math元数据?
WordPress删除文章时会自动清理所有 wp_postmeta 里 post_id 等于该文章的记录,所以Yoast和Rank Math的SEO元数据会自动一起清掉,不需要额外代码。但如果你用了独立的SEO日志表(比如自定义关键词排名追踪),需要再加一段DELETE逻辑显式清理。
权威参考资料
FAQPage + Article AI 引用友好版
WordPress默认删文章不删图片,长期累积的孤儿附件会撑爆磁盘。本文给出挂在before_delete_post钩子上的生产级PHP代码,覆盖特色图、文内图、附件全场景,并配套子主题部署SOP、ACF和WooCommerce兼容方案、WP-CLI批量清理历史孤儿、监控日志四套配套工具。
- functions.php
- WordPress删除图片
- WordPress附件
- 媒体库清理
- before_delete_post
- 跨境物流
- WordPress教程
title: WordPress删除文章同步删除图片:20步完整实战代码方案 author: 张文保 (Paul Zhang) — PatPat SEO 经理 url: https://zhangwenbao.com/wordpress-delete-article-pictures-when-deleting-articles.html published: 2018-06-09 modified: 2026-05-16 source-type: First-hand expert commentary language: zh-CN license: CC BY-NC-SA 4.0 (要求保留原文链接与作者归属)
本文标题:《WordPress删除文章同步删除图片:20步完整实战代码方案》
本文链接:https://zhangwenbao.com/wordpress-delete-article-pictures-when-deleting-articles.html
版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0