
🗑 Limpieza programática de la biblioteca de medios de WordPress: scripts PHP para eliminar archivos basura
La biblioteca de medios de WordPress funciona como un desván: usted elimina una entrada, las imágenes se quedan. Cambia de tema, los tamaños de imagen antiguos permanecen como peso muerto. Migra el sitio, la mitad de las miniaturas devuelven error 404.
La limpieza manual desde el panel de administración en un sitio con un par de miles de archivos es un ejercicio de meditación. Pero hay una forma más rápida: cinco funciones PHP que encuentran y eliminan la basura en una sola pasada. Sin plugins, con código claro y control sobre cada archivo borrado.
Antes de ejecutar, haga una copia de seguridad completa. Estas funciones eliminan archivos de forma permanente: no hay papelera, no hay deshacer. Si tiene dudas, ejecute primero en una copia de pruebas.
💡 Resumen rápido:
- Eliminar adjuntos no vinculados, archivos que quedan tras borrar una entrada
- Limpiar archivos multimedia de un tipo de contenido personalizado (CPT) específico
- Limpiar la biblioteca de medios de enlaces rotos, adjuntos 404 sin archivo en el servidor
- Encontrar y eliminar archivos en
wp-content/uploadsno registrados como adjuntos de WordPress - Escenario para sitios donde las imágenes se almacenan en campos personalizados (ACF, Meta Box), no como adjuntos
Lo que necesita saber antes de ejecutar
El código que sigue elimina archivos físicamente, del disco y de la base de datos. Tres cosas que salvarán su sitio.
Primero: las imágenes en páginas de archivo de etiquetas o en descripciones SEO a menudo no están adjuntas a ninguna entrada. Quedan como «huérfanas», pero el sitio las necesita. Si tiene este tipo de imágenes, exclúyalas del alcance de estas funciones o ajuste las condiciones.
Segundo: WordPress crea varios tamaños de cada imagen. Las miniaturas heredan el post_parent del original, por lo que la función delete_unattached_attachments() no las toca, filtra estrictamente por post_parent = 0. El problema solo surge si el propio original perdió su adjunto a la entrada.
Tercero: si existe un enlace al archivo eliminado en el contenido de una entrada, se romperá tras la limpieza. Antes de ejecutar, rastree el sitio con Screaming Frog o similar y mapee los enlaces.
1. Eliminar adjuntos no vinculados
El escenario más común: usted eliminó una entrada, los adjuntos permanecieron en la base de datos con post_type = 'attachment' y post_parent = 0. Ocupan espacio en disco y en las copias de seguridad.
La función siguiente encuentra todos esos registros y los elimina. Colóquela en el functions.php de un tema hijo o mediante un plugin de fragmentos como WPCode. No se ejecutará por sí sola, es una definición que necesita una llamada.
1 function delete_unattached_attachments() { 2 $attachments = get_posts( array( 3 'post_type' => 'attachment', 4 'numberposts' => -1, 5 'fields' => 'ids', 6 'post_parent' => 0, 7 ) ); 8 9 if ( $attachments ) { 10 foreach ( $attachments as $attachment_id ) { 11 $attachment_path = get_attached_file( $attachment_id ); 12 wp_delete_attachment( $attachment_id, true ); 13 unlink( $attachment_path ); 14 } 15 } 16 }
get_posts() selecciona todos los adjuntos sin una entrada padre. wp_delete_attachment() con el parámetro true borra tanto el registro de la base de datos como el archivo con sus miniaturas. El unlink() adicional es un seguro: si el archivo de alguna manera permaneció en disco, se elimina forzosamente.
Nota: las imágenes destacadas también tienen post_parent = 0 en algunas configuraciones. Antes de la ejecución en producción, reemplace wp_delete_attachment por echo $attachment_id . '<br>', verá la lista de IDs que se eliminarán. Una vez confirmado que todo es correcto, vuelva a la versión de producción.
Después de una sola ejecución, elimine la función de functions.php. No necesita mantenerla en cada init.
2. Eliminar adjuntos de un CPT específico
Una antigua tienda WooCommerce, una sección de portafolio vieja, un tipo de contenido personalizado eliminado: todas sus imágenes siguen ocupando espacio en el servidor. La función siguiente limpia los adjuntos vinculados a entradas de un tipo especificado.
1 function delete_cpt_attachments( $cpt = 'card' ) { 2 $attachments = get_posts( array( 3 'post_type' => 'attachment', 4 'numberposts' => -1, 5 ) ); 6 7 if ( $attachments ) { 8 foreach ( $attachments as $attachment ) { 9 $parent_id = $attachment->post_parent; 10 11 if ( $cpt === get_post_type( $parent_id ) ) { 12 $attachment_path = get_attached_file( $attachment->ID ); 13 wp_delete_attachment( $attachment->ID, true ); 14 unlink( $attachment_path ); 15 } 16 } 17 } 18 }
Reemplace 'card' por el slug de su CPT. Para productos WooCommerce, 'product'. Si el CPT ya está eliminado, get_post_type() devolverá false, los adjuntos de ese tipo no se verán afectados. Para CPTs eliminados, la lógica necesita un ajuste: verifique no el tipo del padre, sino la pertenencia a una taxonomía o un metadato.
En bases de datos grandes, tenga cuidado: 'numberposts' => -1 sin 'fields' => 'ids' carga objetos WP_Post completos. Con más de 10 000 adjuntos esto puede alcanzar el memory_limit. Para volúmenes de producción, añada 'fields' => 'ids' y obtenga solo IDs, get_post_type() también funcionará con los IDs padre.
3. Limpiar la biblioteca de medios de adjuntos 404
Las miniaturas rotas en la biblioteca de medios son un síntoma de que el archivo en disco se eliminó (manualmente, por un fallo del hosting o un plugin defectuoso), pero el registro de la base de datos permaneció. WordPress muestra un rectángulo gris, pero al hacer clic, error 404.
La función consulta la URL de cada adjunto y elimina aquellos que devuelven 404.
1 function delete_404_attachments() { 2 $attachments = get_posts( array( 3 'post_type' => 'attachment', 4 'numberposts' => -1, 5 'fields' => 'ids', 6 ) ); 7 8 if ( $attachments ) { 9 foreach ( $attachments as $attachment_id ) { 10 $file_url = wp_get_attachment_url( $attachment_id ); 11 $file_headers = @get_headers( $file_url ); 12 13 if ( $file_headers && strpos( $file_headers[0], '404' ) !== false ) { 14 wp_delete_attachment( $attachment_id, true ); 15 } 16 } 17 } 18 }
Importante: esta función consume muchos recursos. Cada llamada a get_headers() es una solicitud HTTP a su propio servidor. Con mil adjuntos usted realiza mil solicitudes HTTP en una sola pasada. Resultado: lentitud, carga del servidor, algunos proveedores de hosting matan el proceso por tiempo de espera.
Para bibliotecas de medios grandes, divida en lotes con 'offset' y 'numberposts' o ejecute mediante WP-CLI con un límite de lote. Si el sitio está detrás de CDN o proxy, reemplace la comprobación con wp_remote_head() con un tiempo de espera, get_headers() no siempre maneja correctamente las redirecciones y no admite autenticación.
4. Comprobación inversa: archivos en uploads sin registro en la base de datos
Las tres funciones anteriores limpian la base de datos, eliminan registros de adjuntos. Pero wp-content/uploads puede contener archivos que no están registrados como adjuntos en absoluto: subidos por FTP, dejados por plugins, generados por caché.
Esta función va en sentido contrario: no de la base de datos a los archivos, sino de los archivos a la base de datos. Escanea recursivamente wp-content/uploads y para cada archivo verifica mediante attachment_url_to_postid() si es un adjunto. Si no lo es, lo elimina.
1 function clean_uploads_from_nonattachments() { 2 $uploads_dir = wp_upload_dir(); 3 $search = $uploads_dir['basedir']; 4 $replace = $uploads_dir['baseurl']; 5 $root = $uploads_dir['basedir']; 6 7 $iter = new RecursiveIteratorIterator( 8 new RecursiveDirectoryIterator( $root, RecursiveDirectoryIterator::SKIP_DOTS ), 9 RecursiveIteratorIterator::SELF_FIRST, 10 RecursiveIteratorIterator::CATCH_GET_CHILD 11 ); 12 13 foreach ( $iter as $fileinfo ) { 14 if ( $fileinfo->isFile() ) { 15 $image_path = $fileinfo->getPathname(); 16 $image_url = str_replace( $search, $replace, $image_path ); 17 $attachment_id = attachment_url_to_postid( $image_url ); 18 19 if ( ! $attachment_id ) { 20 unlink( $image_path ); 21 } 22 } 23 } 24 }
En un servidor de pruebas con 1 GB de uploads, la función se ejecutó en 15 segundos y liberó 700 MB, dejando 300 MB de archivos realmente utilizados. Para carpetas de más de 5 GB, divida el escaneo por años: reemplace $root por $uploads_dir['basedir'] . '/2025/', luego '/2024/' y así sucesivamente.
Primero ejecute la versión sin eliminación, reemplace unlink( $image_path ) por echo $image_path . PHP_EOL. Verá la lista completa de archivos que la función considera basura. Revise visualmente, luego vuelva a unlink().
5. Escenario con campos personalizados: cuando las imágenes no son adjuntos
El caso más complejo: un sitio donde las imágenes no se almacenan como adjuntos de WordPress, sino como URLs en campos personalizados (ACF, Meta Box, campos de temas personalizados). Ejemplo típico, una librería: portada del libro en el campo bookcover, foto del autor en bookauthor_picture, imagen de listado en book_list_pictrue.
En esta arquitectura, para todos los archivos en uploads attachment_url_to_postid() devolverá 0. La función anterior lo eliminará todo, incluidas las imágenes realmente utilizadas. Se necesita un enfoque diferente.
5.1. Construir una lista blanca
Primero recopile las URLs de todas las imágenes de todos los campos personalizados necesarios. En el ejemplo siguiente, tres CPTs y tres campos:
1 $all_good_pictures = array(); 2 3 // Book covers (CPT 'post', field 'bookcover') 4 $posts = get_posts( array( 5 'post_type' => 'post', 6 'posts_per_page' => -1, 7 'post_status' => 'any', 8 'fields' => 'ids', 9 ) ); 10 foreach ( $posts as $post_id ) { 11 $cover = get_field( 'bookcover', $post_id ); 12 if ( $cover ) { 13 $all_good_pictures[] = $cover; 14 } 15 } 16 17 // List images (CPT 'book_list', field 'book_list_pictrue') 18 $lists = get_posts( array( 19 'post_type' => 'book_list', 20 'posts_per_page' => -1, 21 'post_status' => 'any', 22 'fields' => 'ids', 23 ) ); 24 foreach ( $lists as $list_id ) { 25 $pic = get_field( 'book_list_pictrue', $list_id ); 26 if ( $pic ) { 27 $all_good_pictures[] = $pic; 28 } 29 } 30 31 // Author photos (CPT 'bookauthor', field 'bookauthor_picture') 32 $authors = get_posts( array( 33 'post_type' => 'bookauthor', 34 'posts_per_page' => -1, 35 'post_status' => 'any', 36 'fields' => 'ids', 37 ) ); 38 foreach ( $authors as $author_id ) { 39 $pic = get_field( 'bookauthor_picture', $author_id ); 40 if ( $pic ) { 41 $all_good_pictures[] = $pic; 42 } 43 } 44 45 $all_good_pictures = array_filter( $all_good_pictures );
En un proyecto real, una librería, este enfoque permitió calcular la mayoría de los archivos basura y liberar una porción significativa del espacio en disco.
5.2. Eliminar todo lo que no esté en la lista blanca
Ahora recorra wp-content/uploads y elimine cada archivo que no esté en $all_good_pictures:
1 $uploads_dir = wp_upload_dir(); 2 $search = $uploads_dir['basedir']; 3 $replace = $uploads_dir['baseurl']; 4 $root = $uploads_dir['basedir']; 5 6 $iter = new RecursiveIteratorIterator( 7 new RecursiveDirectoryIterator( $root, RecursiveDirectoryIterator::SKIP_DOTS ), 8 RecursiveIteratorIterator::SELF_FIRST, 9 RecursiveIteratorIterator::CATCH_GET_CHILD 10 ); 11 12 foreach ( $iter as $fileinfo ) { 13 if ( $fileinfo->isFile() ) { 14 $image_path = $fileinfo->getPathname(); 15 $image_url = str_replace( $search, $replace, $image_path ); 16 17 if ( ! in_array( $image_url, $all_good_pictures, true ) ) { 18 unlink( $image_path ); 19 } 20 } 21 }
El método in_array() con comparación estricta en un array de más de 10 000 elementos no es el más rápido. Para volúmenes de producción, reemplace el array regular por uno asociativo: $all_good_pictures = array_fill_keys( $all_good_pictures, true ) y verifique mediante isset(). La diferencia en 40 000 elementos va de decenas de segundos a fracciones de segundo.
Cómo ejecutar estas funciones
Todos los fragmentos anteriores son definiciones de funciones. No hacen nada hasta que usted las llama. Tres formas seguras de ejecutarlas:
Método | Cuándo usarlo | Vuelta atrás |
|---|---|---|
WP-CLI | Limpieza puntual, acceso a consola | No, solo copia de seguridad |
Hook | Sin consola, necesita ejecutar desde el admin | No, solo copia de seguridad |
Plugin de fragmentos (WPCode) | Almacenamiento cómodo y activar/desactivar | Desactivar fragmento, función inactiva |
Ejemplo de ejecución puntual mediante admin:
1 add_action( 'admin_init', 'run_cleanup_once' ); 2 function run_cleanup_once() { 3 if ( isset( $_GET['cleanup'] ) && 'confirmed' === $_GET['cleanup'] ) { 4 delete_unattached_attachments(); 5 } 6 }
Visite https://yoursite.com/wp-admin/?cleanup=confirmed, la función se ejecuta una vez. Después de la ejecución, elimine el fragmento.
Para WP-CLI, el método recomendado para producción, guarde el código de la función en un archivo temporal y ejecute:
1 wp eval-file cleanup.php
Antes de la limpieza, es útil ver el proceso visualmente. En el vídeo a continuación, un desglose paso a paso de la limpieza de la biblioteca de medios de WordPress con métodos manuales y automáticos.
⁉️🤔 Preguntas frecuentes
¿Se pueden restaurar los archivos después de eliminarlos?
No. Las funciones usan
wp_delete_attachment()contrueyunlink(), los archivos se eliminan físicamente, sin pasar por la papelera. El único seguro: copia de seguridad completa de archivos y base de datos antes de ejecutar. Verifique si su proveedor de hosting tiene copias de seguridad diarias automáticas, en Kinsta, WP Engine y SiteGround están activadas por defecto. Esto le da un punto de restauración adicional además de su copia manual.
¿Por qué no funcionó la función, los archivos permanecieron en su lugar?
La razón más común: añadió la definición de la función a
functions.php, pero no la llamó. Un bloquefunction ... { }es solo una instrucción. Para que el código se ejecute, la función necesita engancharse a un hook medianteadd_action()o ejecutarse manualmente mediante WP-CLI. En la sección «Cómo ejecutar», tres métodos, elija según su nivel de acceso al servidor.
¿La función eliminará las miniaturas de imágenes utilizadas si son adjuntos no vinculados?
No. Las miniaturas (thumbnail, medium, large) tienen el mismo
post_parentque el adjunto original. La función filtra estrictamente porpost_parent = 0, solo registros sin ninguna entrada padre. Los tamaños de los originales heredanpost_parenty no entran en la selección. El problema solo surge si el propio original perdió su adjunto, entonces la función lo eliminará junto con todos sus tamaños.
¿Qué pasa si algunas imágenes están en campos personalizados y otras son adjuntos regulares?
Combine los enfoques de las secciones 4 y 5. Primero recopile una lista blanca de los campos personalizados (sección 5.1). Luego, al escanear uploads (sección 4), para cada archivo verifique ambas condiciones: si el archivo es un adjunto de WordPress mediante
attachment_url_to_postid()Y si está presente en la lista blanca. Un archivo se elimina solo si no cumple ninguna condición:if ( ! $attachment_id && ! isset( $good_pictures[ $image_url ] ) ) { unlink( $image_path ); }.
¿Qué tan seguro es esto para un sitio WooCommerce?
WooCommerce almacena las imágenes de producto como adjuntos estándar de WordPress, están adjuntas al tipo de contenido
product. La función de eliminación de adjuntos no vinculados (sección 1) no las tocará. Pero la función para un CPT específico (sección 2), sí, si usted pasa'product'. Para WooCommerce lo más seguro es el método de la sección 5 (lista blanca): opera sobre lo que realmente se usa, no sobre lo que está adjunto. Antes de ejecutar, exporte los IDs de todos los adjuntos de producto para una verificación cruzada.
Qué usar en su proyecto: desglose final
La elección del método depende de la arquitectura del sitio:
- Blog o sitio de noticias estándar, las funciones de la sección 1 (adjuntos no vinculados) y la sección 3 (adjuntos 404) son suficientes. Ejecute una vez cada seis meses, la biblioteca de medios estará bien.
- Sitio con CPTs antiguos (portafolio, catálogo, anuncios clasificados), añada la sección 2. Limpie con precisión la basura de tipos de contenido eliminados o abandonados.
- Proyecto sobre ACF/Meta Box con campos personalizados para imágenes, su opción: sección 5. Recopile una lista blanca, elimine el resto. Configúrelo una vez, luego solo repita según sea necesario.
- Todo junto y no está claro, comience con el escaneo recursivo de uploads (sección 4). Vea cuánta basura ocupa el disco. Luego aplique las secciones 1-3 y 5 selectivamente según la situación.
Ninguno de estos scripts reemplaza la higiene regular del sitio. Pero una vez que escriba la función necesaria y la guarde en la documentación del proyecto, se ahorrará horas de trabajo manual en la próxima auditoría.
Y sí, ya hizo una copia de seguridad.




