Определить, является ли строка последней строкой файла, используя только один цикл?

Я пишу анализатор текстовых файлов на C.

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

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

В настоящее время единственный способ, которым я могу это сделать, - это запустить два цикла, что-то вроде следующего:

char line[ LINEMAXLEN+1u ];
unsigned int nlines;
unsigned int i;

nlines = 0u;
while ( fgets (line, LINEMAXLEN, file) != NULL )
    nlines += 1u;

i = 0u;
while ( fgets (line, LINEMAXLEN, file) != NULL ) {
    if ( i >= nlines - 1u )
        break;
    //...parse the line
    i += 1u;
}

Но ведь должен быть более умный способ сделать это всего за один цикл, не так ли?

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

John Bollinger 15.02.2023 18:46

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

Weather Vane 15.02.2023 18:49

(e/c с комментарием Weather Vane) Имейте линии чтения цикла. В начале цикла прочитайте строку, и если вы получите конец файла, выйдите из цикла. В конце цикла скопируйте только что прочитанную строку во вторую переменную prevline. Наконец, в середине цикла (и кроме первого прохождения цикла) процесс prevline.

Steve Summit 15.02.2023 18:54

Если это в Linux, вы можете перейти к концу файла, получить позицию в файле. Затем, когда вы читаете каждую строку, вы можете увеличивать счетчик символов с длиной строки. Когда счетчик достигает размера файла, вы просто читаете последнюю строку.

Barmar 15.02.2023 18:55

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

John Bollinger 15.02.2023 18:56

Кстати, когда вы вызываете fgets, третьим аргументом обычно является точный размер вашего буфера. Я заметил, что вы объявили char line[LINEMAXLEN+1];, по-видимому, чтобы оставить место для окончания \0. Но вам не нужно об этом беспокоиться — fgets учитывает это.

Steve Summit 15.02.2023 18:56
Инструменты для веб-скрапинга с открытым исходным кодом: Python Developer Toolkit
Инструменты для веб-скрапинга с открытым исходным кодом: Python Developer Toolkit
Веб-скрейпинг, как мы все знаем, это дисциплина, которая развивается с течением времени. Появляются все более сложные средства борьбы с ботами, а...
Калькулятор CGPA 12 для семестра
Калькулятор CGPA 12 для семестра
Чтобы запустить этот код и рассчитать CGPA, необходимо сохранить код как HTML-файл, а затем открыть его в веб-браузере. Для этого выполните следующие...
ONLBest Online HTML CSS JAVASCRIPT Training In INDIA 2023
ONLBest Online HTML CSS JAVASCRIPT Training In INDIA 2023
О тренинге HTML JavaScript :HTML (язык гипертекстовой разметки) и CSS (каскадные таблицы стилей) - две основные технологии для создания веб-страниц....
Как собрать/развернуть часть вашего приложения Angular
Как собрать/развернуть часть вашего приложения Angular
Вам когда-нибудь требовалось собрать/развернуть только часть вашего приложения Angular или, возможно, скрыть некоторые маршруты в определенных средах?
Запуск PHP на IIS без использования программы установки веб-платформы
Запуск PHP на IIS без использования программы установки веб-платформы
Установщик веб-платформы, предлагаемый компанией Microsoft, перестанет работать 31 декабря 2022 года. Его закрытие привело к тому, что мы не можем...
Оптимизация React Context шаг за шагом в 4 примерах
Оптимизация React Context шаг за шагом в 4 примерах
При использовании компонентов React в сочетании с Context вы можете оптимизировать рендеринг, обернув ваш компонент React в React.memo сразу после...
1
6
79
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

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

Вот пример:

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

#define LINEMAXLEN 30

//forward function declarations
void process_line( const char *line );
bool read_start_of_line_and_discard_rest( char buffer[], int buffer_size, FILE *fp );

int main( void )
{
    FILE *fp;
    char lines[2][LINEMAXLEN];

    //This index specifies which index in the array "lines"
    //represents the newest line. The other index is the
    //index of the previous line.
    int newest_index = 0;

    //attempt to open file
    fp = fopen( "input.txt", "r" );
    if ( fp == NULL )
    {
        fprintf( stderr, "Error opening file!\n" );
        exit( EXIT_FAILURE );
    }

    //read first line
    if ( !read_start_of_line_and_discard_rest( lines[newest_index], LINEMAXLEN, fp ) )
    {
        fprintf( stderr, "Error reading first line!\n" );
        exit( EXIT_FAILURE );
    }

    //process one line per loop iteration
    for (;;)
    {
        //swap the index, so that the newest line is now the
        //previous line
        newest_index = !newest_index;

        //read the new line
        if ( !read_start_of_line_and_discard_rest( lines[newest_index], LINEMAXLEN, fp ) )
        {
            //we have reached end-of-file, so we don't process the 
            //previous line, because that line is the last line
            break;
        }

        //since reading in a new line succeeded, we can be sure that
        //the previous line is not the last line, so we can process
        //the previous line

        //process the previous line
        process_line( lines[!newest_index] );
    }

    //cleanup
    fclose( fp );
}

//This function will process a line after it has been read
//from the input file. For now, it will only print it.
void process_line( const char *line )
{
    printf( "Processing line: %s\n", line );
}

