10. seminář - Práce s textovými soubory

TEACHINGZPC2

Otevření souboru

Před tím než začneme pracovat se souborem, tak jej musíme otevřít. K tomu máme v knihovně stdio.h funkci fopen. Tato funkce bere jako svoje argumenty cestu k souboru a mód ve kterém chceme soubor otevřít a vrací ukazatel na strukturu FILE. Pokud otevření souboru selže, tak funkce vrací NULL.


    FILE* fopen(const char *filename, const char *mode);

Mód otevření souboru

Pomocí módu specifikujeme co chceme se souborem dělat. Můžeme si vybrat z následujícíchc možností:

  • "r" - Otevře soubor pro čtení
  • "w" - Vytvoří nový soubor pro zápis (pokud soubor existuje, smaže jej)
  • "a" - Otevře soubor pro zápis na konec souboru

Dále exitují varianty r+, w+, a+. Ty otevřou soubor současně pro zápis i čtení.

  
    // otevre soubor pro cteni  
    fopen("users.txt", "r");
    
    // pokud soubor existuje, smaze jeho obsah
    // a otevre ho jak pro cteni, tak pro zapis
    fopen("users.txt", "w+");
    
    // otevre soubor pro zapis, nesmaze obsah
    // a zapisovat se bude na konec souboru
    fopen("users.txt", "a");

Textový vs. binární režim

Ke všem předchozím módům ještě můžeme přidat t nebo b. Mód t značí textový mód, mód b je binární. Na tomto semináři se budeme zabývat pouze textovými soubory.

    
    // otevre soubor pro cteni v textovem modu
    fopen("seznam.txt", "rt");

Co je to datový proud

Jedná se o posloupnost dat (vstupní/výstupní), kde každá hodnota obsažená v proudu může být. přečtena pouze 1 a hodnoty nelze přeskakovat. Například u souborů se jedná o vstupní datový proud (angl. stream), naopak printf odesílá řetězec do výstupního datového proudu (standartní výstup).

Čtení z textového souboru

Ke čtení jednoho znaku ze souboru používáme funkci fgetc. Tato funkce bere jako svůj jedniný argument ukazatel na stream (FILE*)


    int fgetc(FILE *stream)

Uvažujme soubor users.txt který obrahuje následující data:


    Darth Vader
    Obi-Wan Kenobi
    Yoda

Kód který přečte první 3 znaky vypadá následovně:


    FILE* file = fopen("users.txt", "r");
    printf("%c\n", fgetc(file)); // D
    printf("%c\n", fgetc(file)); // a
    printf("%c\n", fgetc(file)); // r

Čtení jednoho řádku

Pokud chceme přečíst jeden řádek včetně znaku '\n', tak k tomu máme funkci fgets. Tato funkce načte do parametru str maximálně num znaků (včetně ukončovací nuly) ze souboru stream.


    char* fgets(char *str, int num, FILE *stream);

Příklad čtení ze souboru


    #define LINE_SIZE 50

    FILE* file = fopen("users.txt", "r");
    char* line = (char*) malloc(LINE_SIZE * sizeof(char));
    
    fgets(radek, LINE_SIZE, file);
    printf("%s\n", line); // Přečte: Darth Vader\n
    fgets(line, 5, file);
    printf("%s\n", line); // Obi-

Všimněte si, že druhý fgets nezačal číst od začátku souboru, ale pokračoval tam, kde první fgets skončil. Druhý fgets také přečetl pouze 4 znaky, 5. použil na uložení ukončovací nuly.

Formátované čtení

Pro formátované čtení používáme funkci fscanf, která je podobná funkci scanf.


    int fscanf(FILE *stream, const char *format, ... );

Návratovou hodnotou této funkce je počet úspěšně načtených hodnot.

Příklad


    // Obsah souboru: 10 3.14! Neprecte se.
    
    int number;
    double double_number;
    char character;
    FILE* soubor = fopen("file.txt", "r");
    fscanf(soubor, "%i %lf%c", &number, &double_number, &character);
    printf("%i, %g, %c\n", number, double_number, character); // 10, 3.14, !

Jak poznat konec souboru

