
🗑 Limpeza programática da biblioteca de multimédia do WordPress: scripts PHP para remover ficheiros inúteis
A biblioteca de media do WordPress funciona como um sótão: elimina uma publicação, as imagens ficam. Muda de tema, os tamanhos de imagem antigos ficam como peso morto. Migra o site, metade das miniaturas devolve 404.
A limpeza manual através do painel de administração num site com alguns milhares de ficheiros é um exercício de meditação. Mas há uma forma mais rápida: cinco funções PHP que encontram e removem lixo numa única passagem. Sem plugins, com código claro e controlo sobre cada ficheiro eliminado.
Antes de executar, faça um backup completo. Estas funções eliminam ficheiros permanentemente: sem lixo, sem desfazer. Em caso de dúvida, execute primeiro numa cópia de testes.
💡 Visão geral rápida:
- Eliminar anexos não associados, ficheiros deixados para trás após a eliminação de publicações
- Limpar ficheiros de media de um tipo de publicação personalizado (CPT) específico
- Limpar a biblioteca de media de links quebrados, anexos 404 sem ficheiro no servidor
- Encontrar e eliminar ficheiros em
wp-content/uploadsnão registados como anexos do WordPress - Cenário para sites onde as imagens são armazenadas em campos personalizados (ACF, Meta Box), não como anexos
O que precisa de saber antes de executar
O código abaixo elimina ficheiros fisicamente, do disco e da base de dados. Três coisas que vão salvar o seu site.
Primeiro: as imagens em páginas de arquivo de etiquetas ou em descrições de SEO muitas vezes não estão associadas a nenhuma publicação. Ficam como "órfãs", mas o site precisa delas. Se tiver essas imagens, exclua-as do âmbito destas funções ou ajuste as condições.
Segundo: o WordPress cria vários tamanhos de cada imagem. As miniaturas herdam o post_parent do original, por isso a função delete_unattached_attachments() não lhes toca, filtra estritamente por post_parent = 0. O problema só surge se o próprio original perdeu a sua associação à publicação.
Terceiro: se existir um link para o ficheiro eliminado no conteúdo da publicação, este vai quebrar após a limpeza. Antes de executar, rastreie o site com o Screaming Frog ou similar e mapeie os links.
1. Eliminar anexos não associados
Cenário mais comum: eliminou uma publicação, os anexos permaneceram na base de dados com post_type = 'attachment' e post_parent = 0. Ocupam espaço em disco e nos backups.
A função abaixo encontra todos esses registos e elimina-os. Coloque-a no functions.php de um tema filho ou através de um plugin de snippets como o WPCode. Não será executada por si só, é uma definição que precisa de uma chamada.
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() seleciona todos os anexos sem uma publicação pai. wp_delete_attachment() com o parâmetro true apaga tanto o registo da base de dados como o ficheiro com as miniaturas. O unlink() adicional é um seguro: se o ficheiro de alguma forma permaneceu no disco, é eliminado à força.
Nota: as imagens destacadas também têm post_parent = 0 em algumas configurações. Antes da execução em produção, substitua wp_delete_attachment por echo $attachment_id . '<br>', verá a lista de IDs que serão eliminados. Depois de confirmar que está tudo correto, reverta para a versão de produção.
Após uma única execução, remova a função do functions.php. Não é necessário mantê-la em cada init.
2. Eliminar anexos de um CPT específico
Antiga loja WooCommerce, secção de portfólio antiga, tipo de publicação personalizado eliminado, todas as suas imagens continuam no servidor. A função abaixo limpa os anexos associados a publicações de um 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 }
Substitua 'card' pelo slug do seu CPT. Para produtos WooCommerce, 'product'. Se o CPT já foi eliminado, get_post_type() devolverá false, os anexos desse tipo não serão afetados. Para CPTs eliminados, a lógica precisa de ajuste: verifique não o tipo do pai, mas a pertença a uma taxonomia ou campo meta.
Em bases de dados grandes, tenha cuidado: 'numberposts' => -1 sem 'fields' => 'ids' carrega objetos WP_Post completos. Em mais de 10.000 anexos, isto pode atingir o memory_limit. Para volumes de produção, adicione 'fields' => 'ids' e obtenha apenas os IDs, o get_post_type() também funcionará com os IDs dos pais.
3. Limpar a biblioteca de media de anexos 404
As miniaturas quebradas na biblioteca de media são um sintoma de que o ficheiro no disco foi eliminado (manualmente, por falha de alojamento ou plugin com bugs), mas o registo na base de dados permaneceu. O WordPress mostra um retângulo cinzento, mas ao clicar, 404.
A função consulta o URL de cada anexo e elimina os que devolvem 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 função é intensiva em recursos. Cada chamada get_headers() é um pedido HTTP ao seu próprio servidor. Com mil anexos, faz mil pedidos HTTP numa única passagem. Resultado: lentidão, carga no servidor, alguns fornecedores de alojamento interrompem o processo por timeout.
Para bibliotecas de media grandes, divida em blocos com 'offset' e 'numberposts' ou execute via WP-CLI com um limite de lote. Se o site estiver atrás de CDN ou proxy, substitua a verificação por wp_remote_head() com um timeout, o get_headers() nem sempre lida corretamente com redirecionamentos e não suporta autenticação.
4. Verificação inversa: ficheiros nos uploads sem registo na base de dados
As três funções anteriores limpam a base de dados, eliminam registos de anexos. Mas o wp-content/uploads pode conter ficheiros que não estão registados como anexos: carregados via FTP, deixados por plugins, gerados por cache.
Esta função faz o caminho inverso: não da base de dados para os ficheiros, mas dos ficheiros para a base de dados. Percorre recursivamente o wp-content/uploads e para cada ficheiro verifica via attachment_url_to_postid() se é um anexo. Se não for, elimina-o.
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 }
Num servidor de teste com 1 GB de uploads, a função foi executada em 15 segundos e libertou 700 MB, deixando 300 MB de ficheiros realmente utilizados. Para pastas maiores que 5 GB, divida a verificação por anos: substitua $root por $uploads_dir['basedir'] . '/2025/', depois '/2024/' e assim por diante.
Primeiro execute a versão sem eliminação, substitua unlink( $image_path ) por echo $image_path . PHP_EOL. Verá a lista completa de ficheiros que a função considera lixo. Verifique visualmente e depois reverta para unlink().
5. Cenário com campos personalizados: quando as imagens não são anexos
O caso mais complexo: um site onde as imagens são armazenadas não como anexos do WordPress, mas como URLs em campos personalizados (ACF, Meta Box, campos de tema personalizados). Exemplo típico, uma livraria: capa do livro no campo bookcover, foto do autor em bookauthor_picture, imagem de lista em book_list_pictrue.
Nesta arquitetura, para todos os ficheiros nos uploads, o attachment_url_to_postid() devolverá 0. A função anterior eliminará tudo, incluindo imagens realmente utilizadas. É necessária uma abordagem diferente.
5.1. Construir uma whitelist
Primeiro, recolha os URLs de todas as imagens de todos os campos personalizados necessários. No exemplo abaixo, três CPTs e três 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 );
Num projeto real, uma livraria, esta abordagem permitiu calcular a maioria dos ficheiros de lixo e libertar uma porção significativa de espaço em disco.
5.2. Eliminar tudo o que não está na whitelist
Agora percorra o wp-content/uploads e elimine todos os ficheiros que não estão em $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 }
O método in_array() com comparação estrita num array de mais de 10.000 elementos não é o mais rápido. Para volumes de produção, substitua o array normal por um associativo: $all_good_pictures = array_fill_keys( $all_good_pictures, true ) e verifique via isset(). A diferença em 40.000 elementos, de dezenas de segundos para frações de segundo.
Como executar estas funções
Todos os snippets acima são definições de funções. Não fazem nada até que as chame. Três formas seguras de executar:
Método | Quando usar | Reversão |
|---|---|---|
WP-CLI | Limpeza única, acesso à consola | Não, apenas backup |
Hook | Sem consola, precisa de executar a partir do admin | Não, apenas backup |
Plugin de snippets (WPCode) | Armazenamento conveniente e ativar/desativar | Desativar snippet, função inativa |
Exemplo de execução única via 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, a função é executada uma vez. Após a execução, remova o snippet.
Para WP-CLI, o método recomendado para produção, guarde o código da função num ficheiro temporário e execute:
1 wp eval-file cleanup.php
Antes da limpeza, é útil ver o processo visualmente. No vídeo abaixo, uma análise passo a passo da limpeza da biblioteca de media do WordPress com métodos manuais e automáticos.
⁉️🤔 Perguntas frequentes
Os ficheiros podem ser restaurados após a eliminação?
Não. As funções usam
wp_delete_attachment()comtrueeunlink(), os ficheiros são eliminados fisicamente, ignorando o lixo. O único seguro: backup completo dos ficheiros e da base de dados antes de executar. Verifique se o seu fornecedor de alojamento tem backups diários automáticos, na Kinsta, WP Engine e SiteGround estão ativados por defeito. Isto dá um ponto de restauro adicional além do seu backup manual.
Porque é que a função não funcionou, os ficheiros permaneceram no lugar?
Razão mais comum: adicionou a definição da função ao
functions.php, mas não a chamou. Um blocofunction ... { }é apenas uma instrução. Para o código ser executado, a função precisa de ser ligada a um hook viaadd_action()ou executada manualmente via WP-CLI. Na secção "Como executar", três métodos, escolha com base no seu nível de acesso ao servidor.
A função vai eliminar miniaturas de imagens usadas se forem anexos não associados?
Não. As miniaturas (thumbnail, medium, large) têm o mesmo
post_parentque o anexo original. A função filtra estritamente porpost_parent = 0, apenas registos sem nenhuma publicação pai. Os tamanhos dos originais herdam opost_parente não entram na seleção. O problema só surge se o próprio original perdeu a sua associação, então a função irá eliminá-lo juntamente com todos os tamanhos.
E se algumas imagens estiverem em campos personalizados e outras forem anexos normais?
Combine as abordagens das secções 4 e 5. Primeiro, recolha uma whitelist dos campos personalizados (secção 5.1). Depois, ao percorrer os uploads (secção 4), para cada ficheiro, verifique ambas as condições: se o ficheiro é um anexo do WordPress via
attachment_url_to_postid()E se está presente na whitelist. Um ficheiro é eliminado apenas se nenhuma das condições for satisfeita:if ( ! $attachment_id && ! isset( $good_pictures[ $image_url ] ) ) { unlink( $image_path ); }.
Quão seguro é isto para um site WooCommerce?
O WooCommerce armazena imagens de produtos como anexos padrão do WordPress, estão associadas ao tipo de publicação
product. A função de eliminação de anexos não associados (secção 1) não lhes tocará. Mas a função para um CPT específico (secção 2), sim, se passar'product'. Para WooCommerce, o mais seguro é o método da secção 5 (whitelist): opera sobre o que é realmente usado, não sobre o que está associado. Antes de executar, exporte os IDs de todos os anexos de produtos para verificação cruzada.
O que usar no seu projeto: análise final
A escolha do método depende da arquitetura do site:
- Blog ou site de notícias padrão, as funções da secção 1 (anexos não associados) e secção 3 (anexos 404) são suficientes. Execute uma vez a cada seis meses, a biblioteca de media ficará bem.
- Site com CPTs antigos (portfólio, catálogo, classificados), adicione a secção 2. Limpe com precisão o lixo de tipos de publicação eliminados ou abandonados.
- Projeto em ACF/Meta Box com campos personalizados para imagens, a sua opção: secção 5. Recolha uma whitelist, elimine o resto. Configure uma vez e depois repita conforme necessário.
- Tudo junto e não é claro, comece com a verificação recursiva dos uploads (secção 4). Veja quanto lixo está no disco. Depois aplique as secções 1-3 e 5 seletivamente, conforme a situação.
Nenhum destes scripts substitui a higiene regular do site. Mas assim que escrever a função necessária e a guardar na documentação do projeto, poupará horas de trabalho manual na próxima auditoria.
E sim, já fez um backup.




