Как читать столбцы из текстового файла и сохранять в отдельные массивы в C? - PullRequest
0 голосов
/ 10 ноября 2018

В практическом упражнении для ознакомления с указателями я написал короткую программу на C, способную читать текст из файла. Я хотел бы придерживаться ANSI C.

Программа отлично справляется со своей задачей, однако я хочу продолжить чтение столбцов из текстового файла и сохранить их в отдельных массивах. Аналогичные вопросы задавались с ответами, использующими strtok, или fgets или sscanf, но когда мне следует использовать один вместо другого?

Вот мой комментарий:

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

char *read_file(char *FILE_INPUT);     /*function to read file*/

int main(int argc, char **argv) {
    char *string; // Pointer to a char 

    string = read_file("file.txt");
    if (string) {
        // Writes the string pointed to by string to the stream pointed to by stdout, and appends a new-line character to the output.
        puts(string);
        // Causes space pointed to by string to be deallocated
        free(string);
    }
    return 0;
}

//Returns a pointer to a char,
char *read_file(char *FILE_INPUT) {
    char *buffer = NULL;
    int string_size, read_size;
    FILE *input_stream = fopen(FILE_INPUT, "r");

    //Check if file exists
    if (input_stream == NULL) {
        perror (FILE_INPUT);
    }
    else if (input_stream) {
        // Seek the last byte of the file. Offset is 0 for a text file.
        fseek(input_stream, 0, SEEK_END);
        // Finds out the position of file pointer in the file with respect to starting of the file
        // We get an idea of string_size since ftell returns the last value of the file pos
        string_size = ftell(input_stream);
        // sets the file position indicator for the stream to the start of the file
        rewind(input_stream);

        // Allocate a string that can hold it all
        // malloc returns a pointer to a char, +1 to hold the NULL character
        // (char*) is the cast return type, this is extra, used for humans
        buffer = (char*)malloc(sizeof(char) * (string_size + 1));

        // Read it all in one operation, returns the number of elements successfully read,
        // Reads into buffer, up to string_size whose size is specified by sizeof(char), from the input_stream !
        read_size = fgets(buffer, sizeof(char), string_size, input_stream);

        // fread doesn't set it so put a \0 in the last position
        // and buffer is now officially a string
        buffer[string_size] = '\0';

        //string_size determined by ftell should be equal to read_size from fread
        if (string_size != read_size) {
            // Something went wrong, throw away the memory and set
            // the buffer to NULL
            free(buffer);
            buffer = NULL;
        }

        // Always remember to close the file.
        fclose(input_stream);
    }

    return buffer;
}

Как я могу прочитать все столбцы из текстового файла этого формата в массив? Количество столбцов фиксировано, но количество строк может варьироваться.

C 08902019 1020 50 Test1
A 08902666 1040 30 Test2
B 08902768 1060 80 Test3
.
B 08902768 1060 800 Test3000
.
.

В ходе дальнейших исследований я обнаружил, что fread используется для того, чтобы программа могла считывать и записывать большие блоки данных за один шаг, поэтому чтение столбцов по отдельности может не соответствовать тому, что fread предназначено для этого. Таким образом, реализация моей программы для такого рода задач неверна.

Должен ли я использовать getc, strtok, sscanf или getline для чтения такого текстового файла? Я стараюсь придерживаться хороших принципов программирования и динамически распределять память.


EDIT:

Под правильным я подразумеваю (но не ограничиваюсь) использование хороших методов программирования на c и динамического распределения памяти.

Моей первой мыслью было заменить fread на fgets. Обновление, я получаю куда-то благодаря вашей помощи.

    // Allocate a string that can hold it all
    // malloc returns a pointer to a char, +1 to hold the NULL    character
    // (char*) is the cast return type, this is extra, used for humans
    buffer = (char*)malloc(sizeof(char) * (string_size + 1));

    while (fgets(buffer, sizeof(char) * (string_size + 1), input_stream), input_stream)) {
        printf("%s", buffer);     
    }

для указанных выше текстовых файлов:

C 08902019 1020 50 Test1

A 08902666 1040 30 Test2

B 08902768 1060 80 Test3

B 08902768 1060 800 Test3000

Мне также удалось удалить символ новой строки из ввода fgets () с помощью:

strtok(buffer, "\n"); 

Подобные примеры здесь , здесь и здесь

