NOVINKA - Online rekvalifikační kurz Python programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Diskuze: Obrácení směru souboru

V předchozím kvízu, Online test znalostí C++, jsme si ověřili nabyté zkušenosti z kurzu.

Aktivity
Avatar
Denisa Bednarova:30.9.2023 22:38

Ahoj, potřebuju číst textový soubor od konce na začátek, zapisovat do dočasného souboru a ten potom překopírovat zpátky do původního souboru - tedy v obráceném pořadí než byl původně. Našla jsem samé postupy využívající funkci fseek a ftell - abych se posunula na konec původního souboru a poté posouvala pozici po jedné směrem k začátku. Potíž je, že funkce fseek a ftell není vhodná pro práci s textovými řetězci kvůli speciálním symbolům, které zabývají více místa, liší se podle prostředí ve kterém byly vytvořeny, někdy \r\n jindy jen \n. Potřebuju tedy obrátit soubor bez použití fseek.
To se mi podařilo pro malý soubor, zadání je však pro soubor který je větší než paměť RAM, může být i větší než polovina disku. Pravděpodobně by bylo třeba pracovat s buffrem, ale tam jsme ještě nedošli.
Moc děkuju za jakoukoli radu, nechci aby za mě někdo vyloženě udělal celou práci, nejspíše bude stačit nasměrovat kterým směrem se vydat. Možná dynamické alokace pomocí Malloc a free? Napadlo mě ve druhém řešení zapisovat těsně před dosáhnutím konce pole a pole poté plnit znovu od 0, v praxi to však nefunguje.

Zkusil jsem: Pro binární soubory by to fungovat mělo, ale nevhodné pro textové:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//#define VBUFFERU 1024
#pragma warning(disable : 4996)
FILE* fl1, * fl2;
void error(char chyba, char* retezec)
{
        if (chyba == 'a')
                printf("Spatny pocet argumentu. Je potreba zadat %s -i vstup\n", retezec);
        else if (chyba == 'p')
                printf("Nebyl nalezen prepinac \"-i\"\n");
        else if (chyba == 's')
                printf("Nebyl nalezen vstupni soubor\n");
        else if (chyba == 'o')
                printf("Nepodarilo se otevrit soubor %s\n", retezec);
        else if (chyba == 'd')
                printf("Nepodarilo se vytvorit docasny soubor \n");
        else if (chyba == 'x')
                printf("Nepodarilo se vytisknout textovy soubor do binarniho\n");
        printf("Soubor bude ukoncen.");
        exit(1);
}
void zpracuj(int argc, char** argv, int* p, char** soubor)
{
        int i = 0;
        if (argc != 3)
                error('a', argv[0]);
        for (i = 1; i < argc; i++)
                if ((strstr(argv[i], "-i") != 0))
                {
                        *p = 1;
                        if (atoi(argv[i + 1]) == 0) //nalezne soubor
                                *soubor = argv[i + 1];
                }
        if (p == 0)
                error('p', *soubor);
        else if (soubor == NULL)
                error('s', *soubor);
}
void obratSoubor(char* soubor) {
        int c,velikost,i;

        if (!(fl1 = fopen(soubor, "rb")))//otevřu pro čtení
                error('o', soubor);//vrátí chybu, že nebylo možné otevřít původní soubor pro čtení
        if (!(fl2 = fopen("docasna.bin", "wb")))//otevřu pro čtení
                error('d', soubor);//vrátí chybu, že nebylo možné otevřít dočasný soubor pro zápis
        fseek(fl1, 0, SEEK_END);//přesune ´pozici na konec původního souboru
        velikost = ftell(fl1);//zjistí aktuální pozici v souboru, tedy velikost původního souboru
        for (i = velikost - 1; i >= 0; i--)
        {
                fseek(fl1, i, SEEK_SET);
                c = fgetc(fl1);
                fputc(c, fl2);
        }
        fclose(fl1);//zavřu původní soubor
        fclose(fl2);//zavru dočasný soubor
        if (!(fl1 = fopen(soubor, "wb")))//otevřu pro čtení
                error('o', soubor);//vrátí chybu, že nebylo možné otevřít původní soubor pro zápis
        if (!(fl2 = fopen("docasna.bin", "rb")))//otevřu pro čtení
                error('d', soubor);//vrátí chybu, že nebylo možné otevřít dočasný soubor pro čtení
        while ((c = getc(fl2)) != EOF)//přečte z dočasného souboru
                fprintf(fl1,"%c", c);//zapíše do původního souboru
        fclose(fl1);//zavřu původní soubor
        fclose(fl2);//zavru dočasný soubor
}
int main(int argc, char** argv)
{
        int prepinac = 0;
        char* soubor;
        zpracuj(argc, argv, &prepinac, &soubor);
        if (prepinac == 1)
                obratSoubor(soubor);
        getchar();
}

