Executando verificação de segurança...
14

Desvendando o Código: Aprenda a Ler Linhas Aleatórias de um Arquivo em C!

Recentemente, retomei meus estudos em C e, desta vez, decidi criar um projeto prático para aprender os conceitos da linguagem à medida que surgiam. Optei por desenvolver um simples jogo de forca para ser jogado via linha de comando.

Após concluir todo o sistema, que pode ser visualizado em um repositório no meu GitHub, decidi que a palavra a ser adivinhada deveria ser escolhida aleatoriamente e sempre ser diferente a cada jogo. Inicialmente, considerei consultar uma API (ou até mesmo criar uma, se necessário) para obter uma palavra sempre que possível. No entanto, acabei optando por uma abordagem mais simples.

Então, vamos lá.

Lendo uma arquivo em C

Na linguagem C, para ler um arquivo, usamos a função fopen, que está definida em stdio.h, e que retorna um ponteiro para uma estrutura de dados mantida pelo sistema operacional, representando o arquivo aberto.

E ao final, usamos fclose para fechar corretamente o arquivo, liberar memória se necessário e permitir que outros processos possam utilizar o mesmo arquivo sem problemas.

Isso é feito da seguinte forma:

int main()
{
	FILE *file;
	file = fopen("palavras.txt", "r");

	if (file == NULL)
        {
	    fprintf(stderr, "Error when trying to read file.\n");
	    return 1;
	}

	fclose(file);
	return 0;
}

É sempre uma boa prática lidar com tratamento de erros. Por esse motivo, exibo uma mensagem de erro caso o ponteiro FILE seja igual a NULL, o que ocorre se houver algum problema na leitura ou se o arquivo não existir.

Contando o número de linhas

Para ler uma linha aleatória, precisamos primeiro determinar quantas linhas existem no arquivo para escolher um número de linha aleatório válido. Isso pode ser feito na própria função main, mas também podemos criar uma função separada para isso.

int countLines(FILE *file)
{
    int count = 0;
    int caractere;

    while ((caractere = fgetc(file)) != EOF)
    {
        if (caractere == '\n')
        {
            count++;
        }
    }

    rewind(file);
    return count;
}

Nessa função, recebemos como argumento um ponteiro para FILE e retornamos um inteiro que representa o número de linhas no arquivo.

Dentro da função, criamos duas variáveis: count, que armazenará o total de linhas, e caractere, uma variável temporária para armazenar cada caractere do texto para realizar as verificações necessárias.

Em seguida, usamos um laço de repetição while para ler cada caractere do texto usando a função fgetc e verificar se o caractere é diferente do marcador que indica o fim do arquivo, representado pela constante EOF. Se o caractere lido for o marcador de fim de arquivo, o laço é encerrado.

Dentro do laço while, verificamos se cada caractere é um caractere de quebra de linha (\n). Se for, incrementamos a variável que representa o total de linhas.

Após verificar cada caractere, o ponteiro do arquivo estará no final. Portanto, utilizamos a função rewind para voltar o ponteiro para o início do arquivo.

Finalmente, retornamos a variável count, que agora contém o total de linhas no arquivo.

Gerando um número (pseudo) aleatório

Agora que sabemos quantas linhas existem no arquivo, podemos gerar um número aleatório apropriado. No entanto, antes disso, é crucial definir uma semente de geração apropriada para garantir que o número gerado seja sempre diferente em cada inicialização do programa.

Para isso, utilizamos a função srand, definida em stdlib.h, e passamos o tempo atual do sistema usando a função time (definida em time.h) como parâmetro, utilizando NULL. Essa etapa pode ser realizada na própria função main, logo no início do programa.

...

int main()
{
+	srand(time(NULL));

	FILE *file;
	file = fopen("palavras.txt", "r");

	if (file == NULL)
        {
            fprintf(stderr, "Error when trying to read file.\n");
	    return 1;
	}
	
	fclose(file);
	return 0;
}

Vamos criar as variáveis que representarão a linha escolhida e a linha aleatória. Para isso, usaremos a função rand, que gera um número aleatório entre 0 e RAND_MAX (uma constante definida pelo sistema que representa o maior número possível que pode ser gerado aleatoriamente). No entanto, para garantir que o número fique entre 1 e o número total de linhas, precisaremos fazer alguns ajustes. Veja como fazer isso:

...

int main()
{
	srand(time(NULL));

	FILE *file;
	file = fopen("palavras.txt", "r");

	if (file == NULL)
        {
	    fprintf(stderr, "Error when trying to read file.\n");
	    return 1;
	}

+	int lines = countLines(file);
+	int randomLine = rand() % lines + 1;
	
	fclose(file);
	return 0;
}

Basicamente, restringimos o número aleatório entre 0 e o número total de linhas usando o operador de módulo da divisão e, em seguida, adicionamos 1 ao resultado para que o valor comece a partir da primeira linha.

Lendo a linha aleatória

Agora que temos todas as variáveis definidas, podemos percorrer o arquivo novamente até alcançar a linha definida e ler o seu valor.

+#define MAX_LINE_LEN 100
...

int main()
{
	srand(time(NULL));

	FILE *file;
	file = fopen("palavras.txt", "r");

	if (file == NULL)
        {
	    fprintf(stderr, "Error when trying to read file.\n");
	    return 1;
	}

	int lines = countLines(file);
	int randomLine = rand() % lines + 1;

+	char line[MAX_LINE_LEN];
+	int count = 0;

+	while (count < randomLine && fgets(line, MAX_LINE_LEN, file) != NULL)
+       {
+	    count++;
+	}

+	printf("Linha aleatória: %s\n", line);
	
	fclose(file);
	return 0;
}

