W 2013 roku, jeśli chciałeś wyciągnąć dane z WordPressa, kopiowałeś i wklejałeś tabele do Excela. Dziś przenoszenie danych to wymóg prawny (RODO) i konieczność biznesowa.
Niezależnie od tego, czy migrujesz zamówienia WooCommerce, czy robisz kopię danych użytkowników, eksport nie zawsze wymaga wtyczki. Musisz zrozumieć strumienie PHP i WP-CLI.
Do większości eksportów wystarczy zwykły CSV: strumieniuj wiersze przez fputcsv() przy pobieraniu w przeglądarce, pobieraj dane paczkami przez WP_Query i przechodź na WP-CLI, gdy żądanie w panelu uderzyłoby w limit pamięci albo timeout. REST API wybierz wtedy, gdy dane ma odebrać inny system, a nie człowiek z arkuszem.
Jak wyeksportować dane z WordPressa do Excela
Trzy podejścia pokrywają prawie każde zadanie eksportu w WordPressie:
- PHP wewnątrz WordPressa do pobierania z panelu z kontrolą uprawnień
- WP-CLI do dużych zbiorów i zadań planowanych na serwerze
- REST API do dashboardów, hurtowni danych, CRM-ów i aplikacji headless
Okazjonalne, małe eksporty dobrze mieszczą się w pobieraniu przez PHP. Cykliczne albo bardzo duże tabele należą do linii komend. Systemy zewnętrzne powinny pobierać JSON przez REST i budować CSV poza WordPressem, jeśli nadal potrzebują arkusza.
Dobieraj metodę do sposobu, w jaki zawodzi. Pobieranie w przeglądarce pada na timeoutach. Wtyczki padają, gdy próbują zbudować cały obiekt arkusza w jednym żądaniu. Doraźne zrzuty SQL padają, gdy po aktualizacji wtyczki zmieniają się nazwy kolumn. Wybierz drogę, która da ten sam eksport także za miesiąc, nie tylko dziś.
Co eksportuje Narzędzia > Eksport w WordPressie
WordPress ma eksporter w rdzeniu, ale nie tworzy on arkusza. Według dokumentacji ekranu Narzędzia > Eksport pobrany plik to XML w formacie WordPress eXtended RSS (WXR) z wpisami, stronami, własnymi typami treści, komentarzami, polami własnymi, kategoriami, tagami, własnymi taksonomiami i użytkownikami. Przy wpisach i stronach możesz filtrować po kategorii, autorze, zakresie dat i statusie.
Ten plik jest przeznaczony dla Narzędzia > Import > WordPress w innej instalacji. Excel otworzy XML, ale każdy wpis ciągnie za sobą pełny HTML treści, metadane i komentarze, więc jako arkusz jest mało przydatny. WXR służy do migracji i kopii treści. CSV jest dla człowieka, który ma filtrować, sumować albo zestawiać kolumny.
Ten sam WXR powstaje na serwerze przez wp export. Komenda przyjmuje filtry --post_type, --start_date i --end_date (format RRRR-MM-DD), --post_status oraz --author, domyślnie dzieli pliki co 15 MB (--max_file_size, -1 bez limitu), a z --stdout wypisuje XML na standardowe wyjście:
wp export --dir=/tmp/exports/ --post_type=post --start_date=2026-01-01 --end_date=2026-06-30CSV czy plik XLSX z PhpSpreadsheet
Zacznij od CSV, chyba że ktoś ma twardy wymóg pliku .xlsx.
Mocne strony CSV
- Łatwo go wygenerować w PHP albo WP-CLI
- Łatwo go sprawdzić i debugować w edytorze tekstu
- Otwiera się w Excelu, Numbers i Arkuszach Google
- Omija biblioteki XLSX i ich koszt pamięci
Koszty XLSX
- Format binarny z arkuszami, stylami i współdzielonymi ciągami znaków
- Wymaga biblioteki, np. PhpSpreadsheet, albo kroku konwersji po eksporcie
- Buduje więcej stanu w pamięci, zanim plik będzie gotowy
- Trudniej go bezpiecznie strumieniować w długim żądaniu HTTP
Możesz być kuszony, by użyć PhpSpreadsheet do wygenerowania prawdziwych plików .xlsx. Przy dużych zbiorach to złe miejsce: plik powstaje w RAM, zanim do przeglądarki pójdzie pierwszy bajt. Excel otwiera pliki CSV natywnie, a binarny .xlsx rzadko jest potrzebny, chyba że chodzi o formuły albo formatowanie. Praktyczny wzorzec: eksportuj CSV z WordPressa, a jeśli ktoś upiera się przy XLSX, przekonwertuj go potem w Excelu, LibreOffice albo małym skryptem offline. Żądanie WordPressa zostaje lekkie.
Eksport użytkowników WordPress do CSV w PHP
Jeśli chcesz zbudować przycisk “Pobierz raport” w kokpicie klienta, użyj natywnego fputcsv w PHP. Nigdy nie ładuj wszystkich danych do tablicy ($data[] = ...), bo wyczerpiesz limit pamięci. Zamiast tego strumieniuj dane bezpośrednio do przeglądarki. Najpierw uprawnienia i nonce, potem nagłówki, php://output, wiersz nagłówka i dane paczkami.
function wppoland_export_users_csv() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Brak uprawnień.' );
}
check_admin_referer( 'wppoland_export_users' );
header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="eksport-uzytkownikow-' . gmdate( 'Y-m-d' ) . '.csv"' );
// Otwórz strumień wyjściowy
$fp = fopen( 'php://output', 'w' );
// Wiersz nagłówka
fputcsv( $fp, array( 'ID', 'Nazwa użytkownika', 'Email', 'Zarejestrowany' ), ',', '"', '' );
// Iteruj paczkami po 100 użytkowników
$args = array(
'number' => 100,
'offset' => 0,
'orderby' => 'ID',
'order' => 'ASC',
);
while ( $users = get_users( $args ) ) {
foreach ( $users as $user ) {
fputcsv(
$fp,
array( $user->ID, $user->user_login, $user->user_email, $user->user_registered ),
',',
'"',
''
);
}
$args['offset'] += 100;
if ( count( $users ) < 100 ) {
break;
}
}
fclose( $fp );
exit;
}
add_action( 'admin_post_export_users', 'wppoland_export_users_csv' );Formularz, który uruchamia pobieranie, musi zawierać wp_nonce_field( 'wppoland_export_users' ), żeby check_admin_referer() mógł go zweryfikować. Eksporty użytkowników i zamówień zwykle zawierają dane osobowe, więc traktuj je jak każdą inną uprzywilejowaną akcję w panelu w duchu RODO: zapisuj, kto uruchomił eksport, i trzymaj plik z dala od publicznych adresów URL.
fputcsv w PHP 8.4 i parametr escape
Przykłady w tym przewodniku przekazują do fputcsv() pięć argumentów i to jest celowe. Podręcznik PHP podaje, że od PHP 8.4.0 poleganie na domyślnej wartości escape jest przestarzałe: parametr trzeba podać jawnie, pozycyjnie albo jako argument nazwany. Eksport uruchamiany przy każdym cronie zapełniłby log ostrzeżeniami o przestarzałym użyciu.
Ten sam podręcznik wyjaśnia, że każda wartość escape inna niż pusty ciąg może dać CSV niezgodny z RFC 4180 albo taki, który nie przetrwa drogi tam i z powrotem przez funkcje CSV w PHP. Domyślnie jest to ukośnik wsteczny, a zalecenie brzmi: ustaw "". Dlatego wszystkie wywołania tutaj używają ',', '"', ''. Podręcznik zapowiada też, że wartość domyślna zmieni się w przyszłej wersji, nie wcześniej niż w PHP 9.0, więc ustawienie jej dziś chroni przed zmianą wyniku po aktualizacji serwera.
Eksport dużej bazy WordPress bez limitu pamięci
Typowa awaria to załadowanie zbyt wielu danych naraz. Limity pamięci PHP, max_execution_time i timeouty reverse proxy przerywają długie eksporty w panelu. Pobieraj dane paczkami przez WP_Query z posts_per_page i offset, a przy dużych tabelach lepiej stronicuj stabilnym kursorem.
// $fp to strumień otwarty jak w przykładzie z użytkownikami.
$offset = 0;
$limit = 500;
do {
$query = new WP_Query(
array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => $limit,
'offset' => $offset,
'orderby' => 'ID',
'order' => 'ASC',
'fields' => 'ids',
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
)
);
foreach ( $query->posts as $post_id ) {
fputcsv( $fp, array( $post_id, get_the_title( $post_id ) ), ',', '"', '' );
}
$offset += $limit;
wp_cache_flush_runtime(); // WP 6.0+: czyści tylko pamięć podręczną tego żądania
} while ( count( $query->posts ) === $limit );Dlaczego to pomaga:
fields => 'ids'ogranicza budowanie obiektów- Wyłączone cache meta i termów nie pozwalają, by object cache rósł bez końca
- Stały rozmiar paczki trzyma szczyt pamięci blisko jednej strony wierszy
- Strumieniowanie przez
fputcsv()nie trzyma całego CSV w RAM - Sortowanie rosnąco po
IDsprawia, że wpis opublikowany w trakcie eksportu nie przesunie stron i nie zdubluje ani nie pominie wierszy
W pętli używaj wp_cache_flush_runtime(), a nie wp_cache_flush(). Przy trwałym object cache (Redis albo Memcached) wp_cache_flush() w połowie eksportu czyści cache całej strony. Funkcja runtime, dostępna od WordPressa 6.0, czyści tylko pamięć bieżącego żądania, a jeśli drop-in jej nie obsługuje, zwraca false i nie dotyka współdzielonego cache.
Przy bardzo dużych tabelach sam offset zwalnia, bo MySQL i tak przechodzi przez pominięte wiersze. Lepsza jest paginacja kluczem: WHERE ID > last_id ORDER BY ID ASC LIMIT N. W zadaniach CLI ten wzorzec jest wart dodatkowego kodu.
Jak naprawić błąd memory limit exhausted przy eksporcie
Przy eksporcie setek tysięcy rekordów PHP może wyczerpać pamięć. Podniesienie limitu to doraźna łatka, przetwarzanie w małych paczkach to właściwa poprawka:
// Doraźnie: podnieś limit pamięci w kontekście panelu
wp_raise_memory_limit( 'admin' );
// Właściwie: przetwarzaj w małych paczkach
function wppoland_safe_export() {
global $wpdb;
$batch_size = 100;
$offset = 0;
while ( true ) {
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT ID, post_title, post_date FROM {$wpdb->posts} WHERE post_type = 'post' ORDER BY ID ASC LIMIT %d OFFSET %d",
$batch_size,
$offset
)
);
if ( empty( $results ) ) {
break;
}
// Zapisz wiersze do pliku...
$offset += $batch_size;
// Zwolnij wyniki ostatniego zapytania
$wpdb->flush();
}
}wp_raise_memory_limit() podnosi limit do wartości WP_MAX_MEMORY_LIMIT zamiast wpisywać ją na sztywno w kodzie.
Jak uniknąć timeoutu serwera przy eksporcie
// Zwiększ czas wykonania (część hostingów to blokuje)
set_time_limit( 0 );
ignore_user_abort( true );
// Albo dziel długą operację na kroki AJAX
add_action( 'wp_ajax_wppoland_long_export', 'wppoland_long_export_handler' );
function wppoland_long_export_handler() {
check_ajax_referer( 'wppoland_long_export' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( null, 403 );
}
$page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 0;
$total_pages = isset( $_POST['total_pages'] ) ? absint( $_POST['total_pages'] ) : 10;
// Twoja funkcja dopisująca jedną stronę danych do pliku
wppoland_process_export_page( $page );
if ( $page < $total_pages ) {
wp_send_json_success( array( 'next_page' => $page + 1 ) );
}
wp_send_json_success( array( 'complete' => true ) );
}Oficjalny opis argumentów zapytania: klasa WP_Query na developer.wordpress.org.
Jak wyeksportować wpisy do CSV przez WP-CLI
Jeśli masz 500 000 wpisów, przeglądarka zaliczy timeout. Użyj WP-CLI bezpośrednio na serwerze. Omijasz przeglądarkę i łańcuch timeoutów serwera WWW, a zadanie da się zautomatyzować cronem.
Komenda:
wp post list --post_type=product --fields=ID,post_title,post_status --format=csv > produkty.csvPrzydaje się przy:
- dużych katalogach wpisów albo produktów
- zrzutach zamówień WooCommerce, które w wp-admin skończyłyby się timeoutem
- nocnych eksportach z crona do object storage albo wspólnego folderu
- przekazaniu danych przy migracji i raportowaniu
Dokumentacja komendy: wp post list. Komenda przyjmuje --format=csv, json, yaml, ids, count albo table oraz dowolny argument WP_Query w postaci --<pole>=<wartość>. Na dużych stronach liczy się jeden szczegół: --posts_per_page ma domyślnie wartość -1, czyli wszystkie wpisy w jednym zapytaniu. Przy ogromnej tabeli stronicuj przez --posts_per_page=1000 --paged=N w pętli powłoki.
Dla użytkowników jest odpowiednik wp user list z domyślnymi polami ID, user_login, display_name, user_email, user_registered i roles. Każda komenda przyjmuje też globalny parametr --ssh=uzytkownik@host/sciezka, który uruchamia eksport na zdalnym serwerze z twojego komputera bez osobnej sesji.
Zaawansowany eksport (pola własne): Możesz napisać własną komendę WP-CLI, jeśli domyślne flagi nie wystarczą. Komenda otwiera plik, stronicuje zamówienia i zapisuje CSV na dysk, więc pamięć zostaje przewidywalna, a przerwane zadanie można wznowić od punktu kontrolnego.
// Komenda: wp wppoland export-orders
WP_CLI::add_command( 'wppoland export-orders', function ( $args, $assoc_args ) {
$file = fopen( 'zamowienia.csv', 'w' );
fputcsv( $file, array( 'ID zamówienia', 'Utworzone', 'Status', 'Suma', 'Klient' ), ',', '"', '' );
$page = 1;
do {
$orders = wc_get_orders(
array(
'type' => 'shop_order',
'limit' => 200,
'paged' => $page,
'orderby' => 'ID',
'order' => 'ASC',
)
);
foreach ( $orders as $order ) {
$created = $order->get_date_created();
fputcsv(
$file,
array(
$order->get_id(),
$created ? $created->date( 'c' ) : '',
$order->get_status(),
$order->get_total(),
$order->get_billing_email(),
),
',',
'"',
''
);
}
$page++;
} while ( count( $orders ) === 200 );
fclose( $file );
WP_CLI::success( 'Eksport zakończony do zamowienia.csv' );
} );limit i paged to udokumentowane parametry wc_get_orders(). Z limit => -1 komenda załadowałaby wszystkie zamówienia do pamięci przed zapisaniem pierwszego wiersza, czyli dokładnie to, czego paczki mają uniknąć.
Komendy WP-CLI do masowego eksportu danych
Podstawowe komendy WP-CLI do eksportu CSV
# Eksport wpisów (domyślny typ to post)
wp post list --format=csv > wszystkie-wpisy.csv
# Eksport stron
wp post list --post_type=page --format=csv > strony.csv
# Eksport użytkowników
wp user list --format=csv > uzytkownicy.csv
# Eksport z konkretnymi polami
wp post list --post_type=post --fields=ID,post_title,post_date,post_status --format=csv > wybrane-wpisy.csv
# Eksport komentarzy
wp comment list --format=csv > komentarze.csv
# Eksport z filtrowaniem
wp post list --post_status=publish --format=csv > opublikowane-wpisy.csv
wp post list --post_type=product --format=csv > produkty.csvEksport wpisów z polami meta przez WP-CLI
# Eksport z metadanymi przez własny skrypt
wp eval-file export-with-meta.phpGdzie export-with-meta.php:
<?php
// Plik: export-with-meta.php, uruchamiany przez wp eval-file
$file = fopen( 'wpisy-z-meta.csv', 'w' );
fwrite( $file, "\xEF\xBB\xBF" );
fputcsv( $file, array( 'ID', 'Tytuł', 'Autor', 'Views', 'Featured' ), ',', '"', '' );
$paged = 1;
do {
$posts = get_posts(
array(
'post_type' => 'post',
'posts_per_page' => 500,
'paged' => $paged,
'orderby' => 'ID',
'order' => 'ASC',
)
);
foreach ( $posts as $post ) {
$views = get_post_meta( $post->ID, 'views', true );
$featured = get_post_meta( $post->ID, 'featured', true );
fputcsv(
$file,
array(
$post->ID,
$post->post_title,
get_the_author_meta( 'display_name', $post->post_author ),
$views ? $views : 0,
$featured ? 'Tak' : 'Nie',
),
',',
'"',
''
);
}
$paged++;
} while ( count( $posts ) === 500 );
fclose( $file );
WP_CLI::success( 'Eksport ukończony.' );Automatyczny eksport WP-CLI przez crontab
# Dodaj do crontab (crontab -e)
# Codziennie o 4:00, plik poza katalogiem publicznym
0 4 * * * cd /path/to/wordpress && wp post list --format=csv > /backups/posts-$(date +\%Y-\%m-\%d).csv
# Co godzinę: własny skrypt przez wp eval-file
0 * * * * cd /path/to/wordpress && wp eval-file hourly-export.phpKomplet bazy danych przez wp db export
Czasem prośba nie dotyczy arkusza, tylko kopii wszystkiego. wp db export uruchamia mysqldump z danymi dostępowymi z wp-config.php i zapisuje plik SQL. Przez --tables wybierasz konkretne tabele, przez --exclude_tables je pomijasz, a - jako nazwa pliku wysyła zrzut na standardowe wyjście:
wp db export - --tables=wp_posts,wp_postmeta | gzip > tresci.sql.gzDopasuj prefiks tabel do swojej instalacji. Zrzut SQL służy do odtworzenia albo załadowania do innej bazy, nie do otwierania w Excelu. Zawiera też tabelę użytkowników z hashami haseł i często klucze API, które wtyczki trzymają w wp_options. Traktuj go jak sekret: nigdy w publicznym katalogu, nigdy jako załącznik maila.
Jak pobrać wszystkie wpisy przez REST API w WordPressie
Jeśli budujesz dashboard w React lub aplikację headless, konsumujesz JSON, a nie CSV. Gdy odbiorcą jest program, a nie człowiek z Excelem, REST API bywa czystsze niż generowanie CSV w WordPressie.
Typowe przypadki:
- synchronizacja treści do hurtowni danych albo narzędzia BI
- zasilanie CRM-u użytkownikami albo leadami
- frontendy headless, które potrzebują ustrukturyzowanych danych wpisów
WordPress REST API (/wp-json/wp/v2/posts) domyślnie stronicuje dane. Według przewodnika po paginacji per_page ma limit 100 rekordów, a każda odpowiedź niesie nagłówki X-WP-Total (liczba rekordów) i X-WP-TotalPages (liczba stron). Opis kolekcji: endpoint Posts.
Aby pobrać wszystkie dane, napisz pętlę albo rekurencyjną funkcję JS, która podąża za nagłówkiem X-WP-TotalPages, zamiast zgadywać, kiedy przestać. Uwierzytelniaj się przez Application Passwords albo przepływy OAuth obsługiwane przez hosting i szanuj limity zapytań. CSV zbuduj po stronie odbiorcy, jeśli arkusz nadal jest potrzebny. Taki podział zostawia WordPressowi kontrolę dostępu, a formatowanie pliku odbiorcy.
Nie wystawiaj uprzywilejowanych tras eksportu bez uwierzytelnienia. Traktuj endpointy eksportu jak każdą inną powierzchnię panelu bliską zapisowi, nawet jeśli tylko czytają.
Eksport produktów WooCommerce do CSV bez wtyczki
Katalog nie wymaga kodu. Według dokumentacji importera i eksportera CSV produktów WooCommerce ma własne narzędzie do importu, eksportu i masowej aktualizacji produktów:
- Wejdź w Produkty > Wszystkie produkty i kliknij Eksportuj u góry strony.
- Wybierz kolumny albo zostaw opcję eksportu wszystkich.
- Filtruj po typach produktów i kategoriach albo zostaw oba pola na wszystkich.
- Zaznacz eksport wszystkich własnych metadanych, jeśli potrzebujesz danych WooCommerce albo innych wtyczek. Te kolumny dostają prefiks
meta:. - Kliknij Generuj CSV i poczekaj, aż przeglądarka pobierze plik.
Plik ma ten sam format, który czyta importer, więc nadaje się jako baza do masowych zmian cen albo stanów: eksportujesz, edytujesz w arkuszu, importujesz z powrotem. Narzędzie obejmuje katalog produktów. Zamówień nie eksportuje, o nich jest następna sekcja.
Jak wyeksportować zamówienia WooCommerce do CSV
Eksport zamówień wygląda prosto, dopóki pozycje, zwroty i HPOS nie zmienią obrazu.
Pozycje to nie jeden wiersz. Zamówienie może mieć kilka produktów, opłat, pozycji wysyłki i podatków. Ustal z góry, czy wiersz CSV oznacza zamówienie, czy pozycję. Mieszanie obu bez jasnego schematu daje bezużyteczne arkusze.
Meta i pola własne rozdymają szerokość. Identyfikatory bramek płatności, numery przesyłek i meta wtyczek tworzą szerokie, rzadko wypełnione kolumny. Eksportuj tylko pola potrzebne w raporcie. Jeśli musisz zachować obie formy, dodaj drugi plik na pozycje.
Magazyny danych się różnią. Klasyczne wpisy shop_order i tabele High-Performance Order Storage (HPOS) odpytuje się inaczej. HPOS jest domyślnym magazynem od WooCommerce 8.2, według dokumentacji wc_get_orders(). Używaj CRUD WooCommerce (wc_get_orders() / WC_Order_Query) zamiast surowego WP_Query na shop_order, żeby eksporter przetrwał zmianę magazynu. Do filtrowania dat date_created przyjmuje zakres w postaci 2026-01-01...2026-03-31, liczony w strefie czasowej strony.
Filtry statusów mają znaczenie. Szkice, nieudane płatności, zwrócone zamówienia i kosz zawyżają albo zniekształcają raporty przychodu. Zapisz, które statusy uwzględniasz.
Dane osobowe i dostęp. Zamówienia zawierają imiona, maile i adresy. Kontrola uprawnień (manage_woocommerce albo własna capability), nonce i log audytu należą do każdego eksportera w panelu. Masowe zrzuty rób przez WP-CLI, żeby plik nigdy nie leżał w publicznie dostępnym katalogu uploads.
Wydajność. Złączenie wszystkich kluczy meta zamówienia w jednym zapytaniu SQL wygląda na szybkie, dopóki tabela nie urośnie. Stronicuj zamówienia, ładuj każde przez wc_get_order(), zapisz wiersz i porzuć obiekt. To wolniejsze na zamówienie niż jeden wielki JOIN, ale dużo stabilniejsze przy limitach pamięci.
Waluta i format. Zapisuj sumy jako zwykłe liczby. Formatowanie waluty zostaw Excelowi. Separatory tysięcy zależne od języka wewnątrz pól CSV psują import w innych narzędziach.
Pola dat. Zapisuj ISO-8601 albo znaczniki czasu Unix i formatuj dopiero w arkuszu. Mieszane lokalne formaty (03.04.2026 i 04/03/2026) powodują ciche błędy sortowania, gdy plik przekracza granice.
Zwroty i zwroty częściowe. Zdecyduj, czy zwrócone kwoty pojawiają się jako ujemne wiersze, osobne identyfikatory zwrotów czy sumy netto zamówienia. Zapisz wybór w README obok crona eksportu, żeby księgowość i programiści pracowali na tym samym arkuszu.
Poniższy kod tworzy eksport zamówień z panelu z danymi rozliczeniowymi i wysyłkowymi, według tych zasad:
function wppoland_export_woocommerce_orders_csv() {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( 'Brak uprawnień.' );
}
check_admin_referer( 'wppoland_export_orders' );
if ( ! function_exists( 'wc_get_orders' ) ) {
wp_die( 'WooCommerce nie jest aktywne.' );
}
$date_from = isset( $_GET['date_from'] ) ? sanitize_text_field( wp_unslash( $_GET['date_from'] ) ) : '';
$date_to = isset( $_GET['date_to'] ) ? sanitize_text_field( wp_unslash( $_GET['date_to'] ) ) : '';
header( 'Content-Type: text/csv; charset=UTF-8' );
header( 'Content-Disposition: attachment; filename="zamowienia-woocommerce-' . gmdate( 'Y-m-d' ) . '.csv"' );
$fp = fopen( 'php://output', 'w' );
// BOM dla Excela (poprawne polskie znaki)
fwrite( $fp, "\xEF\xBB\xBF" );
fputcsv(
$fp,
array( 'ID zamówienia', 'Data', 'Status', 'Email', 'Imię i nazwisko', 'Telefon', 'Adres', 'Miasto', 'Kod pocztowy', 'Kraj', 'Suma', 'Suma VAT', 'Metoda płatności', 'Metoda wysyłki', 'Uwagi' ),
',',
'"',
''
);
$args = array(
'type' => 'shop_order',
'limit' => 50,
'paged' => 1,
'orderby' => 'ID',
'order' => 'ASC',
'status' => array( 'completed', 'processing', 'on-hold', 'cancelled', 'refunded' ),
);
// Jeden parametr date_created: zakres albo pojedyncza granica
if ( $date_from && $date_to ) {
$args['date_created'] = $date_from . '...' . $date_to;
} elseif ( $date_from ) {
$args['date_created'] = '>=' . $date_from;
} elseif ( $date_to ) {
$args['date_created'] = '<=' . $date_to;
}
do {
$orders = wc_get_orders( $args );
foreach ( $orders as $order ) {
$billing = $order->get_address( 'billing' );
$created = $order->get_date_created();
fputcsv(
$fp,
array(
$order->get_id(),
$created ? $created->date( 'Y-m-d H:i:s' ) : '',
$order->get_status(),
$order->get_billing_email(),
wppoland_csv_safe( $billing['first_name'] . ' ' . $billing['last_name'] ),
$billing['phone'],
wppoland_csv_safe( trim( $billing['address_1'] . ' ' . $billing['address_2'] ) ),
wppoland_csv_safe( $billing['city'] ),
$billing['postcode'],
$billing['country'],
$order->get_total(),
$order->get_total_tax(),
$order->get_payment_method_title(),
$order->get_shipping_method(),
wppoland_csv_safe( $order->get_customer_note() ),
),
',',
'"',
''
);
}
$args['paged']++;
} while ( count( $orders ) === 50 );
fclose( $fp );
exit;
}
add_action( 'admin_post_wppoland_export_orders', 'wppoland_export_woocommerce_orders_csv' );Sumy idą jako zwykłe liczby z get_total(), bez przecinka dziesiętnego: Excel w polskich ustawieniach sformatuje je sam, a inne narzędzia nie potkną się o separator. Funkcja wppoland_csv_safe() pochodzi z sekcji o wstrzykiwaniu formuł niżej.
Formularz eksportu zamówień w panelu WooCommerce
Dodaj formularz do eksportu w panelu WooCommerce:
function wppoland_export_orders_page() {
?>
<div class="wrap">
<h1>Eksport zamówień</h1>
<form method="get" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="wppoland_export_orders">
<?php wp_nonce_field( 'wppoland_export_orders' ); ?>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="date_from">Data od</label></th>
<td><input type="date" id="date_from" name="date_from" /></td>
</tr>
<tr>
<th scope="row"><label for="date_to">Data do</label></th>
<td><input type="date" id="date_to" name="date_to" /></td>
</tr>
</table>
<?php submit_button( 'Eksportuj do CSV' ); ?>
</form>
</div>
<?php
}
function wppoland_add_export_menu() {
add_submenu_page(
'woocommerce',
'Eksport zamówień',
'Eksport CSV',
'manage_woocommerce',
'wppoland-export-orders',
'wppoland_export_orders_page'
);
}
add_action( 'admin_menu', 'wppoland_add_export_menu' );Eksport pól ACF i meta do CSV
Wiele wtyczek (np. ACF, Meta Box) przechowuje dane w niestandardowych polach. Tak wyeksportujesz je razem z wpisami:
function wppoland_export_posts_with_meta_csv() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Brak uprawnień.' );
}
check_admin_referer( 'wppoland_export_meta' );
header( 'Content-Type: text/csv; charset=UTF-8' );
header( 'Content-Disposition: attachment; filename="wpisy-z-polami-' . gmdate( 'Y-m-d' ) . '.csv"' );
$fp = fopen( 'php://output', 'w' );
fwrite( $fp, "\xEF\xBB\xBF" );
$custom_fields = array( 'views', 'featured', 'external_url', 'video_id' );
$headers = array_merge( array( 'ID', 'Tytuł', 'Autor', 'Data', 'Status', 'Kategoria', 'Tagi', 'Miniatura' ), $custom_fields );
fputcsv( $fp, $headers, ',', '"', '' );
$paged = 1;
do {
$query = new WP_Query(
array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 100,
'paged' => $paged,
'orderby' => 'ID',
'order' => 'ASC',
'no_found_rows' => true,
)
);
foreach ( $query->posts as $post ) {
$categories = get_the_category( $post->ID );
$tags = get_the_tags( $post->ID );
fputcsv(
$fp,
array(
$post->ID,
wppoland_csv_safe( get_the_title( $post ) ),
get_the_author_meta( 'display_name', $post->post_author ),
get_the_date( 'Y-m-d H:i:s', $post ),
$post->post_status,
implode( ', ', wp_list_pluck( $categories, 'name' ) ),
$tags ? implode( ', ', wp_list_pluck( $tags, 'name' ) ) : '',
(string) get_the_post_thumbnail_url( $post, 'full' ),
get_post_meta( $post->ID, 'views', true ),
get_post_meta( $post->ID, 'featured', true ) ? 'Tak' : 'Nie',
get_post_meta( $post->ID, 'external_url', true ),
get_post_meta( $post->ID, 'video_id', true ),
),
',',
'"',
''
);
}
$paged++;
} while ( count( $query->posts ) === 100 );
fclose( $fp );
exit;
}
add_action( 'admin_post_wppoland_export_meta', 'wppoland_export_posts_with_meta_csv' );Pętla kończy się, gdy strona zwróci mniej niż 100 wpisów. Konstrukcja while ( $query = new WP_Query( $args ) ) nigdy nie zwraca fałszu, bo nowy obiekt zawsze jest prawdziwy, więc bez osobnego warunku kręciłaby się w nieskończoność.
Jak ustawić automatyczny eksport CSV przez WP-Cron
Jeśli potrzebujesz codziennej kopii danych, skonfiguruj eksport przez WordPress Cron. Plik z danymi osobowymi nie powinien trafiać do wp-content/uploads, bo ten katalog jest publicznie dostępny. Zapisuj go poza katalogiem głównym strony:
// Katalog poza public_html, np. zdefiniowany w wp-config.php:
// define( 'WPPOLAND_EXPORT_DIR', '/home/konto/exports/' );
// Rejestracja harmonogramu przy aktywacji wtyczki
register_activation_hook( __FILE__, function () {
if ( ! wp_next_scheduled( 'wppoland_daily_export' ) ) {
wp_schedule_event( strtotime( 'tomorrow 03:00' ), 'daily', 'wppoland_daily_export' );
}
} );
add_action( 'wppoland_daily_export', 'wppoland_run_daily_export' );
function wppoland_run_daily_export() {
$dir = trailingslashit( WPPOLAND_EXPORT_DIR );
if ( ! file_exists( $dir ) ) {
wp_mkdir_p( $dir );
}
$filename = $dir . 'users-' . gmdate( 'Y-m-d' ) . '.csv';
$fp = fopen( $filename, 'w' );
fwrite( $fp, "\xEF\xBB\xBF" );
fputcsv( $fp, array( 'ID', 'Login', 'Email', 'Data rejestracji', 'Rola' ), ',', '"', '' );
$args = array( 'number' => 500, 'offset' => 0, 'orderby' => 'ID', 'order' => 'ASC' );
while ( $users = get_users( $args ) ) {
foreach ( $users as $user ) {
fputcsv(
$fp,
array( $user->ID, $user->user_login, $user->user_email, $user->user_registered, implode( ', ', $user->roles ) ),
',',
'"',
''
);
}
$args['offset'] += 500;
if ( count( $users ) < 500 ) {
break;
}
}
fclose( $fp );
// Powiadomienie bez załącznika i bez danych osobowych w treści
wp_mail( get_option( 'admin_email' ), 'Codzienny eksport WordPress', 'Eksport zakończony: ' . basename( $filename ) );
}
// Wyrejestrowanie przy wyłączaniu wtyczki
register_deactivation_hook( __FILE__, function () {
wp_clear_scheduled_hook( 'wppoland_daily_export' );
} );strtotime( 'tomorrow 03:00' ) liczy godzinę w strefie PHP, a WordPress ustawia ją na UTC, więc w Polsce zadanie ruszy o 4:00 albo 5:00 czasu lokalnego. WP-Cron uruchamia się dopiero przy wizycie na stronie; przy małym ruchu podepnij wp cron event run --due-now pod systemowy crontab.
Jak eksportować dane osobowe z WordPressa zgodnie z RODO
Pseudonimizacja danych użytkowników w eksporcie CSV
Przy eksporcie danych użytkowników do analiz rozważ zastąpienie adresu email skrótem:
function wppoland_export_users_pseudonymised() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Brak uprawnień.' );
}
check_admin_referer( 'wppoland_export_users_pseudo' );
header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="uzytkownicy-pseudonimizowani.csv"' );
$fp = fopen( 'php://output', 'w' );
fputcsv( $fp, array( 'ID', 'Hash email', 'Rola', 'Data rejestracji' ), ',', '"', '' );
$args = array( 'number' => 100, 'offset' => 0, 'orderby' => 'ID', 'order' => 'ASC' );
while ( $users = get_users( $args ) ) {
foreach ( $users as $user ) {
// HMAC z kluczem z wp-config.php zamiast gołego sha256
$email_hash = hash_hmac( 'sha256', strtolower( $user->user_email ), wp_salt( 'auth' ) );
fputcsv(
$fp,
array( $user->ID, $email_hash, implode( ', ', $user->roles ), $user->user_registered ),
',',
'"',
''
);
}
$args['offset'] += 100;
if ( count( $users ) < 100 ) {
break;
}
}
fclose( $fp );
exit;
}To pseudonimizacja, nie anonimizacja. Motyw 26 RODO mówi, że dane spseudonimizowane, które przy użyciu dodatkowych informacji można przypisać osobie fizycznej, należy uznać za informacje o osobie możliwej do zidentyfikowania. Plik dalej jest daną osobową, a ID użytkownika w pierwszej kolumnie i tak prowadzi do konta. Goły sha256 z adresu email da się odwrócić słownikiem znanych adresów, dlatego przykład używa HMAC z sekretem z wp-config.php.
Jak wyeksportować dane użytkownika na jego żądanie
Gdy prośba przychodzi od osoby, która korzysta ze swoich praw, nie potrzebujesz własnego eksportera. Dokumentacja dla twórców wtyczek opisuje narzędzie eksportu danych osobowych dodane w WordPressie 4.9.6 (Narzędzia > Eksport danych osobowych), przygotowane z myślą o przepisach takich jak RODO.
Kluczem każdego eksportu jest adres email, co obejmuje zarejestrowanych użytkowników i osoby, które zostawiły tylko komentarz. Administrator wpisuje nazwę użytkownika albo email, WordPress wysyła link potwierdzający, a po potwierdzeniu administrator pobiera ZIP albo wysyła go bezpośrednio do osoby. W środku jest strona indeksu HTML z danymi pogrupowanymi, np. komentarzami. Wtyczki, które przechowują własne dane osobowe, podpinają się filtrem wp_privacy_personal_data_exporters, żeby ich dane trafiły do tego samego pliku.
Jak zapobiec wstrzykiwaniu formuł w wyeksportowanym CSV
CSV z WordPressa zawiera dane wpisane przez użytkowników: nazwy wyświetlane, komentarze, pola formularzy, adresy rozliczeniowe. OWASP opisuje to ryzyko jako CSV injection, czyli wstrzykiwanie formuł: pojawia się, gdy strona osadza niezaufane dane w pliku CSV. Jeśli komórka zaczyna się od =, +, - albo @, od tabulatora, powrotu karetki albo nowej linii, arkusz może przy otwarciu potraktować ją jak formułę.
OWASP zaleca, by żadna komórka nie zaczynała się od tych znaków, np. przez dodanie pojedynczego apostrofu na początku, dzięki czemu program odczyta komórkę jako tekst:
function wppoland_csv_safe( $value ) {
$value = (string) $value;
if ( '' !== $value && in_array( $value[0], array( '=', '+', '-', '@', "\t", "\r", "\n" ), true ) ) {
return "'" . $value;
}
return $value;
}Stosuj ją do kolumn z wolnym tekstem, nie do kwot liczonych przez WooCommerce: ujemna suma zwrotu zaczyna się od - i zamieniłaby się w tekst, a to psuje sumowanie w arkuszu.
Wtyczki do eksportu WordPress do CSV i Excel
Darmowe wtyczki do eksportu CSV
- WP All Export - eksport z interfejsem drag-and-drop
- Stream - logowanie aktywności użytkowników z możliwością eksportu logów
Płatne wtyczki do eksportu z supportem
Komercyjne wtyczki z gwarancją wsparcia i aktualizacji kupisz m.in. w sklepie plogins:
- WP All Export Pro + WP All Import Pro - kompleksowe rozwiązanie do importu i eksportu, z dodatkiem do pól ACF
- WooCommerce Customer / Order / Coupon Export - eksport klientów, zamówień i kuponów WooCommerce
Jak zainstalować WP-CLI
# Instalacja WP-CLI
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
# Weryfikacja
wp --infoPolskie znaki i inne błędy eksportu CSV
Jak naprawić polskie znaki w pliku CSV
Excel w Windows zgaduje kodowanie pliku CSV. Pomoc Microsoftu potwierdza, że plik CSV w UTF-8 zapisany z BOM otwiera się normalnie. Bez BOM udokumentowana droga to Dane > Pobierz dane > Z pliku > Z pliku tekstowego/CSV, gdzie wybierasz kodowanie przed wczytaniem.
// Dodaj BOM na początku pliku, przed wierszem nagłówka
$fp = fopen( 'php://output', 'w' );
fwrite( $fp, "\xEF\xBB\xBF" );Filtr convert.iconv.UTF-8/UTF-8 niczego tu nie naprawia, bo konwertuje UTF-8 na UTF-8. Gdy plik trafia do innego skryptu, a nie do Excela, pomiń BOM: wiele parserów czyta te trzy bajty jako część nazwy pierwszej kolumny.
Błąd headers already sent przy eksporcie CSV
Komunikat oznacza, że przed header() poszedł już jakiś bajt: spacja przed <?php, BOM w pliku motywu, ostrzeżenie PHP albo echo z innej wtyczki. Podepnij eksport pod admin_post_*, które rusza przed wysłaniem jakiegokolwiek HTML panelu, a resztki bufora wyczyść przed nagłówkami:
// Na początku funkcji eksportu, przed header():
while ( ob_get_level() > 0 ) {
ob_end_clean();
}Nie wywołuj ob_end_clean() na końcu eksportu: skasowałbyś wygenerowany CSV, zanim trafi do przeglądarki.
Dlaczego plik CSV z eksportu jest pusty
// Sprawdź uprawnienia
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Brak uprawnień administratora.' );
}
// Debuguj zapytanie
error_log( 'Eksport - liczba użytkowników: ' . count( $users ) );Którą metodę eksportu CSV wybrać
| Metoda | Zalety | Wady | Zastosowanie |
|---|---|---|---|
| PHP fputcsv | Natywne, szybkie, bez zewnętrznych zależności | Wymaga kodu PHP | Małe i średnie zbiory, niestandardowe eksporty |
| WP-CLI | Brak limitów żądania HTTP, przetwarzanie wsadowe | Wymaga dostępu SSH | Duże zbiory, automatyzacja |
| REST API | Programatyczny dostęp, headless | Stronicowanie, JSON zamiast CSV | Aplikacje, integracje |
| Narzędzia > Eksport (WXR) | W rdzeniu, bez kodu | XML, nie arkusz | Migracja treści |
| Wtyczki | Interfejs GUI, gotowe rozwiązania | Ograniczenia wersji darmowych | Użytkownicy nietechniczni |
Metoda eksportu a wielkość sklepu
- Mały sklep (< 1000 zamówień): PHP + fputcsv
- Duży sklep (> 10 000 zamówień): WP-CLI + cron
- Wiele sklepów (franczyza): WP-CLI + skrypt bash
- Aplikacja headless: REST API
- Zgodność z RODO: narzędzie eksportu danych osobowych z rdzenia i pseudonimizacja w analizach
- Automatyczna kopia:
wp db export+ cron + FTP/S3
Jak bezpiecznie eksportować dane z WordPressa
- Weryfikuj uprawnienia i nonce przed eksportem
- Sanityzuj dane wejściowe (daty, filtry)
- Szyfruj wrażliwe eksporty przed wysłaniem
- Loguj eksporty na potrzeby audytu RODO
- Automatycznie usuwaj stare pliki z serwera
Jak przyspieszyć eksport dużych zbiorów danych
- Używaj przetwarzania wsadowego
- Strumieniuj zamiast ładować do pamięci
- Optymalizuj zapytania (pobieraj tylko potrzebne kolumny)
- Używaj indeksów w bazie danych
- Cache’uj wyniki dla powtarzających się eksportów
Lista kontrolna eksportu danych z WordPressa
- W WordPressie wybieraj CSV zamiast XLSX, a WXR tylko do przenoszenia treści.
- Strumieniuj przez
fputcsv()z jawnymescapei kontrolą uprawnień przy małych pobraniach z panelu. - Pobieraj paczkami przez
WP_Queryalbo zapytania WooCommerce; wyłącz zbędne cache. - Duże i cykliczne zadania przenieś do WP-CLI.
- Używaj REST API, gdy dane pobiera inny system.
- W WooCommerce ustal, co znaczy wiersz (zamówienie czy pozycja), i używaj API CRUD, żeby HPOS nie zepsuł eksportera.
- Neutralizuj komórki z wolnym tekstem, które zaczynają się od
=,+,-albo@.
Twoje dane należą do Ciebie. Upewnij się, że zawsze możesz je wyciągnąć.
Eksport danych bywa ostatnią rzeczą, o którą ktoś dba, aż do momentu zmiany systemu albo żądania z RODO. Jeśli chcesz mieć te ścieżki przygotowane wcześniej, przygotujemy je w ramach opieki nad stroną.







