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.


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..
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);
}
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ů.
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);
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áčí.
Zobrazeno 6 zpráv z 6.