Как перейти к сохранению столбцов в отдельные массивы?

Ответы [ 4 ]

0 голосов
/ 10 ноября 2018

«Передовой опыт» несколько субъективен, но цель «всегда подтверждено, логична и читабельна» всегда должна быть целью.

Для чтения фиксированного количества полей (в вашем случае выбрав cols 1, 2, 5 в качестве строковых значений неизвестной длины) и cols 3, 4 в качестве простых int значений), вы можете прочитать неизвестное количество строк из файла просто выделение хранилища для некоторого разумно ожидаемого количества строк данных, отслеживание количества заполненных строк, а затем перераспределение хранилища по мере необходимости, когда вы достигнете предела выделенного хранилища.

Эффективным способом обработки перераспределения является перераспределение с помощью некоторого разумного количества дополнительных блоков памяти, когда требуется перераспределение (вместо выполнения вызовов realloc для каждой дополнительной строки данных). Вы можете либо добавить фиксированное количество новых блоков, умножить то, что у вас есть, на 3/2 или 2, либо на какую-то другую разумную схему, соответствующую вашим потребностям. Обычно я просто удваиваю память каждый раз, когда достигается предел выделения.

Поскольку у вас есть фиксированное количество полей неизвестного размера, вы можете упростить задачу, просто разделив пять полей с помощью sscanf и подтвердив, что 5 преобразований имели место, проверив возврат sscanf. Если бы вы читали неизвестное количество полей, то вы бы просто использовали ту же схему перераспределения, чтобы обрабатывать раскрытие по столбцам, рассмотренное выше, для чтения неизвестного количества строк.

(в этом случае не требуется, чтобы в каждой строке было одинаковое количество полей, но вы можете принудительно выполнить проверку, установив переменную, содержащую количество полей, прочитанных в первой строке, и затем проверив, что все последующие строки имеют тот же номер ...)

Как обсуждалось в комментариях, чтение строки данных с помощью строчно-ориентированной функции ввода, такой как fgets или POSIX getline, и последующий анализ данных с помощью токенизации с strtok или в этом случае с фиксированным числом полей простой анализ с sscanf - это надежный подход. Это обеспечивает возможность независимой проверки (1) чтения данных из файла; и (2) разбора данных на необходимые значения. (хотя и менее гибкий, для некоторых наборов данных вы можете сделать это за один шаг с fscanf, но это также создает проблемы scanf для пользовательского ввода того, что остается непрочитанным во входном буфере, в зависимости от преобразования -спецификаторы б / у ...)

Самый простой способ приблизиться к хранилищу ваших 5 полей - объявить простое struct. Поскольку число символов для каждого из символьных полей неизвестно, члены структуры для каждого из этих полей будут указателем символов, а остальные поля int, например,

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

#define ARRSZ   2   /* use 8 or more, set to 2 here to force realloc */
#define MAXC 1024

typedef struct {
    char *col1, *col2, *col5;
    int col3, col4;
} mydata_t;

Теперь вы можете начать выделение для обработки неизвестного количества из них, выделив некоторую разумно ожидаемую сумму (я бы обычно использовал 8 или 16 со схемой удвоения, поскольку она будет расти достаточно быстро), но мы имеем выберите 2 здесь с #define ARRSZ 2, чтобы убедиться, что мы принудительно перераспределим при обработке вашего файла данных из 3 строк Также обратите внимание, что мы устанавливаем максимальное количество символов в строке из #define MAXC 1024 для ваших данных ( не экономьте на размере буфера )

Для начала все, что нам нужно сделать, это объявить буфер для хранения каждой строки и несколько переменных для отслеживания текущего количества распределенных структур, счетчик строк (для вывода точных сообщений об ошибках) и счетчик для числа строк данных, которые мы заполнили. Затем, когда (rows_filled == allocated_array_size) вы realloc, например,