Pokud jsme již narazili na konec souboru, vrací funkce fgetc a fscanf konstantu EOF („End of file) a funkce fgets vrací NULL.


    char character;
    FILE* file = fopen("file.txt", "r");
    
    while((character = fgetc(file)) != EOF) {
        printf("%c\n", character);
    }
    // program vypise kazdy znak souboru
    // na samostatny radek a skonci

Toto řešení není dokonalé – fgetc vrací EOF i při chybě.

Funkce feof

Funkce feof zjišťuje, jestli už jsme přečetli celý soubor. Pokud jsme ho přečetli celý, vrací 1, jinak 0.


    int feof(FILE *stream);

Pozor – funkce vrací 1 jen v případě, kdy už jsme se pokusili přečíst znak mimo soubor. Pokud má soubor 10 znaků, my přečteme 10 znaků, funkce feof vrací 0.

Špatné použití funkce feof


    // Obsah souboru: "abcd"
    FILE* file = fopen("file.txt", "r");
    while(!feof(file)) {
        printf("%c, ", fgetc(file));
    }
    
    // Výstup:
    // a, b, c, d, EOF,

Správné použití funkce feof


    // Obsah souboru: "abcd"
    FILE* file = fopen("file.txt", "r");
   
    while(1) {
        char c = fgetc(file);
        if (feof(file)) {
            return 0;
        }
        printf("%c, ", c);
    }
    
    // Výstup:
    // a, b, c, d,

Navrácení znaku do proudu

Častým požadavkem je něco jako: "čti znaky, dokud nenarazíš na číslici". Což ale znamená, že se zarazíme až ve chvíli, kdy nějakou číslici přečteme. Pokud čteme soubor s obsahem abcdef123456, zůstane nám ve zbytku proudu obsah 23456. Funkce ungetc vrací znak do proudu.


    int ungetc(int character, FILE *stream);
    
    // Použití
    if (is_digit(character)) {
        ungetc(character, file);
    }

Zápis do textového souboru

Zápis do souboru probíhá analogicky ke čtení. Nejdříve je nutné soubor otevřít v módu pro zápis a pak již je možné do souboru zapisovat pomocí následujících funkcí.

Funkce fputc

Funkce zapíše jeden znak do souboru.


    int fputc(int character, FILE* stream);
    
    // Použití
    FILE* file = fopen("file.txt", "w");
    fputc('4', file);
    fputc('2', file);

Zápis do textového souboru v ”a” módu

V módu "a" budeme zapisovat na konec souboru.


    FILE* file = fopen("users.txt", "a");
    fputc('4', file);
    fputc('2', file);
    
    // Obsah souboru users.txt:
    Darth Vader
    Obi-Wan Kenobi
    Yoda42

Zápis textového řetězce

Pro zapsání textového řetězce používáme funkci fputs. Funkce zapíše do souboru celý řetězec vyjma ukončovací nuly.


    int fputs(const char *str, FILE *stream);
    
    // Použití
    FILE* file = fopen("file.txt", "w");
    fputs("Hello", file);
    fputs(" World!", file);
    
    // Obsah souboru:
    Hello World!

Funkce fprintf

Funkce je stejná jako funkce printf, akorát se zapisuje do souboru.


    int fprintf(FILE *stream, const char *format, ...);
    
    // Použití
    FILE* file = fopen("file.txt", "w");
    fprintf(file, "%i -- %g", 12, 3.0 / 7);
    
    // Obsah souboru:
    12 -- 0.428

Uzavření souboru

Po ukončení práce se souborem vždy musíme soubor uzavřít pomocí funkce fclose:


    int fclose(FILE * stream);

Proč uzavírat?

  • Aby se skutečně zapsal obsah na disk. Data se po použití zapisovacích funkcí ukládají do mezipaměti, zápis na disk je drahá záležitost, proto se to OS snaží optimalizovat.
  • Mohou existovat limity na počet otevřených souborů pro jeden program.
  • Jiný program může chtít zapisovat do stejného souboru.

Příklad


    FILE* file = fopen("file.txt", "w");
    // zapis, cteni, cokoliv...
    fclose(file);

Vynucení zápisu bez uzavření souboru

Pokud chceme vynutit uložení dat z mezipaměti na disk, můžeme použít funkci fflush:


    FILE* file = fopen("file.txt", "w");
    fprintf(file, "%i -- %g", 12, 3.0 / 7);
    fflush(file);
    fputs("dalsi zapis", file);
    fclose(file);

