Ich stehe gerade vor dem Problem, einige Zeilen aus einer großen (Gigabyte) Datei ausschneiden zu müssen. Da ich mir der potenziellen CPU-Belastung beim Lesen der Datei im Speicher bewusst bin, wollte ich sie stattdessen direkt bearbeiten ... und bin dabei auf diese Fragen gestoßen:
- Wie entferne ich bestimmte Zeilen (mithilfe von Zeilennummern) in einer Datei?
- Gibt es eine Möglichkeit, eine Datei direkt vor Ort zu ändern?
... und weiters auch diese:
- Wie kann ich eine große Datei direkt bearbeiten?
- Wie kann ich eine große Datei an Ort und Stelle ausschneiden?
Ich habe mich jedoch über etwas anderes gewundert: Ich glaube (bin mir aber nicht sicher), dass jedes Dateisystem (wie ext3
) so etwas wie eine verknüpfte Liste verwenden müsste, um so etwas wie Fragmente einer Datei beschreiben zu können, die auf Bereiche der Festplatte abgebildet sind.
Daher sollte es möglich sein, so etwas zu tun. Nehmen wir zum Beispiel an, ich habe eine Datei bigfile.dat
wie diese (die Zahlen sollten den Byte-Offset angeben, aber es ist ein bisschen schwierig, sie auszurichten):
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
L 1\n L 2\n L 3\n L 4\n L 5\n L 6\n
Diese Datei könnte dann im Prinzip in eine Terminalanwendung zum Durchsuchen geladen werden - nehmen wir an, wir rufen ein Tool auf editsegments bigfile.dat
und sagen, es less -N bigfile.dat
würde dieselbe Datei (mit Zeilennummern) ähnlich wie folgt anzeigen:
1 1 L 1
2 2 L 2 *
3 3 L 3
4 4 L 4 *
5 5 L 5
6 6 L 6
bigfile.dat (END)
Ich könnte dort beispielsweise einen Befehl eingeben (etwa d
zum Löschen von Zeilen), eine andere Taste oder die Maus dort klicken, wo es oben mit - steht, und *
damit sagen, dass alles zwischen den Zeilen 2 und 4 gelöscht werden soll. Das Programm würde dann antworten und folgendes anzeigen:
1 1 L 1
2 5 L 5
3 6 L 6
bigfile.dat (END)
Jetzt können wir sehen, dass die erste Spalte ganz links die „neue“ Zeilennummer (nach dem Schnitt) zeigt, die zweite Spalte die „alte“ Zeilennummer (vor dem Schnitt) – und dann folgt der eigentliche Zeileninhalt.
Ich stelle mir vor, dass nach editsegments
dem Beenden dieser Pseudoanwendung zunächst alles bigfile.dat
unverändert bleibt. Allerdings würde sich jetzt im selben Verzeichnis auch eine zusätzliche Textdatei befinden, beispielsweise bigfile.dat.segments
mit folgendem Inhalt:
d 4:15 # line 2-4
... und zusätzlich würde eine spezielle Datei (wie ein „Symlink“) – nennen wir sie bigfile.dat.iedit
– erscheinen.
Das Ergebnis von all dem wäre im Grunde, dass ich, wenn ich jetzt versuche, bigfile.dat.iedit
mit etwas wie zu öffnen less -N bigfile.dat.iedit
, den „bearbeiteten“ Inhalt erhalten möchte:
1 L 1
2 L 5
3 L 6
bigfile.dat (END)
... was, denke ich, dadurch erreicht werden könnte, dass man dem Betriebssystem irgendwie die Anweisung gibt, dass es beim $FILE.iedit
Öffnen zuerst $FILE.segments
öffnen und lesen soll; das d 4:15
würde dann anweisen, dass die Bytes 4 bis 15 in der Originaldatei ausgelassen werden sollen - was zu etwa folgendem Ergebnis führen würde:
0 1 2 3 4 5 6 7 8 9 10 11 12,3,4 15 16 17 18 19 20 21 22 23
L 1\n
L
2
\n
L
3
\n
L
4
\n
L 5\n
L 6\n
0 1 2 3 ------------------------------->16 17 18 19 20 21 22 23
Mit anderen Worten -vorausgesetztdass im Dateisystemkonzept einer Datei jedes Inhaltsbyte auch einen „Link“ zum nächsten Byte in der Kette enthält – es sollte möglich sein, das Dateisystem anzuweisen, auf der Grundlage eines Skripts eine neue verknüpfte Liste zu erstellen und den Inhalt, wie er durch diese geänderte verknüpfte Liste dargestellt wird, über eine spezielle Datei (Symlink oder Pipe) bereitzustellen.
Das ist, was ich mit „geskriptet“ im Titel meinte – dass die „neue“ verknüpfte Liste durch eine Skriptdatei ( $FILE.segments
) gesteuert werden kann, die vom Benutzer in einem Texteditor bearbeitet werden kann (oder von einer Frontend-Anwendung generiert wird). Was ich mit „Multipass“ meinte, ist die Tatsache, dass bigfile.dat
in diesem Prozess überhaupt nichts geändert wird; ich könnte also heute das erste (ursprüngliche) Gigabyte bearbeiten und den Fortschritt in () speichern $FILE.segments
– dann könnte ich morgen das zweite Gigabyte bearbeiten und den Fortschritt wieder in ( $FILE.segments
) speichern usw. – währenddessen bigfile.dat
bleibt das Original unverändert.
Wenn alle Änderungen abgeschlossen sind, könnte man wahrscheinlich eine Art Befehl aufrufen (z. B. editsegments --finalize bigfile.dat
), der die neue verknüpfte Liste einfach dauerhaft als Inhalt von kodiert bigfile.dat
(und entsprechend bigfile.dat.segments
und entfernt bigfile.dat.iedit
). Oder noch einfacher: Man könnte einfach Folgendes tun:
cp bigfile.dat.iedit /path/to/somewhere/else/bigfile.modified.dat
Natürlich d
könnte man neben einem Elete-Skriptbefehl r
auch einen Eplace-Befehl haben, etwa:
r 16:18 AAA
... mit der Aussage: Ersetzen Sie den Inhalt zwischen den Bytes 16 und 18 durch die nächsten 18-16+1=3 Bytes nach dem Leerzeichen (also dem AAA
) – die verknüpfte Liste könnte sich tatsächlich in den Inhalt des Skriptbefehls selbst „einhaken“ (die folgende Tabelle enthält auch das d
Elete):
0 1 2 3 4 5 6 7 8 9 10 11 12,3,4 15 16 17 18 19 20 21 22 23
L 1\n
L
2
\n
L
3
\n
L
4
\n
L
5
\n
L 6\n
0 1 2 3 ----------------------------->| | 19 20 21 22 23
.
.
.
.
.
.
.
\n
r
1
6
:
1
8
AAA
\n
.
.
.
.
Nun, ich vermute, dass Programme wie hexedit
(wie erwähntHier) ändern Dateien direkt vor Ort – aber ich möchte den Vorteil der Möglichkeit der Skripterstellung (noch besser, wenn dies über eine GUI-Anwendung geregelt werden könnte, selbst wenn es sich um eine Terminalanwendung handelt) und den Vorteil, dass die Originaldatei nicht tatsächlich geändert wird, bis bestätigt wurde, dass alle Änderungen wie erforderlich sind.
Ich bin nicht sicher, ob so etwas überhaupt möglich ist – und selbst wenn, vermute ich, dass dafür ein dedizierter Treiber erforderlich ist (und nicht nur ein Benutzerprogramm) … Aber ich denke, die Frage ist es trotzdem wert: Gibt es so etwas für Linux?
Vielen Dank im Voraus für alle Antworten,
Prost!
Antwort1
Die Struktur der Dateien auf der Festplatte hängt vom verwendeten Dateisystem ab. Keines der realen Dateisysteme verwendet verknüpfte Listen, wie Sie es beschreiben (das wäre fseek(3)
unerträglich). Am nächsten kommt dem MicrosoftsFETT, wodurch die Zeiger im Wesentlichen aus den Datenblöcken in ein Array verschoben werden, das sie überschattet.
Die meisten Dateisysteme verwenden jedoch einige zeigerbasierte Verweise auf Datenblöcke in der Datei. Im Prinzip könnte man also einen Block aus einer Datei ausschneiden, indem man einfach eine Handvoll Zeiger (nicht den gesamten Dateiinhalt) verschiebt und einen Block in der Mitte der Datei als frei markiert. Leider ist dies keine sehr nützliche Operation, da Dateiblöcke ziemlich groß sind (normalerweise 4 KB) und sich selten vernünftig an den Strukturen in der Datei ausrichten (seien es Zeilen oder andere Unterteilungen).
Antwort2
Was Sie beschreiben, klingt sehr nach einemWiederholungeines TexteditorsWiederherstellen-Listemit der unveränderten Originaldatei, zu der dasWiederherstellen-Listegehört. Ich bin ziemlich sicher, dass das gvim
so einhartnäckigUndo/Redo-Liste, die Sie vielleicht(?) nutzen können, und ich weiß, dass emacs
es definitiv eine solche Liste gibt, die Sie höchstwahrscheinlich dazu bringen könnten, alles zu tun, was Sie wollen (über ein elisp
Skript), z. B.Speichern Sie den Emacs-Rückgängig-Verlauf zwischen den Sitzungen.
Als Randbemerkung: Bei so großen Dateien kann es sinnvoll sein, alle unerwünschten Aktionen zu deaktivieren, z. B.:Automatisches Speichern,Syntaxhervorhebung(langsam auf einemgroßemacs-Datei), usw. und emacs auf einem 32-Bit-System hat eine 256 MBDateigrößenbeschränkung.
Es wird sicherlich nicht so prägnant sein wie Ihr Vorschlag, könnte aber brauchbar sein, wenn es nicht zu viele Änderungen gibt.
Antwort3
Im Allgemeinen können Sie eine Datei nicht direkt bearbeiten, ohne die gesamte Datei in den Speicher zu laden. Ich gehe davon aus, dass Sie eigentlich nur eine neue Datei haben möchten, die eine Kopie der alten ohne bestimmte Zeilen ist. Dies lässt sich leicht mit den Unix-Dienstprogrammen head
und erreichen tail
. Um beispielsweise alles außer den Zeilen 5, 12 und 52 aus einer Datei zu kopieren, können Sie Folgendes tun:
head -n 4 bigfile.dat > tempfile.dat
tail -n +6 bigfile.dat | head -n 6 >> tempfile.dat
tail -n +13 bigfile.dat | head -n 39 >> tempfile.dat
tail -n 53 bigfile.dat >> tempfile.dat
Falls Sie mit diesen Dienstprogrammen nicht vertraut sind, werde ich sie genauer erklären.
Das head
Dienstprogramm druckt die ersten n Zeilen einer Datei aus. Wenn kein Positionsargument angegeben wird, wird die Standardeingabe als Datei verwendet. Das -n
Flag teilt head mit, wie viele Zeilen ausgedruckt werden sollen. head -n 2
Es werden also nur die ersten 2 Zeilen der Standardeingabe ausgedruckt.
Das tail
Dienstprogramm druckt die letzten n Zeilen einer Datei aus. Wie head kann es aus einer Datei oder der Standardeingabe lesen. Das Flag -n teilt tail mit, wie viele Zeilen vom Ende aus ausgedruckt werden sollen. Sie können der Zahl auch ein Pluszeichen voranstellen, um tail anzuweisen, die Zeilen vom Ende der Datei auszugeben, beginnend mit so vielen Zeilen vom Anfang. tail -n 2
Druckt beispielsweise die letzten beiden Zeilen der Standardeingabe aus. tail -n +2
Druckt jedoch alle Zeilen ab Zeile 2 aus (Zeile 1 wird weggelassen).
Wenn Sie also im Allgemeinen Zeilen im Bereich [x, y) aus einer Datei drucken möchten, tun Sie dies
`tail -n +x | head -n d`
wobei d = y - x. Diese Befehle erzeugen eine neue Datei. Sie können die alte Datei dann löschen, wenn Sie möchten. Der Vorteil dieser Vorgehensweise besteht darin, dass Sie immer head
nur tail
eine Zeile im Speicher behalten müssen, sodass Ihr RAM nicht schnell voll wird.
Antwort4
Klingt nach einem Job für ein Sed-Skript. Wenn ich mich richtig erinnere, wurde es für solche Aufgaben entwickelt. Zeilenweise Verarbeitung, wiederholte Verarbeitung derselben Befehlsgruppe und Regex – alles in einem Tool vereint. Obwohl ich weiß, dass es den Job erledigen wird, kann ich Ihnen nicht weiterhelfen, außer Sie auf die entsprechenden Anweisungen hinzuweisen.manpage.