Я пишу анализатор текстовых файлов на 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;
}
Но ведь должен быть более умный способ сделать это всего за один цикл, не так ли?
Используйте пару чередующихся буферов, меняя местами указатели в каждом цикле. Тогда вы не перезапишете предыдущую строку. Или, что еще более грубо, скопируйте буфер в «предыдущий» буфер, прежде чем читать каждую строку.
(e/c с комментарием Weather Vane) Имейте линии чтения цикла. В начале цикла прочитайте строку, и если вы получите конец файла, выйдите из цикла. В конце цикла скопируйте только что прочитанную строку во вторую переменную prevline. Наконец, в середине цикла (и кроме первого прохождения цикла) процесс prevline.
Если это в Linux, вы можете перейти к концу файла, получить позицию в файле. Затем, когда вы читаете каждую строку, вы можете увеличивать счетчик символов с длиной строки. Когда счетчик достигает размера файла, вы просто читаете последнюю строку.
Обратите внимание, что ваш пример кода не работает так, как вы, кажется, предполагали, когда любая строка длиннее LINEMAXLEN - 1 символов (включая любую завершающую новую строку). Вы не можете полностью игнорировать хвосты таких строк — вам нужно прочитать и отбросить каждый хвост, чтобы перейти к следующей строке.
Кстати, когда вы вызываете fgets, третьим аргументом обычно является точный размер вашего буфера. Я заметил, что вы объявили char line[LINEMAXLEN+1];, по-видимому, чтобы оставить место для окончания \0. Но вам не нужно об этом беспокоиться — fgets учитывает это.
Вместо использования двух циклов было бы более эффективно всегда читать две строки заранее и обрабатывать строку только после того, как следующая строка будет успешно прочитана. Таким образом, последняя строка не будет обработана.
Вот пример:
#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 символы. Огромное спасибо!
FWIW: в блоке if ( p == NULL ), если первым прочитанным символом является '\n' или EOF, буфер был не слишком длинным для того, что пытается сохранить OP. Но правда это или нет, этот код разумно обрабатывает этот случай, и строка была слишком длинной для случая входного буфера.
if ( ferror( fp ) ) { fprintf( stderr, "Input error!\n" ); exit( EXIT_FAILURE ); }
имеет небольшую слабость. ferror()
возвращает true, когда ошибка ввода только что произошла или если она произошла ранее. Попробуйте протестировать !feof() вместо этого, чтобы отличить случай, когда конец файла только что произошел, но индикатор ошибки был установлен ранее, от случая, когда ошибка только что произошла.
@chux: Да, вы правы в том, что в случае вызова функции read_start_of_line_and_discard_rest при установленном флаге ошибки потока, возможно, лучше проверить !feof() вместо ferror(). С другой стороны, если функция вызывается при установленном флаге ошибки, то вполне вероятно, что эта ошибка была пропущена вызывающим кодом. В противном случае вызывающий код, вероятно, вызвал бы clearerr перед вызовом функции. В этом случае может быть желательно, чтобы функция перехватывала ошибку с соответствующим сообщением об ошибке, что и делает мой код. [...]
@chux: [...] В качестве альтернативы я мог бы вызвать clearerr в начале функции. Но я не уверен, что возиться с флагами состояния потока — хорошая идея. Вызывающий код, вероятно, должен контролировать флаги состояния потока.
Это довольно просто сделать в одном цикле, если мы используем чередующиеся буферы [как уже упоминалось другими].
В цикле ниже мы читаем строку в «текущий» буфер. Если не первая строка, мы обрабатываем предыдущую строку в «другом» буфере.
Чередуя индекс в пуле буферов из двух буферов, мы избегаем ненужного копирования.
Это вносит задержку в обработку буфера. На последней итерации последняя строка будет в текущем буфере, но не будет обработана.
#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().
Внедрение этого в обработку текста остается в качестве упражнения... ;-)
Если файл заканчивается символом, отличным от символа конца строки, то, предположительно, последняя строка состоит из всех символов, следующих за последним разделителем. (Да?) Но если файл заканчивается разделителем строки, то является ли последняя строка той, которая содержит этот разделитель, или это пустая строка, условно следующая за разделителем?