Skip to content

Allt om WordPress, webbutveckling — och mer därtill

🗑 Programmatisk rensning av WordPress mediebibliotek: PHP-skript för att ta bort skräpfiler

🗑 Programmatisk rensning av WordPress mediebibliotek: PHP-skript för att ta bort skräpfiler

WordPress mediabibliotek fungerar som en vind: du raderar ett inlägg, bilderna ligger kvar. Byter du tema ligger gamla bildstorlekar kvar som dödvikt. Migrerar du sajten returnerar hälften av miniatyrerna 404.

Manuell rensning via adminpanelen på en sajt med ett par tusen filer är en övning i meditation. Men det finns ett snabbare sätt: fem PHP-funktioner som hittar och tar bort skräp i en enda körning. Inga plugins, med tydlig kod och kontroll över varje raderad fil.

Gör en fullständig backup innan du kör. Dessa funktioner raderar filer permanent: ingen papperskorg, ingen ångra. Kör på en staging-kopia först om du är osäker.

💡 Snabb översikt:

  • Radera oanslutna bilagor, filer som blivit kvar efter att inlägg raderats
  • Rensa mediafiler från en specifik anpassad inläggstyp (CPT)
  • Rensa mediebiblioteket från trasiga länkar, 404-bilagor utan en fil på servern
  • Hitta och radera filer i wp-content/uploads som inte är registrerade som WordPress-bilagor
  • Scenario för sajter där bilder lagras i anpassade fält (ACF, Meta Box), inte som bilagor

Vad du behöver veta innan du kör

Koden nedan raderar filer fysiskt, från disk och databas. Tre saker som kommer att rädda din sajt.

För det första: bilder på taggarkivsidor eller i SEO-beskrivningar är ofta inte kopplade till något inlägg. De ligger som "föräldralösa", men sajten behöver dem. Om du har sådana bilder, exkludera dem från omfattningen av dessa funktioner eller justera villkoren.

För det andra: WordPress skapar flera storlekar av varje bild. Miniatyrer ärver post_parent från originalet, så funktionen delete_unattached_attachments() rör dem inte, den filtrerar strikt på post_parent = 0. Problemet uppstår bara om originalet självt har förlorat sin koppling till inlägget.

För det tredje: om en länk till den raderade filen finns i inläggsinnehåll kommer den att gå sönder efter rensningen. Innan du kör, genomsök sajten med Screaming Frog eller liknande och kartlägg länkarna.

1. Radera oanslutna bilagor

Vanligaste scenariot: du raderade ett inlägg, bilagorna blev kvar i databasen med post_type = 'attachment' och post_parent = 0. De tar upp plats på disk och i backuper.

Funktionen nedan hittar alla sådana poster och raderar dem. Placera den i functions.php i ett child theme eller via ett snippet-plugin som WPCode. Den körs inte av sig själv, det är en definition som behöver ett anrop.

1function 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() väljer alla bilagor utan ett föräldrainlägg. wp_delete_attachment() med parametern true raderar både databasposten och filen med miniatyrer. Extra unlink() är en försäkring: om filen av någon anledning blev kvar på disken raderas den tvångsmässigt.

Notera: utvalda bilder har också post_parent = 0 i vissa konfigurationer. Innan produktionskörning, ersätt wp_delete_attachment med echo $attachment_id . '<br>', så ser du listan med ID:n som kommer att raderas. När du har bekräftat att allt är korrekt, återgå till produktionsversionen.

Efter en enda körning, ta bort funktionen från functions.php. Ingen anledning att ha den kvar på varje init.

2. Radera bilagor från en specifik CPT

Före detta WooCommerce-butik, gammal portföljsektion, raderad anpassad inläggstyp, alla deras bilder ligger kvar på servern. Funktionen nedan rensar bilagor kopplade till inlägg av en angiven typ.

1function 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}

Ersätt 'card' med din CPT-slug. För WooCommerce-produkter, 'product'. Om CPT:n redan är raderad kommer get_post_type() att returnera false, bilagor av den typen påverkas inte. För raderade CPT:er behöver logiken justeras: kontrollera inte förälderns typ, utan medlemskap i en taxonomi eller ett metafält.

Var försiktig på stora databaser: 'numberposts' => -1 utan 'fields' => 'ids' laddar fullständiga WP_Post-objekt. På 10 000+ bilagor kan detta slå i memory_limit. För produktionsvolymer, lägg till 'fields' => 'ids' och hämta bara ID:n, get_post_type() fungerar också med förälder-ID:n.