2. postup pro malé textové soubory:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define VBUFFERU 1024
#pragma warning(disable : 4996)
FILE* fl1;
void error(char chyba, char* retezec)
{
        if (chyba == 'a')
                printf("Spatny pocet argumentu. Je potreba zadat %s -i vstup\n", retezec);
        else if (chyba == 'p')
                printf("Nebyl nalezen prepinac \"-i\"\n");
        else if (chyba == 's')
                printf("Nebyl nalezen vstupni soubor\n");
        else if (chyba == 'o')
                printf("Nepodarilo se otevrit soubor %s\n", retezec);
        else if (chyba == 'd')
                printf("Nepodarilo se vytvorit docasny soubor \n");

        printf("Soubor bude ukoncen.");
        exit(1);
}
void zpracuj(int argc, char** argv, int* p, char** soubor)
{
        int i = 0;
        if (argc != 3)
                error('a', argv[0]);
        for (i = 1; i < argc; i++)
                if ((strstr(argv[i], "-i") != 0))
                {
                        *p = 1;
                        if (atoi(argv[i + 1]) == 0) //nalezne soubor
                                *soubor = argv[i + 1];
                }
        if (p == 0)
                error('p', *soubor);
        else if (soubor == NULL)
                error('s', *soubor);
}

void vynulujPole(int pole[])
{
        int i;
        for (i = 0; i < VBUFFERU; i++)
                pole[i] = NULL;
}
void obratSoubor(int pole[], char* soubor)
{
        int i = 0,c;
        vynulujPole(pole);
        fl1 = fopen(soubor, "r");
        while((c=getc(fl1))!=EOF)
        {
                pole[i] = c;
                i++;
        }
        fclose(fl1);
        fl1 = fopen(soubor, "w");
        for (i = VBUFFERU - 1; i >= 0; i--)
                if (pole[i] != NULL)
                        fputc(pole[i], fl1);
        fclose(fl1);
}
int main(int argc, char** argv)
{
        int prepinac = 0;
        char* soubor;
        int pole[VBUFFERU];
        zpracuj(argc, argv, &prepinac, &soubor);
        if (prepinac == 1)
                obratSoubor(pole, soubor);
        getchar();
}

Chci docílit: Obrátit směr textového souboru (abcde => edcba) bez použití funkce fseek, s tím že soubor je větší než RAM a může být i větší než polovina disku.

 
Odpovědět
30.9.2023 22:38
Avatar
DarkCoder
Člen
Avatar
Odpovídá na Denisa Bednarova
DarkCoder:1.10.2023 17:40

Úvaha nad tím, jak řešit úlohu, je správná. Smyslem je najít konec souboru, s tím poslední pozici znaku a brát postupně znaky jeden po druhém do úplného začátku a tyto znaky zapisovat do otevřeného dočasného souboru. K tomu je třeba určit správný formát souboru na kterým provedeme zápis dat do dočasného souboru v obráceném pořadí. Soubor s novým obsahem pak lehce nastavíme na výchozí soubor elegantním trikem.

Použití fseek je efektivní a jednoduché. fseek lze použít i pro textové soubory pokud soubory otevřeme v binárním režimu. Ano, textové soubory lze otevírat v binárním režimu, aby se předešlo různým konverzím. Použij fseek a ftell i zde.

