Diesmal wollen wir eine Textdatei einlesen, verarbeiten und das Ergebnis in eine zweite Textdatei ausgeben. Die Eingabedatei ist eine CSV-Datei, allerdings mit Semikolon als Trennzeichen statt Komma. Die Datei enthält Informationen über die Hauptstädte der Erde; die Daten stammen aus Wikipedia. Zur Vermeidung unnötiger Probleme habe ich sie um Sonderzeichen bereinigt.
Die Datei hat den Namen
hauptst.csv und enthält die Felder Staat, Hauptstadt, Einwohner, Stand und Quelle. Wir wollen daraus eine Datei erstellen, die zu jeder Hauptstadt die Anzahl ihrer Einwohner im Format
<Hauptstadt>:
<Anzahl Einwohner> ausgibt.
Dazu öffnen wir zunächst die Eingabe- und die Ausgabedatei. Dann lesen wir in einer Schleife Zeile für Zeile ein, verarbeiten die Zeile und schreiben das Ergebnis in die Ausgabedatei. Zuletzt schließen wir beide Dateien.
Beginnen wir mit der Funktion main(), wo wir zuerst die benötigten Variablen deklarieren:
Code: Alles auswählen
/* Main
*/
int main(void) {
char filename[13];
char filenameOut[13];
FILE *fp;
FILE *fpOut;
char buf[100];
t_line line;
Die beiden Arrays für die Dateinamen sind 13 Zeichen lang; das genügt für 8.3-Dateinamen plus das String-Ende-Zeichen '\0'. Wenn man noch eine Laufwerksangabe etc. unterbringen möchte, dann muss man die Arrays natürlich entsprechend größer dimensionieren. Es folgen zwei file pointer, ein Puffer für die aktuell eingelesene Zeile und eine Variable für das Ergebnis der verarbeiteten Zeile mit den einzelnen Feldern. Letztere definieren wir folgendermaßen mit typedef:
Code: Alles auswählen
typedef struct {
char staat[60];
char hauptstadt[50];
char einwohner[12];
char stand[10];
char quelle[40];
} t_line;
Alle Felder sind als char-Arrays definiert, weil eine Textdatei nun einmal Textzeichen enthält. Nun könnte man argumentieren, dass die Anzahl der Einwohner doch eigentlich eine Zahl ist und man besser einen Zahlentyp verwenden sollte. Aber das brauchen wir nur dann, wenn wir damit Berechnungen anstellen wollen, wie z.B. die Summe der Einwohner aller Hauptstädte zu ermitteln. Für unseren Anwendungsfall brauchen wir das nicht, also verzichten wir auf eine Typumwandlung.
Wir öffnen die Eingabedatei:
Code: Alles auswählen
strcpy(filename, "hauptst.csv");
fp = fopen(filename, "r");
if (fp == NULL) {
printf("Fehler: Eingabedatei %s nicht gefunden!\n", filename);
return -1;
}
Wir kopieren die String-Konstante "hauptst.csv" in die Variable
filename, weil wir sie weiter unten nochmal benötigen und wir den Dateinamen nicht doppelt in den Quellcode schreiben wollen. Dafür gibt es auch noch andere Lösungen. Die Datei wird wieder mit
fopen geöffnet, hier im Modus "r" (für "read"). Der resultierende file pointer
fp kann in der Folge nur für Leseoperationen verwendet werden. Liefert fopen einen NULL-Pointer zurück, dann ist das Öffnen fehlgeschlagen, was wir mit einer Fehlermeldung und dem Rücksprung ins Betriebssystem mit dem Fehlercode -1 quittieren.
Die Ausgabedatei wird ganz ähnlich geöffnet:
Code: Alles auswählen
strcpy(filenameOut, "extrakt.csv");
fpOut = fopen(filenameOut, "w");
if (fpOut == NULL) {
printf("Fehler: Konnte Ausgabedatei %s nicht oeffnen!\n", filenameOut);
fclose(fp);
return -1;
}
Hier verwenden wir den Modus "w" (für "write") und öffnen die Datei damit zum Schreiben. Ergo sind mit
fpOut nur Schreiboperationen möglich. Die Fehlerbehandlung ist ähnlich der bei der Eingabedatei, allerdings dürfen wir hier nicht vergessen die Eingabedatei zu schließen, bevor wir zum Betriebssystem zurückkehren, denn diese ist zu diesem Zeitpunkt ja bereits geöffnet.
Die Schleife für das Lesen der Zeilen sieht folgendermaßen aus:
Code: Alles auswählen
// Zeilen lesen
while (fgets(buf, sizeof buf, fp) != NULL) {
printf(".");
// Zeile in Felder aufteilen
parseLine(buf, &line);
// Hauptstadt und Einwohner schreiben
fprintf(fpOut, "%s: %s\r", line.hauptstadt, line.einwohner);
}
putchar('\n');
Die Funktion
fgets liest eine ganze Zeile ein. Die maximale Länge der Zeile wird dabei durch die Länge des Puffers
buf begrenzt, die wir mit
sizeof buf ermitteln. Der Compiler-Operator
sizeof ermittelt die Größe einer Variablen zur Compile-Zeit. Hier liefert er den Wert 100.
fgets liefert einfach den ersten Parameter, also
buf, zurück, es sei denn, wir sind am Dateiende angekommen: dann liefert
fgets NULL zurück, was wir als Schleifenende-Kriterium verwenden.
Die Ausgabe eines Punkts pro Zeile soll nur etwas visuelles Feedback für das Lesen der Zeilen geben. Die Funktion parseLine soll dann die Zeile in die Felder Staat, Hauptstadt, Einwohner, Stand und Quelle aufteilen und das Ergebnis in die Struktur
line schreiben. Der typedef für die Struktur sieht so aus:
Code: Alles auswählen
typedef struct {
char staat[60];
char hauptstadt[50];
char einwohner[12];
char stand[10];
char quelle[40];
} t_line;
Schließlich werden die Felder Hauptstadt und Einwohner via
fprintf in die Ausgabedatei geschrieben. Das
putchar soll nur einen Zeilenumbruch nach den vielen Punkten ausgeben, damit der Cursor nach Beendigung des Programms wieder am Zeilenanfang steht, wo dann der Prompt des Betriebssystems erscheint.
Am Ende von main() müssen wir noch die Dateien schließen:
Jetzt fehlt noch die Funktion
parseLine zum Aufteilen der eingelesenen Zeilen:
Code: Alles auswählen
/* Zerlegt eine Zeile in Felder.
*/
char *parseLine(char *buf, t_line *line) {
int i;
int len;
initLine(line);
buf = parseToken(buf, line->staat, ';');
buf = parseToken(buf, line->hauptstadt, ';');
buf = parseToken(buf, line->einwohner, ';');
buf = parseToken(buf, line->stand, ';');
buf = parseToken(buf, line->quelle, ';');
}
Zunächst wird die Struktur
line initialisiert. Beim Aufruf von
parseLine hatten wir die Adresse der Struktur mit
&line als zweiten Parameter übergeben. Wir haben jetzt also einen Pointer auf die Struktur. Den übergeben wir an
initLine, um die Struktur zu initialisieren.
initLine setzt nur das erste Zeichen aller char-Arrays auf '\0' und erzeugt damit leere Strings. Nota bene: das erste Zeichen eines Arrays ist in C immer das Zeichen mit dem Index 0!
Code: Alles auswählen
void initLine(t_line *line) {
line->staat[0] = '\0';
line->hauptstadt[0] = '\0';
line->einwohner[0] = '\0';
line->stand[0] = '\0';
line->quelle[0] = '\0';
}
So, die Felder sind leer, also können wir sie jetzt mit den Daten der aktuellen Zeile füllen. Das erledigt die Funktion
parseToken, die den char-Pointer buf sukzessive weiterschaltet, bis alle fünf Felder eingelesen sind.
buf zeigt also immer auf den Anfang des nächsten Feldes im Puffer. Man beachte, dass die Parameter-Variable buf eine eigene lokale Variable der Funktion
parseLine ist. Wenn wir sie verändern, dann wird die gleichnamige Variable
buf der Funktion main() nicht verändert!
Code: Alles auswählen
/* Liest ein Feld bis zum Trennzeichen.
* Gibt einen Zeiger auf das naechste Feld zurueck.
*/
char *parseToken(char *start, char *field, char delimiter) {
while (*start != delimiter && *start != '\0') {
*field++ = *start++;
}
*field = '\0';
return ++start;
}
In der while-Schleife wird Zeichen für Zeichen aus dem Puffer in die jeweilige Feld-Variable aus der Struktur
line kopiert, bis wir das Trennzeichen oder das String-Ende finden. Das Kopieren könnten wir mit der Anweisung
*field = *start durchführen; anschließend müssten wir noch die beiden Pointer weiterschalten. Die hier gezeigte Variante führt die Zuweisung durch und schaltet anschließend die Pointer weiter - alles auf einmal. Nach der Schleife zeigt
start auf das Trennzeichen, daher das Inkrementieren vor dem return.
Einige Dinge hätte man auch anders machen können. So wäre es z.B. möglich ohne Kopieren der Strings auszukommen, indem man '\0'-Zeichen in den Puffer schreibt, wo die Trennzeichen sind. Oder man hätte mit
strtok arbeiten können - eine String-Funktion, die Tokens anhand von Trennzeichen ermittelt. Diese hat allerdings den Nachteil, dass sie aufeinanderfolgende Trennzeichen wie eines behandelt. Das taugt nichts, wenn Felder auch leer sein können.
Auch die Struktur wäre nicht unbedingt nötig. Man könnte stattdessen mit separaten Array-Variablen arbeiten, die nicht zu einer Struktur zusammengefasst sind. Dann hätte man allerdings auch alle Felder einzeln als Parameter an die Unterfunktionen übermitteln müssen - oder man hätte die Arrays gleich global gemacht. Letzteres ist aber wegen des "information hiding"-Prinzips in der Software-Entwicklung nicht wünschenswert.
Hier ist noch das fertige C-Programm und die Eingabedatei zum Download:
- file_io_2.zip
- file2.c und hauptst.csv
- (4.85 KiB) 536-mal heruntergeladen
Das ganze wird kompiliert mit