Funkce fflush ve skutečnosti nevynucuje zápis na disk, vynucuje jen uvolnění mezipaměti. Data ale mohou skončit v další mezipaměti.

Odchytávání chyb

Jakákoliv práce s diskem může selhat. Soubor nemusí existovat, program nemusí mít práva daný soubor otevřít, disk může být plný, ...

Po každém volání některé z funkcí bychom tak měli otestovat, zda vše proběhlo v pořádku.

Pokud dojde k chybě

Funkce fputc, fputs, fgetc, fscanf, fclose a ungetc vrací při chybě EOF a funkce fgets, fopen vrací NULL.

Kód s ošetřením chyb při zápisu


    FILE* file = fopen("file.txt", "r");
    
    if (file) {
        if (fputs('!', file) ==  EOF) {
             // Nepodarilo se zapsat znak
        }
        if (fclose(file) == EOF) {
            // Nepodarilo se uzavrit soubor
        }
    } else {
        // Soubor se nepodarilo otevrit
    }

Kód s ošetřením chyby při čtení


    int character;
    FILE* file = fopen("file.txt", "r");
    
    if (file) {
        character = fgetc(file);
        if (character == EOF) {
            if (feof(file)) {
                // OK, jsme na konci souboru
            } else {
                // Hmm, nepodarilo se precist znak
            }
        } else {
            printf("Nacteny znak: %c\n", character);
    } else {
        // nepodarilo se otevrit soubor
    }

Přečetli jsme celý soubor?


    FILE* file = fopen("file.txt", "r");
    char* buffer = (char*) malloc(30 * sizeof(char));
    
    // nacitej radky, dokud fgets nevrati NULL.
    // pak jsme bud precetli cely soubor, nebo nastala chyba
    
    while (fgets(buffer, 30, file)) {
        printf("%s", buffer);
    }

    if (feof(file)) {
        printf("Konec souboru.\n");
    } else {
        printf("Nastala chyba.\n");
    }

Typ chyby

V knihovně errno.h existuje globální proměnná errno, ve které je uloženo číslo chyby. Funkce perror slouží k vypsání chybové hlášky. Řetězec, který bere jako parametr, vypíše před chybovou hláškou.


    void perror(const char * str);

Příklad


    FILE* file = fopen("neexistujici_soubor", "r");
    
    if (file) {
        // udelej neco se souborem...
    } else {
        perror("Chyba");
        printf("ID chyby: %i\n", errno); 
        
        switch (errno) {
           case // Kód chyby
        }
    }

    // Vystupem bude:
    // Chyba: No such file or directory
    // ID chyby: 2

Ošetření chyb pomocí errno

Proměnnou errno můžeme použít k ošetření chyb. Pokud chyba nenastala, má proměnná errno hodnotu 0. Jinak obsahuje číslo chyby. Musíme ale testovat hodnotu hned po provedení funkce! Aby hodnotu nepřepsala zase jiná funkce.


    FILE* file = fopen("file.txt", "r");
    if (errno) {
        printf("Cislo chyby: %i\n", errno); 
    } else {
        // zpracovani souboru...
    }

Relativní cesta k souboru

Absolutní cesta: /cesta/ke/slozce na unixu, C:\cesta\ke\slozce na Windows

Relativní cesta: např. zmíněné fopen("file.txt", "r"). Cesta je vždy relativní vůči umístění, ze kterého se program spouští, ne vůči souboru, ve kterém je program uložený.

Tj. pokud otevřeme příkazový řádek ve složce /home/vyjidacek/zpc2/ a spustíme v něm program ./home/vyjidacek/programs/load_file, bude se hledat ve složce /home/vyjidacek/zpc2/.

Úkoly

Napište program, tak aby zkusil číst neexistující soubor. Zajistěte, aby program vhodně reagoval na tuto situaci.
Napište program, který́ spočítá celkový počet znaků v souboru.
Napište program, který do souboru uloží prvních 10 násobků čísel od 1 do 10 (každá série na jeden řádek). V souboru tedy bude 10 řádků, první bude obsahovat čísla od 1 do 10, druhý násobky 2 apod.
Napište program, který přečte jména ze souboru names.txt, setřídí je a uloží do nového souboru names_sorted.txt.