【问题标题】:Read random line from .txt file从 .txt 文件中读取随机行
【发布时间】:2017-02-28 08:00:04
【问题描述】:

我正在尝试通过从 .txt 文件中读取随机单词来升级我的 Hangman 游戏。问题是,我不知道如何从 .t​​xt 文件中读取随机行。 .txt 文件的每一行都有一个单词。

void ler_palavras()
{
    FILE *words;

    if ((words = fopen("words.txt", "r")) == NULL) {
        printf("Error! opening file");
        exit(1);
    }

    // reads text until newline
    fscanf(words,"%[^\n]", word);
    fclose(words);
}

【问题讨论】:

  • 首先读取所有单词,然后使用 rand() 在范围内(0,nb 个单词)选择一个。
  • @Jean-FrançoisFabre 我试试看,谢谢回复
  • @Jean-FrançoisFabre: rand 只会在 (0, RAND_MAX) 范围内生成,而 RAND_MAX 通常只有 32767,因此任何大小的 word 文件都需要多次调用rand 与结果一起移动和|-ed。或者使用更好的、但可移植性较差的 PRNG 库,它可以为您提供更多可扩展的随机性。
  • @ShadowRanger 你是对的。没想到。

标签: c file random


【解决方案1】:

如果由于某种原因,您不能只将整组行加载到内存中(太大或其他),有一种方法可以从流式条目集中选择一个随机条目。它不会无限扩展,并且会表现出小的偏差,但这是一个游戏,而不是密码学,因此不应该成为破坏交易的因素。

逻辑是:

  1. 声明一个缓冲区来保存单词
  2. 打开文件
  3. 对于每一行:
    • 增加一个计数器,指示您在哪一行
    • 随机生成一个double(例如,使用drand48 或您可以使用的任何 PRNG 工具)
    • 如果1.0 / lineno > randval,用当前行中的单词替换当前存储的单词(所以第一行是自动存储的,第二行有50%的可能性替换它,第三行有33%的可能性这样做,等)
  4. 当行数用完时,word 中存储的任何内容都是您的选择

假设行数足够小(并且您的 PRNG 生成的doubles 范围足够细),这会尽可能接近任何给定行被选中的可能性;两条线,每条有 50/50 的镜头,三条线,33.33...%,等等。

我现在缺少 C 编译器,但基本代码如下所示:

/* Returns a random line (w/o newline) from the file provided */
char* choose_random_word(const char *filename) {
    FILE *f;
    size_t lineno = 0;
    size_t selectlen;
    char selected[256]; /* Arbitrary, make it whatever size makes sense */
    char current[256];
    selected[0] = '\0'; /* Don't crash if file is empty */

    f = fopen(filename, "r"); /* Add your own error checking */
    while (fgets(current, sizeof(current), f)) {
        if (drand48() < 1.0 / ++lineno) {
            strcpy(selected, current);
        }
    }
    fclose(f);
    selectlen = strlen(selected);
    if (selectlen > 0 && selected[selectlen-1] == '\n') {
        selected[selectlen-1] = '\0';
    }
    return strdup(selected);
}

【讨论】:

  • 因独创性而受到支持,但我认为,一个有十亿个单词可供选择的刽子手游戏将非常具有挑战性:-)
  • @squeamishossifrage:是的。但是在 Hangman 的情况下,每个游戏只选择一次单词,将整个单词文件存储在内存中是浪费的,并且基于用户输入在每个 游戏 迭代文件一次的成本是可能不明显(如果有问题,请在游戏运行时在后台线程中抓取下一个单词 ;-))。对于大型文件或从文件读取频率不足以证明将整个内容放入内存的情况,这都是有意义的。
  • 替代方案:掷骰子;寻找相对于文件大小的位置;读到下一个\n;读单词。
  • @PaulOgilvie:是的,但这会引入偏见;你更可能选择长词后的词而不是短词后的词。
  • @ShadowRanger 我认为这可能行得通。非常感谢!
【解决方案2】:

rand() 有其局限性,包括仅生成值0RAND_MAX,并且一个文件可以有很多次RAND_MAX 行。假设行数大约为RAND_MAX/10 或更少,以下将满足 OP 的目标。

执行一次以计算行数。 --> lc

对于需要的每个随机行,重新读取文件的行,从行索引开始之前[0... lc-1] 范围内的某个随机数。

然后只需阅读并打印该行。不需要行缓冲区。该文件是行缓冲区。该代码重复使用Line_Count() 来计算总行数和读取到第n 行。

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

// Return line count, but stop once the count exceeds a maximum
int Line_Count(FILE *istream, int line_index) {
  int lc = 0;
  int previous = '\n';
  int ch;
  rewind(istream);
  while (line_index > 0 && (ch = fgetc(istream)) != EOF) {
    if (ch == '\n') {
      line_index--;
    }
    if (previous == '\n') {
      lc++;
    }
    previous = ch;
  }
  return lc;
}

void print_random_line(FILE *istream, int line_index) {
  printf("%8d: <", line_index + 1);
  Line_Count(istream, line_index);
  int ch;
  while ((ch = fgetc(istream)) != EOF && ch != '\n') {
    if (isprint(ch)) {
      putchar(ch);
    }
  }
  printf(">\n");
}

