在文本文件C中搜索字符串

5
下面的代码逐个字符读取文本文件并将其打印到标准输出:
#include <stdio.h>

int main()
{
    char file_to_open[] = "text_file.txt", ch;
    FILE *file_ptr;

    if((file_ptr = fopen(file_to_open, "r")) != NULL)
    {
        while((ch = fgetc(file_ptr)) != EOF)
        {
            putchar(ch);
        }
    }
    else
    {
        printf("Could not open %s\n", file_to_open);
        return 1;
    }
    return(0);
}

但我想要的不是像 putchar(ch) 一样输出到 stdout,而是在文件中查找另一个文本文件 strings.txt 中提供的特定字符串,并将匹配的行输出到 out.txt。
text_file.txt:
1993 - 1999 Pentium 1997 - 1999 Pentium II 1999 - 2003 Pentium III 1998 - 2009 Xeon 2006 - 2009 Intel Core 2
strings.txt:
Nehalem AMD Athlon Pentium
在这种情况下,text_file.txt 的前三行将匹配。我已经对 C 中的文件操作进行了一些研究,似乎可以使用 fgetc(就像在我的代码中所做的那样)逐个字符读取,使用 fgets 一次读取一行,使用 fread 一次读取一个块,但在我看来,在我这种情况下完美的方法应该是按单词读取?

4
你为什么要写这个程序?!使用grep/awk/sed来完成这个任务。 - sean riley
不,Tim。标签是用于搜索的。没有人会搜索那个。 - GManNickG
1
是的,我知道使用标准的Unix工具可以在几秒钟内解决这个问题,但这是为了更深入地理解C文件IO。 - CHR_1980
4个回答

9
我假设这是一个学习练习,你只是在寻找一个起点。否则,你不应该重复造轮子。
下面的代码应该让你了解涉及到的内容。它是一个程序,允许你指定要搜索的文件名和一个单一的参数来搜索该文件。你应该能够修改它,将要搜索的短语放在一个字符串数组中,并检查读取的任何行中是否出现了该数组中的任何单词。
你需要查找的关键函数是 strstr
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#ifdef DEBUG
#define INITIAL_ALLOC 2
#else
#define INITIAL_ALLOC 512
#endif

char *
read_line(FILE *fin) {
    char *buffer;
    char *tmp;
    int read_chars = 0;
    int bufsize = INITIAL_ALLOC;
    char *line = malloc(bufsize);

    if ( !line ) {
        return NULL;
    }

    buffer = line;

    while ( fgets(buffer, bufsize - read_chars, fin) ) {
        read_chars = strlen(line);

        if ( line[read_chars - 1] == '\n' ) {
            line[read_chars - 1] = '\0';
            return line;
        }

        else {
            bufsize = 2 * bufsize;
            tmp = realloc(line, bufsize);
            if ( tmp ) {
                line = tmp;
                buffer = line + read_chars;
            }
            else {
                free(line);
                return NULL;
            }
        }
    }
    return NULL;
}

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

    if ( argc != 3 ) {
        return EXIT_FAILURE;
    }

    fin = fopen(argv[1], "r");

    if ( fin ) {
        while ( line = read_line(fin) ) {
            if ( strstr(line, argv[2]) ){
                fprintf(stdout, "%s\n", line);
            }
            free(line);
        }
    }

    fclose(fin);
    return 0;
}

示例输出:

