
🗑 Pulizia programmatica della libreria media di WordPress: script PHP contro i file spazzatura
Libreria media di WordPress funziona come una soffitta: cancelli un post, le immagini restano. Cambi tema, le vecchie dimensioni delle immagini restano lì come peso morto. Migri il sito, metà delle miniature restituisce 404.
La pulizia manuale dal pannello di amministrazione su un sito con un paio di migliaia di file è un esercizio di meditazione. Ma c'è un modo più veloce: cinque funzioni PHP che trovano e rimuovono la spazzatura in un unico passaggio. Nessun plugin, con codice chiaro e controllo su ogni file eliminato.
Prima di eseguire, backup completo. Queste funzioni eliminano i file in modo permanente: nessun cestino, nessun annullamento. In caso di dubbio, eseguire prima su una copia di staging.
💡 Panoramica rapida:
- Eliminazione degli allegati non associati, file lasciati dopo la cancellazione dei post
- Pulizia dei file media da un tipo di post personalizzato (CPT) specifico
- Pulizia della libreria media dai link non funzionanti, allegati 404 senza un file sul server
- Ricerca ed eliminazione dei file in
wp-content/uploadsnon registrati come allegati di WordPress - Scenario per siti in cui le immagini sono archiviate in campi personalizzati (ACF, Meta Box), non come allegati
Cosa devi sapere prima di eseguire
Il codice seguente elimina i file fisicamente, dal disco e dal database. Tre cose che salveranno il tuo sito.
Primo: le immagini nelle pagine di archivio tag o nelle descrizioni SEO spesso non sono collegate a nessun post. Rimangono come "orfane", ma il sito ne ha bisogno. Se hai immagini di questo tipo, escludile dall'ambito di queste funzioni o modifica le condizioni.
Secondo: WordPress crea diverse dimensioni per ogni immagine. Le miniature ereditano il post_parent dell'originale, quindi la funzione delete_unattached_attachments() non le tocca, filtra rigorosamente per post_parent = 0. Il problema sorge solo se l'originale stesso ha perso il suo collegamento al post.
Terzo: se un link al file eliminato esiste nel contenuto di un post, si romperà dopo la pulizia. Prima di eseguire, scansiona il sito con Screaming Frog o strumenti simili e mappa i link.
1. Eliminare gli allegati non associati
Scenario più comune: hai cancellato un post, gli allegati sono rimasti nel database con post_type = 'attachment' e post_parent = 0. Occupano spazio su disco e nei backup.
La funzione seguente trova tutti questi record e li elimina. Inseriscila nel functions.php di un tema child o tramite un plugin di snippet come WPCode. Non si esegue da sola, è una definizione che necessita di una chiamata.
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() seleziona tutti gli allegati senza un post padre. wp_delete_attachment() con il parametro true cancella sia il record del database che il file con le miniature. L'ulteriore unlink() è un'assicurazione: se il file è in qualche modo rimasto sul disco, viene eliminato forzatamente.
Nota: le immagini in evidenza hanno anch'esse post_parent = 0 in alcune configurazioni. Prima dell'esecuzione in produzione, sostituisci wp_delete_attachment con echo $attachment_id . '<br>', vedrai l'elenco degli ID che verranno eliminati. Una volta confermato che tutto è corretto, ripristina la versione di produzione.
Dopo una singola esecuzione, rimuovi la funzione da functions.php. Non è necessario mantenerla su ogni init.
2. Eliminare gli allegati di un CPT specifico
Ex negozio WooCommerce, vecchia sezione portfolio, tipo di post personalizzato eliminato, tutte le loro immagini continuano a risiedere sul server. La funzione seguente pulisce gli allegati collegati ai post di un tipo specificato.
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 }
Sostituisci 'card' con lo slug del tuo CPT. Per i prodotti WooCommerce, 'product'. Se il CPT è già stato eliminato, get_post_type() restituirà false, gli allegati di quel tipo non saranno interessati. Per i CPT eliminati, la logica necessita di un aggiustamento: verificare non il tipo del padre, ma l'appartenenza a una tassonomia o a un meta campo.
Su database di grandi dimensioni, fai attenzione: 'numberposts' => -1 senza 'fields' => 'ids' carica oggetti WP_Post completi. Su oltre 10.000 allegati questo può raggiungere il memory_limit. Per volumi di produzione, aggiungi 'fields' => 'ids' e ottieni solo gli ID, get_post_type() funzionerà anche con gli ID padre.
3. Pulire la libreria media dagli allegati 404
Miniature rotte nella libreria media sono un sintomo che il file su disco è stato eliminato (manualmente, per crash dell'hosting o plugin difettoso), ma il record del database è rimasto. WordPress mostra un rettangolo grigio, ma al click, 404.
La funzione interroga l'URL di ogni allegato ed elimina quelli che restituiscono 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: questa funzione è dispendiosa in termini di risorse. Ogni chiamata get_headers() è una richiesta HTTP al tuo stesso server. Su mille allegati effettui mille richieste HTTP in un solo passaggio. Risultato: lentezza, carico del server, alcuni provider di hosting terminano il processo per timeout.
Per librerie media di grandi dimensioni, suddividi in blocchi con 'offset' e 'numberposts' oppure esegui tramite WP-CLI con un limite batch. Se il sito è dietro CDN o proxy, sostituisci il controllo con wp_remote_head() con un timeout, get_headers() non sempre gestisce correttamente i redirect e non supporta l'autenticazione.
4. Verifica inversa: file negli upload senza un record nel database
Le tre funzioni precedenti puliscono il database, eliminano i record degli allegati. Ma wp-content/uploads potrebbe contenere file che non sono affatto registrati come allegati: caricati via FTP, lasciati da plugin, generati dalla cache.
Questa funzione procede in modo opposto: non dal database ai file, ma dai file al database. Scansiona ricorsivamente wp-content/uploads e per ogni file verifica tramite attachment_url_to_postid() se è un allegato. In caso contrario, 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 }
Su un server di test con 1 GB di upload, la funzione è stata eseguita in 15 secondi e ha liberato 700 MB, lasciando 300 MB di file effettivamente utilizzati. Per cartelle superiori a 5 GB, suddividi la scansione per anni: sostituisci $root con $uploads_dir['basedir'] . '/2025/', poi '/2024/' e così via.
Prima esegui la versione senza eliminazione, sostituisci unlink( $image_path ) con echo $image_path . PHP_EOL. Vedrai l'elenco completo dei file che la funzione considera spazzatura. Controlla visivamente, poi ripristina unlink().
5. Scenario con campi personalizzati: quando le immagini non sono allegati
Il caso più complesso: un sito in cui le immagini non sono archiviate come allegati di WordPress, ma come URL in campi personalizzati (ACF, Meta Box, campi tema personalizzati). Esempio tipico, un negozio di libri: copertina del libro nel campo bookcover, foto dell'autore in bookauthor_picture, immagine di elenco in book_list_pictrue.
In questa architettura, per tutti i file negli upload attachment_url_to_postid() restituirà 0. La funzione precedente eliminerà tutto, incluse le immagini effettivamente utilizzate. È necessario un approccio diverso.
5.1. Costruire una whitelist
Per prima cosa raccogli gli URL di tutte le immagini da tutti i campi personalizzati necessari. Nell'esempio seguente, tre CPT e tre campi:
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 );
Su un progetto reale, un negozio di libri, questo approccio ha permesso di calcolare la maggior parte dei file spazzatura e liberare una porzione significativa di spazio su disco.
5.2. Eliminare tutto ciò che non è nella whitelist
Ora percorri wp-content/uploads ed elimina ogni file non presente in $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 }
Il metodo in_array() con confronto stretto su un array di oltre 10.000 elementi non è il più veloce. Per volumi di produzione, sostituisci l'array normale con uno associativo: $all_good_pictures = array_fill_keys( $all_good_pictures, true ) e verifica tramite isset(). La differenza su 40.000 elementi, da decine di secondi a frazioni di secondo.
Come eseguire queste funzioni
Tutti gli snippet sopra sono definizioni di funzioni. Non fanno nulla finché non le chiami. Tre modi sicuri per eseguirle:
Metodo | Quando usarlo | Rollback |
|---|---|---|
WP-CLI | Pulizia una tantum, accesso alla console | No, solo backup |
Hook | Nessuna console, necessità di eseguire da admin | No, solo backup |
Plugin di snippet (WPCode) | Archiviazione comoda e abilitazione/disabilitazione | Disabilita snippet, funzione inattiva |
Esempio di esecuzione una tantum tramite 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 }
Visita https://iltuosito.com/wp-admin/?cleanup=confirmed, la funzione viene eseguita una volta. Dopo l'esecuzione, rimuovi lo snippet.
Per WP-CLI, il metodo consigliato per la produzione, salva il codice della funzione in un file temporaneo ed esegui:
1 wp eval-file cleanup.php
Prima della pulizia, è utile vedere il processo visivamente. Nel video qui sotto, un'analisi passo passo della pulizia della libreria media di WordPress con metodi manuali e automatici.
⁉️🤔 Domande frequenti
I file possono essere ripristinati dopo l'eliminazione?
No. Le funzioni usano
wp_delete_attachment()contrueeunlink(), i file vengono eliminati fisicamente, bypassando il cestino. L'unica assicurazione: backup completo di file e database prima dell'esecuzione. Verifica se il tuo provider di hosting dispone di backup giornalieri automatici, su Kinsta, WP Engine e SiteGround sono abilitati per impostazione predefinita. Questo fornisce un punto di ripristino aggiuntivo oltre al tuo backup manuale.
Perché la funzione non ha funzionato, i file sono rimasti al loro posto?
Motivo più comune: hai aggiunto la definizione della funzione a
functions.php, ma non l'hai chiamata. Un bloccofunction ... { }è solo un'istruzione. Perché il codice venga eseguito, la funzione deve essere agganciata a un hook tramiteadd_action()o eseguita manualmente tramite WP-CLI. Nella sezione "Come eseguire", tre metodi, scegli in base al tuo livello di accesso al server.
La funzione eliminerà le miniature delle immagini utilizzate se sono allegati non associati?
No. Le miniature (thumbnail, medium, large) hanno lo stesso
post_parentdell'allegato originale. La funzione filtra rigorosamente perpost_parent = 0, solo record senza alcun post padre. Le dimensioni degli originali ereditanopost_parente non rientrano nella selezione. Il problema sorge solo se l'originale stesso ha perso il suo collegamento, allora la funzione lo eliminerà insieme a tutte le dimensioni.
Cosa succede se alcune immagini sono in campi personalizzati e altre sono allegati regolari?
Combina gli approcci delle sezioni 4 e 5. Prima raccogli una whitelist dai campi personalizzati (sezione 5.1). Poi quando scansioni gli upload (sezione 4) per ogni file, verifica entrambe le condizioni: il file è un allegato WordPress tramite
attachment_url_to_postid()ED è presente nella whitelist. Un file viene eliminato solo se nessuna delle due condizioni è soddisfatta:if ( ! $attachment_id && ! isset( $good_pictures[ $image_url ] ) ) { unlink( $image_path ); }.
Quanto è sicuro per un sito WooCommerce?
WooCommerce archivia le immagini dei prodotti come allegati standard di WordPress, sono collegate al tipo di post
product. La funzione di eliminazione degli allegati non associati (sezione 1) non le toccherà. Ma la funzione per un CPT specifico (sezione 2), sì, se passi'product'. Per WooCommerce il metodo più sicuro è quello della sezione 5 (whitelist): opera su ciò che è effettivamente utilizzato, non su ciò che è collegato. Prima di eseguire, esporta gli ID di tutti gli allegati dei prodotti per un controllo incrociato.
Cosa usare sul tuo progetto: riepilogo finale
La scelta del metodo dipende dall'architettura del sito:
- Blog o sito di notizie standard, le funzioni della sezione 1 (allegati non associati) e della sezione 3 (allegati 404) sono sufficienti. Esegui una volta ogni sei mesi, la libreria media sarà a posto.
- Sito con vecchi CPT (portfolio, catalogo, annunci), aggiungi la sezione 2. Pulisci con precisione la spazzatura da tipi di post eliminati o abbandonati.
- Progetto su ACF/Meta Box con campi personalizzati per le immagini, la tua opzione: sezione 5. Raccogli una whitelist, elimina il resto. Configura una volta, poi ripeti semplicemente al bisogno.
- Tutto insieme e non chiaro, inizia con la scansione ricorsiva degli upload (sezione 4). Vedi quanta spazzatura risiede sul disco. Poi applica le sezioni 1-3 e 5 selettivamente in base alla situazione.
Nessuno di questi script sostituisce una regolare igiene del sito. Ma una volta scritta la funzione necessaria e salvata nella documentazione di progetto, risparmierai ore di lavoro manuale al prossimo audit.
E sì, hai già fatto un backup.