Neste código, criamos uma variável do tipo array de caracteres para armazenar o valor da linha. Esperamos que cada linha tenha até 100 caracteres. Caso contrário, você pode ajustar esse valor na definição da constante MAX_LINE_LEN.

Em seguida, criamos uma variável inteira para armazenar a contagem de linhas. No loop while, verificamos se a contagem é menor que a linha escolhida. Se for, continuamos lendo as linhas. Quando chegamos à linha escolhida, lemos o valor dela.

No mesmo loop while, utilizamos a comparação fgets(line, MAX_LINE_LEN, file) != NULL para ler a linha inteira, adicionar o conteúdo na variável line, e verificar se o conteúdo é diferente de NULL. Se for NULL, chegamos ao final do arquivo, e o valor da variável line será o conteúdo da última linha.

Ao final, esse será o nosso código:

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

#define MAX_LINE_LEN 100

int countLines(FILE *file)
{
    int count = 0;
    int caractere;

    while ((caractere = fgetc(file)) != EOF)
    {
        if (caractere == '\n')
        {
            count++;
        }
    }

    rewind(file);
    return count;
}

int main()
{
	srand(time(NULL));

	FILE *file;
	file = fopen("palavras.txt", "r");

	if (file == NULL)
        {
	    fprintf(stderr, "Error when trying to read file.\n");
	    return 1;
	}

	int lines = countLines(file);
	int randomLine = rand() % lines + 1;

	char line[MAX_LINE_LEN];
	int count = 0;

	while (count < randomLine && fgets(line, MAX_LINE_LEN, file) != NULL)
        {
	    count++;
	}

	printf("Linha aleatória: %s\n", line);
	
	fclose(file);
	return 0;
}

Agora basta criar um arquivo palavras.txt, com pelo menos uma palavra, no mesmo diretório que o seu arquivo C, compilar e rodar o seu programa.

Caso você queira ver o meu jogo da forca, basta acessar esse repositório.


Este foi um pequeno exemplo de um programa escrito na linguagem C. O objetivo é apresentar um pouco mais desta linguagem e mostrar como alguns problemas podem ser resolvidos de forma fácil se entendermos a lógica por trás.

Até a próxima e obrigado pelos peixes!

6

Só um detalhe: fgets lê os dados até encontrar uma quebra de linha ou ler a quantidade máxima de caracteres. Isso quer dizer que se tiver uma linha maior do que MAX_LINE_LEN, ele não lerá a linha toda.

Uma abordagem mais garantida é ir lendo os caracteres e guardando em um buffer, até encontrar a quebra de linha (ou o final do arquivo, que indicaria a última linha). Mas se o tamanho máximo for atingido antes disso, bastaria realocar para um tamanho maior. Eu me baseei neste código e adaptei para o seu caso.

Outro ponto é que não precisa ler o arquivo duas vezes (uma para ver a quantidade de linhas, outro para buscar a linha aleatória). Você pode ir lendo apenas uma vez, usando este algoritmo (que também adaptei para o seu caso). A ideia é que a cada linha você teste se 1 dividido pelo número da linha é maior que um determinado valor aleatório - mas para isso ele usa a função drand48, que retorna um float aleatório entre 0 e 1 (disponível apenas no Linux - para Windows, tem outras alternativas).

Enfim, ficou assim:

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

char *choose_random_line(FILE *f) {
    srand48(time(NULL));
    int current_max = 256; // um tamanho inicial qualquer
    char *selected = malloc(current_max);
    char *current = malloc(current_max);
    selected[0] = '\0';

    int length = 0, line_number = 0;
    int ch = 0;
    while (ch != EOF) {
        ch = fgetc(f);
        if (length == current_max) { // atingiu o tamanho máximo, realocar mais memória
            current_max *= 2; // uma estratégia comum é dobrar o tamanho
            current = realloc(current, current_max);
            selected = realloc(selected, current_max);
            // pode incluir if (selected == NULL) para verificar se conseguiu realocar, etc
        }

        if (ch == '\n' || ch == EOF) { // terminou a linha ou o arquivo
            current[length] = '\0';
            if (drand48() < 1.0 / ++line_number) { // aleatoriamente pode selecionar esta linha
                strcpy(selected, current);
            }
            length = 0;
            continue;
        }

        current[length++] = ch;
    }
    free(current);

    return selected;
}

int main(void) {
    FILE *f = fopen("texto.txt", "r");
    if (f == NULL) {
        fprintf(stderr, "Error when trying to read file.\n");
        return 1;
    }
    char *random_line = choose_random_line(f);
    printf("Linha aleatória: %s\n", random_line);
    free(random_line);
    fclose(f);
    return 0;
}
2

Muito obrigado pelo feedback. Não conhecia esse detalhe do fgets que você mencionou.

Achei interessante essa sua versão do código e com certeza seria algo que eu usaria em um caso real. Mas no meu post, eu quis deixar o mais simples possível, sem alocação de memória ou funções do header string, apenas um algoritmo para alguém que teve pouco ou nenhum contato poder entender.

De todo modo, é interessante ter uma visão mais concreta de um caso de uso. Obrigado pelo seu comentário.

2

Conteúdo ótimo! Parabéns pelo post! Atualmente estou estudando sistemas operacionais e recentemente me deparaei com esta função C "fopen" como um dos exemplos de interrupções ao processador.
Confesso que o que me atraiu no post foi um erro de interpretação de minha parte. Estava esperando algo disruptivo que ensinasse qualquer um a entender uma linha de código C 😂

1

Obrigado pelo comentário, Vini. Eu tentei deixar tanto os exemplos quanto o assunto o mais simples possível para qualquer um poder entender mas existem algumas outras etapas anteriores que, possivelmente, acabei não comentando