int main (int argc, char **argv) {

    char buf[MAXC];
    size_t arrsz = ARRSZ, line = 0, row = 0;
    mydata_t *data = NULL;
    /* use filename provided as 1st argument (stdin by default) */
    FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;

    if (!fp) {  /* validate file open for reading */
        perror ("file open failed");
        return 1;
    }

    /* allocate an 'arrsz' initial number of struct */
    if (!(data = malloc (arrsz * sizeof *data))) {
        perror ("malloc-data");
        return 1;
    }

    while (fgets (buf, MAXC, fp)) {         /* read each line from file */
        char c1[MAXC], c2[MAXC], c5[MAXC];  /* temp strings for c1,2,5 */
        int c3, c4;                         /* temp ints for c3,4 */
        size_t len = strlen (buf);          /* length for validation */

        line++;     /* increment line count */

        /* validate line fit in buffer */
        if (len && buf[len-1] != '\n' && len == MAXC - 1) {
            fprintf (stderr, "error: line %zu exceeds MAXC chars.\n", line);
            return 1;
        }

        if (row == arrsz) { /* check if all pointers used */
            void *tmp = realloc (data, arrsz * 2 * sizeof *data);
            if (!tmp) {     /* validate realloc succeeded */
                perror ("realloc-data");
                break;      /* break, don't exit, data still valid */
            }
            data = tmp;     /* assign realloc'ed block to data */
            arrsz *= 2;     /* update arrsz to reflect new allocation */
        }

( примечание: при вызове realloc, вы никогда не перераспределяете сам указатель , например, data = realloc (data, new_size); Если realloc не удается (и это происходит), он возвращает NULL который перезапишет ваш оригинальный указатель, что приведет к утечке памяти. Всегда realloc с временным указателем, проверьте, затем назначьте новый блок памяти вашему исходному указателю)

Остается только разделить строку на наши значения, обработать любые ошибки в формате строки, добавить значения нашего поля в наш массив struct, увеличить количество строк / строк и повторять до тех пор, пока у нас не кончатся данные для чтения, например

        /* parse buf into fields, handle error on invalid format of line */
        if (sscanf (buf, "%1023s %1023s %d %d %1023s", 
                    c1, c2, &c3, &c4, c5) != 5) {
            fprintf (stderr, "error: invalid format line %zu\n", line);
            continue;   /* get next line */
        }

        /* allocate copy strings, assign allocated blocks to pointers */
        if (!(data[row].col1 = mystrdup (c1))) { /* validate copy of c1 */
            fprintf (stderr, "error: malloc-c1 line %zu\n", line);
            break;      /* same reason to break not exit */
        }
        if (!(data[row].col2 = mystrdup (c2))) { /* validate copy of c2 */
            fprintf (stderr, "error: malloc-c1 line %zu\n", line);
            break;      /* same reason to break not exit */
        }
        data[row].col3 = c3;    /* assign integer values */
        data[row].col4 = c4;
        if (!(data[row].col5 = mystrdup (c5))) { /* validate copy of c5 */
            fprintf (stderr, "error: malloc-c1 line %zu\n", line);
            break;      /* same reason to break not exit */
        }
        row++;      /* increment number of row pointers used */
    }
    if (fp != stdin)    /* close file if not stdin */
        fclose (fp);

    puts ("values stored in struct\n");
    for (size_t i = 0; i < row; i++)
        printf ("%-4s %-10s %4d %4d %s\n", data[i].col1, data[i].col2, 
                data[i].col3, data[i].col4, data[i].col5);

    freemydata (data, row);

    return 0;
}

И мы закончили (за исключением использования памяти / проверки ошибок)

Обратите внимание, что есть две вспомогательные функции, которые нужно выделить для каждой строки и скопировать каждую строку в выделенный ей блок памяти, а также присвоить начальный адрес этого блока нашему указателю в нашей структуре. mystrdup() Вы можете использовать strdup(), если он у вас есть, я просто включил функцию, чтобы показать вам, как вручную обрабатывать malloc и копировать. Примечание: как выполняется копирование с memcpy вместо strcpy - Почему? Вы уже отсканировали вперед в строке, чтобы найти '\0', когда нашли длину с strlen - не нужно, чтобы strcpy повторил этот процесс снова - просто используйте memcpy.

/* simple implementation of strdup - in the event you don't have it */
char *mystrdup (const char *s)
{
    if (!s)     /* validate s not NULL */
        return NULL;

    size_t len = strlen (s);            /* get length */
    char *sdup = malloc (len + 1);      /* allocate length + 1 */

    if (!sdup)          /* validate */
        return NULL;

    return memcpy (sdup, s, len + 1);   /* pointer to copied string */ 
}

Последняя вспомогательная функция - freemydata(), которая просто вызывает free() в каждом выделенном блоке, чтобы гарантировать, что вы освободите всю выделенную память. Это также держит ваш код в порядке. (вы можете сделать то же самое для блока realloc, чтобы переместить его в свою собственную функцию)