E:\Temp> searcher.exe searcher.c char
char *
    char *buffer;
    char *tmp;
    int read_chars = 0;
    char *line = malloc(bufsize);
    while ( fgets(buffer, bufsize - read_chars, fin) ) {
        read_chars = strlen(line);
        if ( line[read_chars - 1] == '\n' ) {
            line[read_chars - 1] = '\0';
                buffer = line + read_chars;
主函数(int argc,char *argv[]) {
    char *line;

注意:本文涉及到 IT 技术相关内容。

这看起来非常有趣。你正确地假设了,这是一个学习练习,我可以看到源代码包含我之前使用过的元素,所以我应该能够完全理解这段代码。 - CHR_1980
我对C代码还比较新,但我刚刚用fgets函数调用替换了整个read_line函数调用,并在主函数中分配了char* line到一个任意大的数字,因为fgets会停在'\n'字符上。你能解释一下read_line函数的预期目的吗?看起来里面有很多多余的代码。 - fIwJlxSzApHEZIl
1
@advocate 足够大是多大? 我从一个合理大小的缓冲区开始,并根据需要不断扩展它。实际上,应该再进行一次检查,以防止缓冲区过大,如果有人向其提供没有行结束符的流,则会导致计算机内存耗尽,但这只是一个简单的学习练习。 - Sinan Ünür
给Sinan Unur, 请问您能为我解释一下您的代码吗?为什么您要用bufsize-readchars来指定fgets()的缓冲区大小?此外,当我尝试运行您的代码并打印第一次读取的字符数(第一个循环),它打印了19个字符?由于字符串还没有被初始化,strlen(line)不应该返回0吗?(我是通过提供searcher.c文件来使用这段代码的)谢谢 Edward - zangetsKid
未初始化的内存包含垃圾,因此您不应该有任何期望。缓冲区会填充直到读取完整行。 - Sinan Ünür

4
记住:fgetc(), getc(), getchar()都返回一个整数,而不是一个字符。这个整数可能是EOF或一个有效的字符——但它返回的值比char类型支持的范围多一个。
你正在编写一个替代“fgrep”命令的代理程序:
fgrep -f strings.txt text_file.txt > out.txt

与读取字符不同,你需要使用fgets()来读取行。请忘记gets()函数的存在!

我为您缩进了代码,并在末尾插入了"return 0;"(尽管C99如果从main()中跳出,则会隐式地执行'return 0;')。然而,C99还要求每个函数都有明确的返回类型-我为您添加了'int'到'int main()'(但是您不能使用符合C99标准的理由来解释为什么在末尾没有返回0)。错误消息应该写入标准错误,而不是标准输出。

您可能需要使用动态分配来创建字符串列表。一个简单的搜索方法是将'strstr()'应用于每个输入行中的每个所需字符串(确保一旦找到匹配项就退出循环,以便不重复处理一行如果在单个行上有多个匹配项)。

更复杂的搜索将预先计算哪些字符可以忽略,以便您可以并行搜索所有字符串,比嵌套循环更快地跳过文本。这可能是搜索算法(如Boyer-Moore或Knuth-Morris-Pratt (附加):或Rabin-Karp,它专门为多个字符串设计了并行搜索)的修改版本。


个人而言,我更喜欢编写一个缓冲字符的函数...仅使用fgets会给行长带来任意限制。 - asveikau
@asveikau: 我看不出区别?当使用fgets时,我们提供缓冲区,可以将其设置为任何大小。如果strings.txt中的行比缓冲区长,我们无论如何都会遇到麻烦...你的意思是即使使用fgets,我们也应该处理缓冲区溢出情况吗?确实是这样,并且这比使用未分类的缓冲区要不明显得多。 - kriss
fgets() 函数读取给定缓冲区长度的内容;如果在空间用完之前没有遇到换行符,它将停止读取并返回。因此,如果最后一个字符不是换行符且缓冲区已满,则可以找到更多的空间(重新分配?)将额外的字符放入其中,并再次调用 fgets() (谨慎地 - 从上次结束的位置开始,只告诉它关于额外的空间),以获取该行的其余部分。所以,您可以编写自己的读取器来将数据读入动态分配的缓冲区中并使其增长 - 或使用 fgets() 来处理缓冲区并读取数据。 - Jonathan Leffler
1
你也可以决定,如果该行超过 POSIX 行长度(_POSIX2_LINE_MAX,其最小值为 2048),则无论是拆分还是截断都无关紧要。我倾向于使用 4096 作为“长行缓冲区”。 - Jonathan Leffler
除非strings.txt与他展示的内容有很大不同,否则他可以寻找到它的末尾,获取位置,并将其用作缓冲区的大小--因为它包含了他正在搜索的所有字符串,所以它至少与他正在搜索的任何一个字符串一样长。他唯一真正的要求是他正在寻找的最长字符串能够一次性适合缓冲区。除此之外,任何超出这个范围的东西都无关紧要--输入中不适合该缓冲区大小的单个单词无法匹配他关心的任何单词。 - Jerry Coffin
显示剩余2条评论

2
cat strings.txt |while read x; do grep "$x" text_file.txt; done

1
你是指 fgrep -f strings.txt text_file.txt > out.txt 这个命令吗? - Jonathan Leffler
是的,是的,fgrep -f strings.txt text_file.txt。我猜更多的曝光意味着更多的选择。 - Ewan Todd
谢谢。写一个 C 程序来做这件事完全是浪费时间。 - sean riley
任何与学习有关的事情都不是浪费时间,至少在我看来是这样。但如果这不是为了学习新东西,你可能是正确的。 - CHR_1980

2

块状阅读总是更好的,因为它是底层文件系统的工作方式。

因此,只需按块读取,检查您的单词是否出现在缓冲区中,然后再读取另一个缓冲区。您只需要小心地将前一个缓冲区的最后几个字符复制到新缓冲区中,以避免在搜索词在缓冲区边界时丢失检测。

如果这个简单的算法不够用(在您的情况下可能足够),则有一个更复杂的算法可以在一个缓冲区中同时搜索多个子字符串,即Rabin-Karp算法。


当您使用fgetc()函数时,我相当确定stdio会按块读取并缓存字符... - asveikau
真实,但调用fgetc本身就有其成本,如果要将输入与字符串(或多个字符串)进行比较,则必须将其复制到某个位置。这比读取完整缓冲区并与其一起工作的成本要高得多。读取一整行,正如Jonathan提出的那样,如果您不想自己管理直接读取缓冲区的血腥细节,则也是一个不错的选择。 - kriss

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接