Ekstrakcja konkretnych fragmentów z ogromnych zrzutów SQL przy użyciu PHP

Spis treści
Z dużego zrzutu MySQL czasem potrzebna jest tylko konkretna tabela. Wyodrębnienie odpowiedniego tekstu nie wymaga wczytania całego pliku do edytora ani pamięci PHP. Ważne jest jednak rozróżnienie celu: fragment do analizy albo kopia bazy, którą rzeczywiście da się odtworzyć.
Filtr wierszowy nie jest parserem SQL. Poniższy przykład PHP kopiuje wybrane sekcje tabel rozpoznawane po standardowych komentarzach. Wynik pozostaje fragmentem tekstu do sprawdzenia. Przy odtwarzaniu danych lepszym wyborem jest nowy eksport wybranych obiektów albo zweryfikowana procedura przywracania.

Dlaczego jeden przełącznik nie wystarcza
Baza i tabela to dwa oddzielne stany. Zmiana bazy kończy bieżący wybór. Każdy nowy znacznik tabeli wymaga ponownej decyzji o kopiowaniu. Sprawdzanie kolejnych tabel wyłącznie przy wyłączonym przełączniku powodowałoby kopiowanie także innych tabel po pierwszym trafieniu.
Samo fgets() nie gwarantuje minimalnego zużycia pamięci. Bez ograniczenia długości odczytywany jest cały wiersz, a rozszerzony INSERT może być bardzo długi. Przykład czyta najwyżej 65 536 bajtów na krok i rozpoznaje znaczniki wyłącznie na początku wiersza. Długie wiersze danych są kopiowane porcjami.
Wymagany format zrzutu
- Nieskompresowany plik tekstowy w typowym formacie komentarzy
mysqldump, ze znacznikami baz, na przykład z eksportu przez--databaseslub--all-databases. - Obecne są znaczniki
-- Current Database:oraz odpowiednie sekcje-- Table structure for tablei/lub-- Dumping data for table. Zrzuty z--skip-commentsnie są obsługiwane tą metodą. - Dokładne nazwy bazy i tabel zamiast jednego niejednoznacznego prefiksu. Identyfikatory z osadzonym znakiem backtick i inne formaty generatorów wykraczają poza zakres przykładu.
- PHP 8 CLI i lokalny system plików obsługujący dowiązania twarde w obrębie jednego katalogu. Plik źródłowy, częściowy i wynikowy znajdują się poza publicznym katalogiem witryny.
Skompresowany zrzut wymaga wcześniejszego rozpakowania do chronionego katalogu z odpowiednią ilością miejsca. Obejrzenie początku pliku pomaga rozpoznać format, lecz nie potwierdza całej zawartości, wszystkich późniejszych tabel ani długości wszystkich wierszy.
Przykład PHP: sekcje tabel o podanych nazwach
Wywołanie php extract.php dump.sql excerpt.txt shop orders customers wybiera tabele orders i customers z bazy shop. Plik wynikowy i odpowiadający mu plik .part nie mogą wcześniej istnieć.
<?php
// PHP 8 CLI. Produces a text excerpt, not a validated restore file.
umask(0077);
if ($argc < 5) {
fwrite(STDERR, "Usage: php extract.php input.sql excerpt.txt database table [table...]\n");
exit(2);
}
[, $input, $output, $database] = $argv;
$tables = array_fill_keys(array_slice($argv, 4), true);
$part = $output . '.part';
$in = $out = false;
$created = false;
try {
$in = @fopen($input, 'rb');
if ($in === false) throw new RuntimeException('Cannot open input');
$out = @fopen($part, 'xb');
if ($out === false) throw new RuntimeException('Cannot create new partial output');
$created = true;
$currentDatabase = null;
$copy = false;
$lineStart = true;
$seen = [];
while (($chunk = fgets($in, 65537)) !== false) {
if ($lineStart) {
if (preg_match('/^-- Current Database: `([^`]+)`\r?\n?$/D', $chunk, $m)) {
$currentDatabase = $m[1];
$copy = false;
} elseif (preg_match('/^-- (?:Table structure|Dumping data) for table `([^`]+)`\r?\n?$/D', $chunk, $m)) {
$copy = $currentDatabase === $database && isset($tables[$m[1]]);
if ($copy) $seen[$m[1]] = true;
} elseif (str_starts_with($chunk, '-- ')) {
$copy = false;
}
}
if ($copy) {
$offset = 0;
$length = strlen($chunk);
while ($offset < $length) {
$n = fwrite($out, substr($chunk, $offset));
if ($n === false || $n === 0) throw new RuntimeException('Write failed');
$offset += $n;
}
}
$lineStart = str_ends_with($chunk, "\n");
}
if (!feof($in)) throw new RuntimeException('Read failed');
if (array_diff_key($tables, $seen)) {
throw new RuntimeException('Requested table marker missing; format or names differ');
}
if (!fflush($out)) throw new RuntimeException('Flush failed');
if (!fclose($out)) throw new RuntimeException('Close failed');
$out = false;
// Same-directory hard link: fails if the final name already exists.
if (!@link($part, $output)) throw new RuntimeException('Cannot publish new output');
if (!unlink($part)) throw new RuntimeException('Output exists, partial link cleanup failed');
$created = false;
fwrite(STDERR, "Text excerpt written. Separate restore validation required.\n");
} catch (Throwable $e) {
if (is_resource($out)) fclose($out);
if ($created && is_file($part)) unlink($part);
fwrite(STDERR, $e->getMessage() . "\n");
exit(1);
} finally {
if (is_resource($in)) fclose($in);
}
Końcowy wynik pojawia się dopiero po odczytaniu wejścia, poprawnym zapisie oraz odnalezieniu przynajmniej jednego znacznika każdej wskazanej tabeli. Znacznik potwierdza wybór sekcji, a nie kompletność obiektu SQL. Istniejący wynik nie jest nadpisywany. Błędy kończą program kodem różnym od zera; częściowy fragment nie jest zgłaszany jako gotowy rezultat.
Od fragmentu tekstu do odtworzenia
Ustawienia sesji, takie jak zestaw znaków i tryb SQL, zależności kluczy obcych, triggery, widoki i procedury mogą znajdować się poza kopiowaną sekcją. Nietypowo zapisane ciągi SQL mogą też zawierać tekst podobny do komentarzy. Przykład nie interpretuje tych struktur i nie jest uniwersalnym filtrem importu.
Jeżeli dostępny jest serwer źródłowy, mysqldump przyjmuje nazwę bazy, a po niej konkretne nazwy tabel. Dla InnoDB opcja --single-transaction może zapewnić spójny zrzut; inne silniki i równoczesne zmiany struktury wymagają odpowiedniej strategii. Gdy pozostał wyłącznie zrzut, odtworzenie w odizolowanym środowisku i następnie eksport wybranych obiektów daje lepszą kontrolę. Przed importem potrzebna jest ocena wersji docelowej, miejsca, zależności i uprawnień.
Pytania i odpowiedzi
Jak szybko działa filtr?
Odczytywany jest cały plik wejściowy. Czas i pamięć zależą między innymi od nośnika, ilości zapisywanych danych i PHP. Uniwersalna obietnica przetwarzania wielu gigabajtów w kilka sekund nie jest wiarygodna. Ograniczony bufor zapobiega jedynie przechowywaniu całego bardzo długiego wiersza w pamięci PHP.
Co oznacza brak trafienia?
Nazwa może nie istnieć, mieć inną pisownię albo występować bez oczekiwanych komentarzy. Skrypt zgłasza wtedy błąd. Pusty fragment nie jest przedstawiany jako dowód pustej tabeli.
Czy sukces oznacza możliwość odtworzenia wszystkich wybranych obiektów?
Nie. Próba odtworzenia ze sprawdzeniem schematu, wierszy i zależności jest osobnym etapem. Oryginalny zrzut pozostaje niezmieniony.
Źródła i data weryfikacji
2026-10-24