Nyní se podíváme na celý problém blíž:

Vytvoříme si nový typ pro souborový formát

typedef enum {
        FORMAT_UNKNOWN,  // Neznámý formát
        FORMAT_WINDOWS,  // Windows (CRLF)
        FORMAT_LINUX,    // Linux (LF)
        FORMAT_MAC       // macOS (CR)
} NewLineFormat;

Dále si vytvoříme funkci pro zjištění formátu souboru.

NewLineFormat detectNewLineFormat(FILE* file) {
        int prevChar = EOF;
        int currentChar;

        while ((currentChar = fgetc(file)) != EOF) {
                if (currentChar == '\n') {
                        if (prevChar == '\r') return FORMAT_WINDOWS; // CRLF
                        else return FORMAT_LINUX; // LF
                }
                else if (currentChar == '\r') {
                        int nextChar = fgetc(file);
                        if (nextChar == '\n') return FORMAT_WINDOWS; // CRLF
                        else return FORMAT_MAC; // CR
                }

                prevChar = currentChar;
        }

        return FORMAT_UNKNOWN;
}

Otevřeme dva soubory (zdrojový a dočasný) v binárním režimu a ověříme korektnost otevření.

FILE* srcFile = fopen("soubor.txt", "rb");
FILE* tmpFile = fopen("temp.txt", "wb");

if (srcFile == NULL || tmpFile == NULL) {
        fprintf(stderr, "Chyba pri otevirani souboru\n");
        exit(1);
}

Zjistíme formát zdrojového souboru a vybereme funkci podle zjištěného formátu.

NewLineFormat format = detectNewLineFormat(srcFile);

switch (format) {
case FORMAT_WINDOWS:
        reverse_copy_windows(srcFile, tmpFile);
        break;
case FORMAT_LINUX:
        // ...
        break;
case FORMAT_MAC:
        // ...
        break;
case FORMAT_UNKNOWN:
        // ...
        break;
}

Pro daný formát spustíme funkci, která zjistí velikost souboru, nastaví pozici posledního znaku, provede kopírování souboru do dočasného v obráceném pořadí.

void reverse_copy_windows(FILE* srcFile, FILE* tmpFile) {
        int c;

        long fileSize = get_file_size(srcFile);

        for (long pos = fileSize - 1; pos >= 0; pos--) {
                fseek(srcFile, pos, SEEK_SET);
                c = fgetc(srcFile);

                if (c == '\r') {
                        fputc('\r', tmpFile);
                        fputc('\n', tmpFile);
                }
                else if (c != '\n') {
                        fputc(c, tmpFile);
                }
        }
}

long get_file_size(FILE* fp) {
        fseek(fp, 0, SEEK_END);
        return ftell(fp);
}

Poslední krok je trik, kde vymažeme zdrojový soubor, nový uzavřeme a přejmenujeme na původní. Tím máme soubor jehož obsah je v obráceném pořadí původního zdrojového souboru.

fclose(srcFile);
(void)remove("soubor.txt");

fclose(tmpFile);
(void)rename("temp.txt", "soubor.txt");

Celé to pak může vypadat zhruba následovně:

#include <stdio.h>
#include <stdlib.h>

typedef enum {
        FORMAT_UNKNOWN,  // Neznámý formát
        FORMAT_WINDOWS,  // Windows (CRLF)
        FORMAT_LINUX,    // Linux (LF)
        FORMAT_MAC       // macOS (CR)
} NewLineFormat;

NewLineFormat detectNewLineFormat(FILE* file);
void reverse_copy_windows(FILE* srcFile, FILE* tmpFile);
long get_file_size(FILE* fp);