int main() {
  srand((unsigned) time(NULL));
  FILE *istream = fopen("test.txt", "r");
  assert(istream);
  int lc = Line_Count(istream, RAND_MAX);
  assert(lc && lc < RAND_MAX);
  for (int i = 0; i < 5; i++) {
    print_random_line(istream, rand() % lc);
  }
  fclose(istream);
}

【讨论】:

  • rand 的范围是 0 到 RAND_MAX,而不是 INT_MAX。前者通常更具限制性,在许多系统上,它只有 32767,这明显小于合理大小的单词文件中的行数。您肯定希望在这样的系统上将rand 的多个输出混合在一起。
  • @ShadowRanger 是的,当然应该写成RAND_MAX 而不是INT_MAX。考虑到这一点,我编写了代码。除了 DOS 系统,您是否想到了一个具有文件系统和 RAND_MAX == 32767 的当前平台?
  • @chux 32767 is still the current value in Windows。其他地方都是0x7fffffff
  • @squeamishossifrage LSNED!详细信息“32767 仍然是 Visual Studio 中的当前值。”我的 gcc 编译器是 2147483647 - 在 Windows 中。
【解决方案3】:

这是另一个解决方案,仍然受RAND_MAX 的限制,不需要读取每一行直到所选行。这个想法是使用一个二进制文件,将每个单词存储在相同的字节数中,因此可以使用fseek()fread() 访问任何单词。文件中的第一个条目是一个long 值,用于存储文件中的字数。添加单词时,此值会更新。

这是一个查找名为wordlist.txt 的普通文本文件的实现,该文件每行有一个单词。如果找到,程序会更新(或创建,如有必要)名为wordlist.fmt 的文件。更新函数从文本文件中读取每个单词,跳过空行,并将其以固定字节数存储在二进制文件中。读完所有单词后,更新字数。使用文本文件运行程序一次后,您应该删除文本文件,否则下一次运行将再次添加单词。 .fmt 文件应该保留,如果你想添加更多的话,只需在包含可执行文件的目录中放置一个新的文本文件,然后再次运行。

打印五个随机词的循环生成一个随机数,使用该数字移动到包含一个词的文件位置,将该词读入一个数组,然后打印它。

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

#define RAW_WORDS  "wordlist.txt"
#define FMT_WORDS  "wordlist.fmt"
#define OFFSET_SZ  (sizeof(long))
#define MAXWORD    30

void update_words(FILE *fp_fmt, FILE *fp_raw);
void strip(char *str);

int main(void)
{
    FILE *raw_words, *formatted_words;
    char word[MAXWORD];
    long wordcount;
    int i;
    int wpos;

    raw_words = fopen(RAW_WORDS, "r");

    /* Try to open existing file for update, otherwise open new file */ 
    if ((formatted_words = fopen(FMT_WORDS, "r+b")) == NULL){
        if ((formatted_words = fopen(FMT_WORDS, "w+b")) == NULL) {
            fprintf(stderr, "Unable to open file %s\n", FMT_WORDS);
            exit(EXIT_FAILURE);
        } else {                    // initialize file wordcount
            wordcount = 0L;
            fwrite(&wordcount, OFFSET_SZ, 1, formatted_words);
            fflush(formatted_words);
        }
    }

    /* Update FMT_WORDS file if RAW_WORDS is present */
    if (raw_words != NULL)
        update_words(formatted_words, raw_words);

    /* Get 5 random words and print them */
    srand((unsigned)time(NULL));

    rewind(formatted_words);
    fread(&wordcount, OFFSET_SZ, 1, formatted_words);

    printf("Five random words from %s:\n", FMT_WORDS);
    for (i = 0; i < 5; i++) {
        wpos = rand() % wordcount;
        fseek(formatted_words, wpos * MAXWORD + OFFSET_SZ, SEEK_SET);
        fread(word, MAXWORD, 1, formatted_words);
        puts(word);
    }

    if (raw_words && (fclose(raw_words) != 0))
        fprintf(stderr, "Unable to close file %s\n", RAW_WORDS);
    if (fclose(formatted_words) != 0)
        fprintf(stderr, "Unable to close file %s\n", FMT_WORDS);

    return 0;
}

void update_words(FILE *fp_fmt, FILE *fp_raw)
{
    char word[MAXWORD];
    long wordcount;

    /* Read in wordcount and move to end of file */
    rewind(fp_fmt);
    fread(&wordcount, OFFSET_SZ, 1, fp_fmt);
    fseek(fp_fmt, wordcount * MAXWORD, SEEK_CUR);

    /* Write formatted words, skipping blank lines */
    while (fgets(word, MAXWORD, fp_raw) != NULL) {
        if (word[0] != '\n') {
            strip(word);
            if (fwrite(word, MAXWORD, 1, fp_fmt) != 1) {
                fprintf(stderr, "Error writing to %s\n", FMT_WORDS);
                exit(EXIT_FAILURE);
            }
            ++wordcount;
        }
    }

    /* Update wordcount in file and flush output */
    rewind(fp_fmt);
    fwrite(&wordcount, OFFSET_SZ, 1, fp_fmt);
    fflush(fp_fmt);
}

void strip(char *str)
{
    while (*str != '\n' && *str != '\0')
        str++;
    *str = '\0';
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-11-26
    • 1970-01-01
    相关资源
    最近更新 更多