//This function will read exactly one line of input and remove the
//newline character, if it exists. On success, it will return true.
//If this function is unable to read any further lines due to
//end-of-file, it returns false. If it fails for any other reason, it
//will not return, but will print an error message and call "exit"
//instead.
//If the line is too long to fit in the buffer, it will discard
//the rest of the line and report success.
bool read_start_of_line_and_discard_rest( char buffer[], int buffer_size, FILE *fp )
{
    char *p;

    //attempt to read one line from the stream
    if ( fgets( buffer, buffer_size, fp ) == NULL )
    {
        if ( ferror( fp ) )
        {
            fprintf( stderr, "Input error!\n" );
            exit( EXIT_FAILURE );
        }

        return false;
    }

    //determine whether line was too long for input buffer
    p = strchr( buffer, '\n' );
    if ( p == NULL )
    {
        int c;

        //discard remainder of line
        do
        {
            c = getchar();

        } while ( c != EOF && c != '\n' );
    }
    else
    {
        //remove newline character by overwriting it with a null
        //character
        *p = '\0';
    }

    return true;
}

Для ввода

This is line1.
This is line2 which has an additional length longer than 30 characters.
This is line3.
This is line4.

Эта программа имеет следующий вывод:

Processing line: This is line1.
Processing line: This is line2 which has an ad
Processing line: This is line3.

Как видите, обрабатываются все строки, кроме последней, и только первые LINEMAXLEN-1 (30-1 в моем примере) символы каждой строки обрабатываются/сохраняются. Остальные символы отбрасываются.

Обрабатываются/сохраняются только символы LINEMAXLEN-1 вместо LINEMAXLEN из каждой строки, поскольку для хранения завершающего нулевого символа требуется один символ.

Это не только ответило на вопрос, но также решило еще две важные проблемы, отмеченные в других комментариях: (а) я добавлял один дополнительный байт в свой буфер без уважительной причины, (б) мой код не обрабатывал, если строки были длиннее, чем LINEMAXLEN символы. Огромное спасибо!

Oh Fiveight 15.02.2023 22:29

FWIW: в блоке if ( p == NULL ), если первым прочитанным символом является '\n' или EOF, буфер был не слишком длинным для того, что пытается сохранить OP. Но правда это или нет, этот код разумно обрабатывает этот случай, и строка была слишком длинной для случая входного буфера.

chux - Reinstate Monica 15.02.2023 23:14
if ( ferror( fp ) ) { fprintf( stderr, "Input error!\n" ); exit( EXIT_FAILURE ); } имеет небольшую слабость. ferror() возвращает true, когда ошибка ввода только что произошла или если она произошла ранее. Попробуйте протестировать !feof() вместо этого, чтобы отличить случай, когда конец файла только что произошел, но индикатор ошибки был установлен ранее, от случая, когда ошибка только что произошла.
chux - Reinstate Monica 15.02.2023 23:39

@chux: Да, вы правы в том, что в случае вызова функции read_start_of_line_and_discard_rest при установленном флаге ошибки потока, возможно, лучше проверить !feof() вместо ferror(). С другой стороны, если функция вызывается при установленном флаге ошибки, то вполне вероятно, что эта ошибка была пропущена вызывающим кодом. В противном случае вызывающий код, вероятно, вызвал бы clearerr перед вызовом функции. В этом случае может быть желательно, чтобы функция перехватывала ошибку с соответствующим сообщением об ошибке, что и делает мой код. [...]

Andreas Wenzel 17.02.2023 11:02

@chux: [...] В качестве альтернативы я мог бы вызвать clearerr в начале функции. Но я не уверен, что возиться с флагами состояния потока — хорошая идея. Вызывающий код, вероятно, должен контролировать флаги состояния потока.

Andreas Wenzel 17.02.2023 11:04

Это довольно просто сделать в одном цикле, если мы используем чередующиеся буферы [как уже упоминалось другими].

В цикле ниже мы читаем строку в «текущий» буфер. Если не первая строка, мы обрабатываем предыдущую строку в «другом» буфере.

Чередуя индекс в пуле буферов из двух буферов, мы избегаем ненужного копирования.

Это вносит задержку в обработку буфера. На последней итерации последняя строка будет в текущем буфере, но не будет обработана.

#define LINEMAXLEN      1000            // line length of buffer
#define NBUF            2               // number of buffers

char lines[NBUF][LINEMAXLEN];           // buffer pool

int previdx = -1;                       // index of bufs for _previous_ line
int curidx = 0;                         // index of bufs for _current_ line
char *buf;                              // pointer to line buffer to process

// read all lines into alternating line buffers
for (;  fgets(lines[curidx],LINEMAXLEN,stdin) != NULL;
    previdx = curidx, curidx = (curidx + 1) % NBUF) {

    // process _previous_ line  ...
    if (previdx >= 0) {
        buf = lines[previdx];
        // process line ...
    }
}

Fgets() вообще не изменит буфер, когда он достигнет EOF, поэтому просто читайте строки, пока fgets() не вернет NULL. Последняя прочитанная строка будет сохранена:

#include <stdio.h>

int main( int argc, char **argv )
{
    char line[ 1024 ];

    FILE *f = fopen( argv[ 1 ], "r" );
    if ( NULL == f )
    {
        return( 1 );
    }

    for ( ;; )
    {
        char *p = fgets( line, sizeof( line ), f );
        if ( NULL == p )
        {
            break;
        }
    }

    printf( "last line: %s\n", line );

    return( 0 ); 
}

Это зависит от требуемого поведения fgets():

Функция fgets возвращает s в случае успеха. Если обнаружен конец файла и в массив не были прочитаны символы, содержимое массива остается неизменным и возвращается нулевой указатель.

Надежный код должен проверять наличие ошибок с помощью ferror().

Внедрение этого в обработку текста остается в качестве упражнения... ;-)

Другие вопросы по теме