int main(void) {
        FILE* srcFile = fopen("soubor.txt", "rb");
        FILE* tmpFile = fopen("temp.txt", "wb");

        if (srcFile == NULL || tmpFile == NULL) {
                fprintf(stderr, "Chyba pri otevirani souboru\n");
                exit(1);
        }

        NewLineFormat format = detectNewLineFormat(srcFile);

        switch (format) {
        case FORMAT_WINDOWS:
                reverse_copy_windows(srcFile, tmpFile);
                break;
        case FORMAT_LINUX:
                // ...
                break;
        case FORMAT_MAC:
                // ...
                break;
        case FORMAT_UNKNOWN:
                // ...
                break;
        }

        fclose(srcFile);
        (void)remove("soubor.txt");

        fclose(tmpFile);
        (void)rename("temp.txt", "soubor.txt");

        return 0;
}


NewLineFormat detectNewLineFormat(FILE* file) {
        int prevChar = EOF;
        int currentChar;

        while ((currentChar = fgetc(file)) != EOF) {
                if (currentChar == '\n') {
                        if (prevChar == '\r') return FORMAT_WINDOWS; // CRLF
                        else return FORMAT_LINUX; // LF
                }
                else if (currentChar == '\r') {
                        int nextChar = fgetc(file);
                        if (nextChar == '\n') return FORMAT_WINDOWS; // CRLF
                        else return FORMAT_MAC; // CR
                }

                prevChar = currentChar;
        }

        return FORMAT_UNKNOWN;
}


void reverse_copy_windows(FILE* srcFile, FILE* tmpFile) {
        int c;

        long fileSize = get_file_size(srcFile);

        for (long pos = fileSize - 1; pos >= 0; pos--) {
                fseek(srcFile, pos, SEEK_SET);
                c = fgetc(srcFile);

                if (c == '\r') {
                        fputc('\r', tmpFile);
                        fputc('\n', tmpFile);
                }
                else if (c != '\n') {
                        fputc(c, tmpFile);
                }
        }
}

long get_file_size(FILE* fp) {
        fseek(fp, 0, SEEK_END);
        return ftell(fp);
}

Program v této podobě pracuje pouze se soubory s formátem Windows CRLF. Nicméně analogicky lze doplnit funkce pro formáty MacOS CR a Unix LF.

Doufám, že Ti tato stručná prezentace alespoň trochu pomohla. Pokud Ti cokoli z toho nebude jasné, ptej se..

Nahoru Odpovědět
1.10.2023 17:40
"I ta nejlepší poučka postrádá na významu, není-li patřičně předána." - DarkCoder
Avatar
Denisa Bednarova:2.10.2023 9:25

Nakonec jsem zkusila zjistit velikost souboru bez použití fseek, tím jsem se vyhnula nepřesnostem na konci souboru kvůli speciálním znakům. Pro posun uvnitř souboru asi není nic jiného než fseek, ale máme povoleno posouvat se o jedno dopředu/dozadu, což takhle splňuje.
Vyzkoušeno na anglickém překladu bible, který máme jako příklad velkého souboru. Nejspíše existuje efektivnější řešení, přeci jen projdu původní textový soubor poprve pro zjištění velikosti, podruhé jej kopíruju do docasneho a potreti z docasneho do textoveho. Zatim jsem lepší algoritmus nevymyslela.
Moc děkuju za rady, nakonec jsem vybrala jednodušší řešení.

void obratSoubor(char* soubor)
{
        int i = 0, c, index = 0;
        FILE* fl1, * fl2;
        fl1 = fopen(soubor, "r");
        //fl2 = fopen("docasny.txt", "w");
        fl2 = tmpfile();
        while ((c = getc(fl1)) != EOF)//zjisti index posledniho prku
                index++;
        fclose(fl1);
        fl1 = fopen(soubor, "rb");
        for (i = index-1; i >= 0; i--)
        {
                fseek(fl1, i, SEEK_SET);//posouvám fseekem pouze o 1
                c = getc(fl1);
                putc(c, fl2);
        }
        fclose(fl1);
        rewind(fl2);
        fl1 = fopen(soubor, "wb");
        while ((c = getc(fl2)) != EOF)
                putc(c, fl1);
        fclose(fl1);
        fclose(fl2);
}
 