/* simple function to free all data when done */
void freemydata (mydata_t *data, size_t n)
{
    for (size_t i = 0; i < n; i++) {    /* free allocated strings */
        free (data[i].col1);
        free (data[i].col2);
        free (data[i].col5);
    }
    free (data);    /* free structs */
}

Соединение всех частей даст вам:

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

#define ARRSZ   2   /* use 8 or more, set to 2 here to force realloc */
#define MAXC 1024

typedef struct {
    char *col1, *col2, *col5;
    int col3, col4;
} mydata_t;

/* simple implementation of strdup - in the event you don't have it */
char *mystrdup (const char *s)
{
    if (!s)     /* validate s not NULL */
        return NULL;

    size_t len = strlen (s);            /* get length */
    char *sdup = malloc (len + 1);      /* allocate length + 1 */

    if (!sdup)          /* validate */
        return NULL;

    return memcpy (sdup, s, len + 1);   /* pointer to copied string */ 
}

/* simple function to free all data when done */
void freemydata (mydata_t *data, size_t n)
{
    for (size_t i = 0; i < n; i++) {    /* free allocated strings */
        free (data[i].col1);
        free (data[i].col2);
        free (data[i].col5);
    }
    free (data);    /* free structs */
}

int main (int argc, char **argv) {

    char buf[MAXC];
    size_t arrsz = ARRSZ, line = 0, row = 0;
    mydata_t *data = NULL;
    /* use filename provided as 1st argument (stdin by default) */
    FILE *fp = argc > 1 ? fopen (argv[1], "r") : stdin;

    if (!fp) {  /* validate file open for reading */
        perror ("file open failed");
        return 1;
    }

    /* allocate an 'arrsz' initial number of struct */
    if (!(data = malloc (arrsz * sizeof *data))) {
        perror ("malloc-data");
        return 1;
    }

    while (fgets (buf, MAXC, fp)) {         /* read each line from file */
        char c1[MAXC], c2[MAXC], c5[MAXC];  /* temp strings for c1,2,5 */
        int c3, c4;                         /* temp ints for c3,4 */
        size_t len = strlen (buf);          /* length for validation */

        line++;     /* increment line count */

        /* validate line fit in buffer */
        if (len && buf[len-1] != '\n' && len == MAXC - 1) {
            fprintf (stderr, "error: line %zu exceeds MAXC chars.\n", line);
            return 1;
        }

        if (row == arrsz) { /* check if all pointers used */
            void *tmp = realloc (data, arrsz * 2 * sizeof *data);
            if (!tmp) {     /* validate realloc succeeded */
                perror ("realloc-data");
                break;      /* break, don't exit, data still valid */
            }
            data = tmp;     /* assign realloc'ed block to data */
            arrsz *= 2;     /* update arrsz to reflect new allocation */
        }

        /* parse buf into fields, handle error on invalid format of line */
        if (sscanf (buf, "%1023s %1023s %d %d %1023s", 
                    c1, c2, &c3, &c4, c5) != 5) {
            fprintf (stderr, "error: invalid format line %zu\n", line);
            continue;   /* get next line */
        }

        /* allocate copy strings, assign allocated blocks to pointers */
        if (!(data[row].col1 = mystrdup (c1))) { /* validate copy of c1 */
            fprintf (stderr, "error: malloc-c1 line %zu\n", line);
            break;      /* same reason to break not exit */
        }
        if (!(data[row].col2 = mystrdup (c2))) { /* validate copy of c2 */
            fprintf (stderr, "error: malloc-c1 line %zu\n", line);
            break;      /* same reason to break not exit */
        }
        data[row].col3 = c3;    /* assign integer values */
        data[row].col4 = c4;
        if (!(data[row].col5 = mystrdup (c5))) { /* validate copy of c5 */
            fprintf (stderr, "error: malloc-c1 line %zu\n", line);
            break;      /* same reason to break not exit */
        }
        row++;      /* increment number of row pointers used */
    }
    if (fp != stdin)    /* close file if not stdin */
        fclose (fp);

    puts ("values stored in struct\n");
    for (size_t i = 0; i < row; i++)
        printf ("%-4s %-10s %4d %4d %s\n", data[i].col1, data[i].col2, 
                data[i].col3, data[i].col4, data[i].col5);

    freemydata (data, row);

    return 0;
}