3. Rensa mediebiblioteket från 404-bilagor

Trasiga miniatyrer i mediebiblioteket är ett symptom på att filen på disken har raderats (manuellt, av en hostingkrasch eller ett buggigt plugin), men databasposten finns kvar. WordPress visar en grå rektangel, men vid klick blir det 404.

Funktionen frågar varje bilagas URL och raderar de som returnerar 404.

1function 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}

Viktigt: denna funktion är resurskrävande. Varje get_headers()-anrop är en HTTP-förfrågan till din egen server. På tusen bilagor gör du tusen HTTP-förfrågningar i en körning. Resultat: långsamt, serverbelastning, vissa webbhotell dödar processen via timeout.

För stora mediebibliotek, dela upp i bitar med 'offset' och 'numberposts' eller kör via WP-CLI med en batchgräns. Om sajten ligger bakom CDN eller proxy, ersätt kontrollen med wp_remote_head() med en timeout, get_headers() hanterar inte alltid omdirigeringar korrekt och stöder inte autentisering.

4. Omvänd kontroll: filer i uploads utan en databaspost

De tre föregående funktionerna rensar databasen, raderar bilageposter. Men wp-content/uploads kan innehålla filer som inte alls är registrerade som bilagor: uppladdade via FTP, kvarlämnade av plugins, genererade av cache.

Denna funktion går motsatt väg: inte från databas till filer, utan från filer till databas. Söker rekursivt igenom wp-content/uploads och för varje fil kontrollerar den via attachment_url_to_postid() om det är en bilaga. Om inte, raderas den.

1function 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}

På en testserver med 1 GB uppladdningar körde funktionen på 15 sekunder och frigjorde 700 MB, och lämnade kvar 300 MB faktiskt använda filer. För mappar större än 5 GB, dela upp skanningen per år: ersätt $root med $uploads_dir['basedir'] . '/2025/', sedan '/2024/' och så vidare.

Kör först versionen utan radering, ersätt unlink( $image_path ) med echo $image_path . PHP_EOL. Du ser hela listan med filer som funktionen betraktar som skräp. Kontrollera visuellt och återgå sedan till unlink().

5. Scenario med anpassade fält: när bilder inte är bilagor

Det mest komplexa fallet: en sajt där bilder inte lagras som WordPress-bilagor, utan som URL:er i anpassade fält (ACF, Meta Box, anpassade temafält). Typiskt exempel, en bokhandel: bokomslag i fältet bookcover, författarfoto i bookauthor_picture, listbild i book_list_pictrue.

I denna arkitektur kommer attachment_url_to_postid() att returnera 0 för alla filer i uploads. Den föregående funktionen kommer att radera allt, inklusive faktiskt använda bilder. En annan approach behövs.

5.1. Bygga en vitlista

Samla först URL:er för alla bilder från alla nödvändiga anpassade fält. I exemplet nedan, tre CPT:er och tre fält:

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) );
10foreach ( $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) );
24foreach ( $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) );
38foreach ( $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 );

På ett verkligt projekt, en bokhandel, gjorde denna approach det möjligt att beräkna de flesta skräpfiler och frigöra en betydande del av diskutrymmet.

5.2. Radera allt som inte finns i vitlistan

Gå nu igenom wp-content/uploads och radera varje fil som inte finns i $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
12foreach ( $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}

in_array()-metoden med strikt jämförelse på en array med 10 000+ element är inte den snabbaste. För produktionsvolymer, ersätt den vanliga arrayen med en associativ: $all_good_pictures = array_fill_keys( $all_good_pictures, true ) och kontrollera via isset(). Skillnaden på 40 000 element, från tiotals sekunder till bråkdelar av en sekund.

Hur man kör dessa funktioner

Alla snippets ovan är funktionsdefinitioner. De gör ingenting förrän du anropar dem. Tre säkra sätt att köra:

Metod

När den ska användas

Återställning

WP-CLI wp eval-file

Engångsrensning, konsollåtkomst

Nej, endast backup

Hook admin_init + URL-parameter

Ingen konsol, behöver köras från admin

Nej, endast backup

Snippet-plugin (WPCode)

Bekväm lagring och aktivera/inaktivera

Inaktivera snippet, funktionen inaktiv

Exempel på engångskörning via admin:

1add_action( 'admin_init', 'run_cleanup_once' );
2function run_cleanup_once() {
3 if ( isset( $_GET['cleanup'] ) && 'confirmed' === $_GET['cleanup'] ) {
4 delete_unattached_attachments();
5 }
6}

Besök https://yoursite.com/wp-admin/?cleanup=confirmed, funktionen körs en gång. Efter körning, ta bort snippet.

För WP-CLI, den rekommenderade metoden för produktion, spara funktionskoden till en temporär fil och kör:

1wp eval-file cleanup.php

Innan rensning är det användbart att se processen visuellt. I videon nedan, en steg-för-steg-genomgång av rensning av WordPress mediebibliotek med manuella och automatiska metoder.

⁉️🤔 Vanliga frågor

Kan filer återställas efter radering?

Nej. Funktionerna använder wp_delete_attachment() med true och unlink(), filer raderas fysiskt och går förbi papperskorgen. Den enda försäkringen: fullständig backup av filer och databas innan körning. Kontrollera om ditt webbhotell har automatiska dagliga backuper, hos Kinsta, WP Engine och SiteGround är de aktiverade som standard. Detta ger en extra återställningspunkt utöver din manuella backup.

Varför fungerade inte funktionen, filerna blev kvar?

Vanligaste orsaken: du lade till funktionsdefinitionen i functions.php, men anropade den inte. Ett function ... { }-block är bara en instruktion. För att koden ska köras måste funktionen kopplas till en hook via add_action() eller köras manuellt via WP-CLI. I avsnittet "Hur man kör", tre metoder, välj baserat på din serveråtkomstnivå.

Kommer funktionen att radera miniatyrer av använda bilder om de är oanslutna bilagor?

Nej. Miniatyrer (thumbnail, medium, large) har samma post_parent som originalbilagan. Funktionen filtrerar strikt på post_parent = 0, endast poster utan något föräldrainlägg alls. Storlekar av original ärver post_parent och hamnar inte i urvalet. Problemet uppstår bara om originalet självt har förlorat sin koppling, då kommer funktionen att radera det tillsammans med alla storlekar.

Vad händer om vissa bilder finns i anpassade fält och vissa är vanliga bilagor?

Kombinera approach från avsnitt 4 och 5. Samla först en vitlista från anpassade fält (avsnitt 5.1). När du sedan skannar uploads (avsnitt 4) för varje fil, kontrollera båda villkoren: är filen en WordPress-bilaga via attachment_url_to_postid() OCH finns den med i vitlistan. En fil raderas endast om inget av villkoren är uppfyllt: if ( ! $attachment_id && ! isset( $good_pictures[ $image_url ] ) ) { unlink( $image_path ); }.

Hur säkert är detta för en WooCommerce-sajt?

WooCommerce lagrar produktbilder som standard WordPress-bilagor, de är kopplade till inläggstypen product. Funktionen för radering av oanslutna bilagor (avsnitt 1) rör dem inte. Men funktionen för en specifik CPT (avsnitt 2), ja, om du skickar in 'product'. För WooCommerce är det säkraste metoden från avsnitt 5 (vitlista): den opererar på vad som faktiskt används, inte vad som är kopplat. Innan körning, exportera ID:n för alla produktbilagor för dubbelkontroll.

Vad du ska använda i ditt projekt: slutlig sammanställning

Metodval beror på sajtens arkitektur:

  • Standardblogg eller nyhetssajt, funktioner från avsnitt 1 (oanslutna bilagor) och avsnitt 3 (404-bilagor) räcker. Kör en gång var sjätte månad, mediebiblioteket kommer att må bra.
  • Sajt med gamla CPT:er (portfölj, katalog, annonser), lägg till avsnitt 2. Rensa exakt skräp från raderade eller övergivna inläggstyper.
  • Projekt på ACF/Meta Box med anpassade fält för bilder, ditt alternativ: avsnitt 5. Samla en vitlista, radera resten. Sätt upp en gång, upprepa sedan vid behov.
  • Allt tillsammans och oklart, börja med rekursiv skanning av uploads (avsnitt 4). Se hur mycket skräp som ligger på disken. Tillämpa sedan avsnitt 1-3 och 5 selektivt efter situation.

Inget av dessa skript ersätter regelbunden sajthygien. Men när du väl har skrivit den nödvändiga funktionen och sparat den i projektdokumentationen sparar du timmar av manuellt arbete vid nästa revision.

Och ja, du har redan gjort en backup.

Rensa upp WordPress mediebibliotek från skräpfiler