从文件中读取带有空格的字符串

3
我正在处理一个项目,遇到了一个非常烦人的问题。我有一个文件存储了我帐户收到的所有消息。一条消息是按以下方式定义的数据结构:
typedef struct _message{
char dest[16]; 
char text[512];
}message;

"dest"是一个字符串,不能包含空格,而其他字段可以。字符串使用“fgets()”函数获取,因此“dest”和“text”可以具有“动态”长度(从1个字符到length-1个合法字符)。请注意,我手动删除每个字符串从stdin中检索后的换行符。
“inbox”文件使用以下语法存储消息:
dest
text

因此,例如,如果我收到了Marco的信息,内容是“你好,你怎么样?”和Tarma的信息,内容是“你今天要去健身房吗?”,我的收件箱文件将如下所示:
Marco
Hello, how are you?

Tarma
Are you going to the gym today?

我想从文件中读取用户名并将其存储在字符串s1中,然后对消息执行相同的操作,并将其存储在字符串s2中(并重复此操作直到EOF),但由于text字段允许空格,因此我无法真正使用fscanf()。 我尝试使用fgets(),但正如我之前所说,每个字符串的大小是动态的。例如,如果我使用fgets(my_file,16,username),它最终会读取不需要的字符。我只需要读取第一个字符串直到达到\n,然后读取第二个字符串直到下一个\n被读取,这次包括空格。 你有任何解决这个问题的想法吗?

fgets 会在遇到换行符或者文件结束符时停止读取。如果你已经去掉了换行符,那么你不是已经知道字符串的确切长度了吗? - Tony
你传递给 fgets() 的 16 是字符串的 最大 长度。fgets() 在 (16-1) 个字符、换行符或 EOF 处停止。 - Weather Vane
@Tony 当我从文件中检索字符串时,我不知道确切的长度,我只知道一个字符串可以有的最大长度(目标为16个字符,文本包括\0为512个字符)。对于我的表述不够清晰,我感到抱歉。 - Atlas80b
@Weather Vane 你说得对。也许我应该改变写入文件中消息的语法... - Atlas80b
3个回答

2

由于每个字符串的长度都是动态的,所以如果我是你,我会先读取文件以找到每个字符串的大小,然后创建一个动态数组来存储字符串的长度值。

假设您的文件内容如下:

A long time ago
in a galaxy far,
far away....

所以第一行长度为15,第二行长度为16,第三行长度为12
然后创建一个动态数组来存储这些值。
接着,在读取字符串时,将对应的数组元素作为第二个参数传递给fgets。例如:fgets(string, arrStringLength[i++], f);
但是这样做需要读取文件两次。

2
#include <stdio.h>

int main(void){
    char username[16];
    char text[512];
    int ch, i;
    FILE *my_file = fopen("inbox.txt", "r");

    while(1==fscanf(my_file, "%15s%*c", username)){
        i=0;
        while (i < sizeof(text)-1 && EOF!=(ch=fgetc(my_file))){
            if(ch == '\n' && i && text[i-1] == '\n')
                break;
            text[i++] = ch;
        }
        text[i] = 0;
        printf("user:%s\n", username);
        printf("text:\n%s\n", text);
    }
    fclose(my_file);
    return 0;
}

我只有一个小疑问关于你的实现,"%*c" 是否告诉 fscanf() 忽略在15个字符后遇到的最后一个字符? - Atlas80b
@Toxicroak 15是最多可读取的字符数。实际上并不总是读取15个字符。 %*c用于跳过名称后的换行符。 - BLUEPIXY

1

只要你小心谨慎,就可以轻松使用fgets()。这段代码似乎可行:

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

enum { MAX_MESSAGES = 20 };

typedef struct Message
{
    char dest[16]; 
    char text[512];
} Message;

static int read_message(FILE *fp, Message *msg)
{
    char line[sizeof(msg->text) + 1];
    msg->dest[0] = '\0';
    msg->text[0] = '\0';
    while (fgets(line, sizeof(line), fp) != 0)
    {
        //printf("Data: %zu <<%s>>\n", strlen(line), line);
        if (line[0] == '\n')
            continue;
        size_t len = strlen(line);
        line[--len] = '\0';
        if (msg->dest[0] == '\0')
        {
            if (len < sizeof(msg->dest))
            {
                memmove(msg->dest, line, len + 1);
                //printf("Name: <<%s>>\n", msg->dest);
            }
            else
            {
                fprintf(stderr, "Error: name (%s) too long (%zu vs %zu)\n",
                        line, len, sizeof(msg->dest)-1);
                exit(EXIT_FAILURE);
            }
        }
        else
        {
            if (len < sizeof(msg->text))
            {
                memmove(msg->text, line, len + 1);
                //printf("Text: <<%s>>\n", msg->dest);
                return 0;
            }
            else
            {
                fprintf(stderr, "Error: text for %s too long (%zu vs %zu)\n",
                        msg->dest, len, sizeof(msg->dest)-1);
                exit(EXIT_FAILURE);
            }
        }
    }
    return EOF;
}

int main(void)
{
    Message mbox[MAX_MESSAGES];
    int n_msgs;

    for (n_msgs = 0; n_msgs < MAX_MESSAGES; n_msgs++)
    {
        if (read_message(stdin, &mbox[n_msgs]) == EOF)
            break;
    }

    printf("Inbox (%d messages):\n\n", n_msgs);
    for (int i = 0; i < n_msgs; i++)
        printf("%d: %s\n   %s\n\n", i + 1, mbox[i].dest, mbox[i].text);

    return 0;
}

阅读代码将处理第一个名称之前、名称和文本之间以及最后一个名称之后的(多个)空行。它在决定是否将刚刚读取的行存储在消息的desttext部分方面略有不同。它使用memmove(),因为它知道要移动多少数据,并且数据已经以null结尾。如果您愿意,可以用strcpy()替换它,但它应该会更慢(可能无法测量),因为strcpy()在复制时必须测试每个字节,但memmove()则不需要。我使用memmove(),因为它总是正确的;memcpy()可以在这里使用,但仅在您保证没有重叠时才有效。安全第一,不要冒险增加软件漏洞。您可以决定错误退出是否适当——对于测试代码来说很好,但在生产代码中不一定是个好主意。您可以决定如何处理“0 条消息”与“1 条消息”与“2 条消息”等情况。
你可以轻松修改代码,使用动态内存分配来为消息数组分配空间。在main()中,将消息读入一个简单的Message变量中,并安排在获取完整消息时复制到动态数组中。另一种选择是“冒险”过度分配数组,尽管这不太可能成为一个主要问题(无论如何,您都不会一次增加一个条目来避免内存在每次分配期间移动时出现二次行为)。
如果每个消息有多个字段需要处理(例如,也包括接收日期和阅读日期),则需要进一步重新组织代码,可能需要另一个函数。
请注意,该代码避免了保留命名空间。例如,_message这样的名称被保留给“实现”。这样的代码不是C编译器及其支持系统的一部分,因此不应创建以下划线开头的名称。(这过于简化了约束条件,但只是稍微简单了一点,比更复杂的版本容易理解。)
代码小心地没有写任何魔数超过一次。
示例输出:
Inbox (2 messages):

1: Marco
   How are you?

2: Tarma
   Are you going to the gym today?

非常详细的解释,对我帮助很大。谢谢! - Atlas80b

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