Сейчас тестируем.

Пример входного файла

$ cat dat/fivefields.txt
C 08902019 1020 50 Test1
A 08902666 1040 30 Test2
B 08902768 1060 80 Test3

Пример использования / Вывод

$ ./bin/fgets_fields <dat/fivefields.txt
values stored in struct

C    08902019   1020   50 Test1
A    08902666   1040   30 Test2
B    08902768   1060   80 Test3

Использование памяти / проверка ошибок

В любом написанном вами коде, который динамически распределяет память, у вас есть 2 обязанностей относительно любого выделенного блока памяти: (1) всегда сохраняет указатель на начальный адрес для блока памяти, таким образом, (2) он может быть освобожден , когда он больше не нужен.

Крайне важно, чтобы вы использовали программу проверки ошибок памяти, чтобы гарантировать, что вы не пытаетесь получить доступ к памяти или писать за пределами / за пределами выделенного блока, пытаться прочитать или основать условный переход на неинициализированном значении и, наконец, , чтобы подтвердить, что вы освобождаете всю выделенную память.

Для Linux valgrind - нормальный выбор. Для каждой платформы есть похожие проверки памяти. Все они просты в использовании, просто запустите вашу программу через него.

$ valgrind ./bin/fgets_fields <dat/fivefields.txt
==1721== Memcheck, a memory error detector
==1721== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==1721== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==1721== Command: ./bin/fgets_fields
==1721==
values stored in struct

C    08902019   1020   50 Test1
A    08902666   1040   30 Test2
B    08902768   1060   80 Test3
==1721==
==1721== HEAP SUMMARY:
==1721==     in use at exit: 0 bytes in 0 blocks
==1721==   total heap usage: 11 allocs, 11 frees, 243 bytes allocated
==1721==
==1721== All heap blocks were freed -- no leaks are possible
==1721==
==1721== For counts of detected and suppressed errors, rerun with: -v
==1721== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Всегда подтверждайте, что вы освободили всю выделенную память и что ошибок памяти нет.

Посмотрите вещи и дайте мне знать, если у вас есть дополнительные вопросы.

0 голосов
/ 10 ноября 2018

В зависимости от данных и смелости, вы можете использовать scanf или парсер, созданный с помощью yacc / lex.

0 голосов
/ 10 ноября 2018

Я хочу продолжить чтение только определенных столбцов этого текстового файла.

Вы можете сделать это с помощью любой функции ввода: getc, fgets, sscanf, getline ... но сначала вы должны точно определить, что вы подразумеваете под определенными столбцами .

  • могут быть определены как разделенные определенным символом, таким как ,, ; или TAB, в этом случае strtok() определенно не является правильным выбором, поскольку он обрабатывает все последовательности разделяющих символов как один разделитель: следовательно, a,,b будет рассматриваться как имеющий только 2 столбца.
  • если они вместо этого разделены пробелами, любая последовательность пробелов или табуляций, strtok, strpbrk или strspn / strcspn может пригодиться.

В любом случае вы можете читать файл построчно с помощью fgets, но у вас могут быть проблемы с очень длинными строками. getline - это решение, но оно может быть доступно не во всех системах.

0 голосов
/ 10 ноября 2018

Если вы знаете, что такое разделитель столбцов и сколько у вас столбцов, вы используете getline с разделителем столбцов, а затем с разделителем строк.

Вот getline:

http://man7.org/linux/man-pages/man3/getline.3.html

Это очень хорошо, потому что он выделяет место для вас, не нужно знать, сколько байтов составляет ваш столбец или строка.

Или вы просто используете getline, как в примере кода в ссылке, чтобы прочитать всю строку, а затем "анализировать" и извлекать столбцы по своему желанию ....

Если вы вставите именно так, как вы хотите запустить программу с вводом, который вы показываете, я могу попробовать написать быструю программу на Си для хорошего ответа. Теперь это просто ответ в стиле комментария, содержащий слишком много слов для комментария: - (

Или это как-то нельзя использовать библиотеку?

Хотя в ожидании лучшего вопроса отмечу, что вы можете использовать awk для чтения столбцов из текстового файла, но, вероятно, это не то, что вам нужно? Потому что ты на самом деле пытаешься сделать?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...