Nahoru Odpovědět
2.10.2023 9:25
Avatar
DarkCoder
Člen
Avatar
Odpovídá na Denisa Bednarova
DarkCoder:2.10.2023 12:12

Toto nebude korektně fungovat. Chyba není v tom že určování velikosti souboru je pomalé. Pokud otevíráš soubor v textovém režimu a spočítáš znaky od začátku souboru pomocí cyklu ve kterém testuješ znaky na neEOF hodnotu, pak hodnota proměnné index bude odlišná od systémové velikosti souboru. Nelze pak znovu otevřít soubor v binárním módu a použít index pro nastavení koncové pozice. Protože s největší pravděpodobností se trefíš před nebo za skutečný konec souboru a nakopíruješ pouze část souboru. Dále proměnná index typu int není vhodná pro práci s velikostmi souborů velkých hodnot. Jednoduše proto, že rozsah tohoto typu nestačí. Používá se size_t. Dále úryvek kódu postrádá identifikaci formátu souboru a nebude to korektně fungovat na různých platformách. Použití maker putc a getc je naproti tomu vhodné. Pokud nechceš použít fseek a ftell pro určení velikosti souboru, můžeš použít funkci stat(), její použití vyžaduje vložení hlavičkového souboru sys/stat.h. Nezapomínej na ověřování otevření souborů.

Nahoru Odpovědět
2.10.2023 12:12
"I ta nejlepší poučka postrádá na významu, není-li patřičně předána." - DarkCoder
Avatar
Caster
Člen
Avatar
Odpovídá na Denisa Bednarova
Caster:9.3.2024 1:06

Šel bych na to jinak. Funkcí bych si nejdříve zjistil velikost souboru a pak na něj alokoval RAM, celý soubor bych pak do ní načetl a pracoval s načtenými daty v paměti. Není pak žádný problém procházet soubor od konce, vybrat si potřebné řádky a ty zapsat do nového soboru. Nepíšeš, jak je ten soubor veliký.

V C++ můžeš jedním příkazem pro čtení načíst 2,5 GB dat. Dříve jsem takto na 2x načetl textový soubor 4 GB se 35 702 067 řádky a pak s ním pracoval. I kdyby byl soubor větší než je RAM počítače, já mám na notebooku 24 GB, lze soubor načítat do RAM po částech a ty pak postupně zpracovávat.

Ukázka části kódu pro alokaci RAM a čtení souboru po 2,5 GB:

const wchar_t *fname = L"C:\\Users\\Jan\\SRT_180823.dat";
__int64 delka = 0;
__int64 nacteno = 0;
__int64 cti_znaku = 0;
BYTE* ptr = NULL;
BYTE* bsrc = NULL;
const DWORD cti_blok = 2501566399;

delka = FileSize(fname); // 64 bitů

 //* Zkusíme otevřít soubor pro čtení *//
 HANDLE handle = CreateFile(fname, GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
 //if (handle == INVALID_HANDLE_VALUE)
 //      report_error("Unable to open input file!\n");


 //* Alokujeme pamět pro celý soubor *//
 static const SIZE_T giga = 1024 * 1024 * 1024;
 static const SIZE_T size = 5 * giga;
 ptr = static_cast<BYTE*>(VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_READWRITE));
 bsrc = ptr;

 //* Načteme soubor *//
 DWORD bytes_read;
 cti_znaku = cti_blok;
 do {
         BOOL e = ReadFile(handle, ptr, cti_znaku, &bytes_read, nullptr);
         nacteno += bytes_read;
         ptr += bytes_read;
         if (delka - nacteno < cti_blok) {
                 cti_znaku = delka - nacteno;
         }
 } while (nacteno < delka);
 CloseHandle(handle);
 
Nahoru Odpovědět
9.3.2024 1:06
Avatar
Odpovídá na Caster
Jiří Kocián:2. března 1:40

Také jsem toto řešil, a výsledek najdete zde: OtocSoubor64
Textové soubory jsem neřešil, takže konce řádků to taky otáčí.

Nahoru Odpovědět
2. března 1:40
When in doubt, use brute force.
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 6 zpráv z 6.