【问题标题】:Parsing file when no data between tokens令牌之间没有数据时解析文件
【发布时间】:2016-03-14 19:31:13
【问题描述】:

大家好,我正在读取一个文件并希望将数据解析为一个结构数组。该文件如下所示:

Country,City,Area Code,Population
China,Beijing,,21256972
France,Paris,334,3568253
Italy,Rome,,1235682

我想解析数据并将成员分配给文件中的每个区域。我在解析第 1 行和第 3 行的数据时没有问题。但是如果没有区号并且彼此相邻有两个逗号,则令牌变为空,并且出现错误。我一直在寻找,似乎无法找到解决方案。这是我的代码:

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

int main(void) {
    static struct Locations {
        char country[20];
        char city[20];
        char areaCode[5];
        char population[100];
    } line[2000000];

    // open file
    FILE *Lfile;
    Lfile = fopen("locations.txt", "r");
    if (!Lfile) {
        perror("File Error");
    }

    char buf[100];
    const char delim[2] = ",";
    char *token;

    int i = 0;
    while (fgets(buf, 100, Lfile) != NULL) {
        token = strtok(buf, delim);
        while (token != NULL) {
            strcpy(line[i].country, token);
            token = strtok(NULL, delim);
            strcpy(line[i].city, token);
            token = strtok(NULL, delim);
            strcpy(line[i].areaCode, token); //Error here
            token = strtok(NULL, delim);
            strcpy(line[i].population, token);
            token = strtok(NULL, delim);
        }
        printf("%s %s %s %s\n", line[i].country,
               line[i].city, line[i].areaCode, line[i].population);
        i++;
    }
    return 0;
}

【问题讨论】:

标签: c parsing struct token delimiter


【解决方案1】:

您不能使用strtok() 拆分CSV 文件,因为strtok 将分隔符字符串中的字符序列视为单个分隔符。它仅用于拆分空格分隔的标记。

您也不能使用sscanf("%[^,]", ...),因为sscanf 期望解析至少一个与, 不同的字符。

您可以使用strchr,也可以使用&lt;string.h&gt; 中的另一个函数来实现您的目的:strcspn()

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

/* copy at most len bytes from src to an array size char and
   null terminate it.  Return the length of the resulting C string
   possibly smaller than len if the destination is too small.
*/
size_t strcpymem(char *dest, size_t size, const char *src, size_t len) {
    if (len >= size)
        len = size - 1;
    memcpy(dest, src, len);
    dest[len] = '\0';
    return len;
}

#define RECORD_NUMBER   2000000

int main(void) {
    static struct Locations {
        char country[20];
        char city[20];
        char areaCode[5];
        char population[100];
    } line[RECORD_NUMBER];

    // open file
    FILE *Lfile;
    Lfile = fopen("locations.txt", "r");
    if (!Lfile) {
        perror("File Error");
    }

    char buf[100];

    int i = 0;
    while (i < RECORD_NUMBER && fgets(buf, 100, Lfile) != NULL) {
        char *p = buf;
        int len = strcspn(p, ",\n");
        strcpymem(line[i].country, sizeof line[i].country, p, len);
        p += len;
        if (*p == ',') p++;
        len = strcspn(p, ",\n");
        strcpymem(line[i].city, sizeof line[i].city, p, len);
        p += len;
        if (*p == ',') p++;
        len = strcspn(p, ",\n");
        strcpymem(line[i].areacode, sizeof line[i].areacode, p, len);
        p += len;
        if (*p == ',') p++;
        len = strcspn(p, ",\n");
        strcpymem(line[i].population, sizeof line[i].population, p, len);
        printf("%s %s %s %s\n", line[i].country,
               line[i].city, line[i].areaCode, line[i].population);
        i++;
    }
    return 0;
}

【讨论】:

    【解决方案2】:

    sscanf() 用于扫描零长度字段很麻烦。
    strtok() 合并相邻标记。
    有时最好的答案是写一些代码。


    形成一个辅助函数来解析子字符串。下面是一些未经测试的代码。关键是将代码的功能与更易于维护的功能分开

    // Starting a `p`, copy until delimiter found or size exhausted
    // Return NULL on failure
    const char *foo(const char *p, char *dest, size_t size, int end) {
      if (p) {
        while (size-- > 0 && *p) {
          *dest = *p++;
          if (*dest == end) {
            *dest = '\0';
            return p;
          }
          dest++; 
        }
      }
      return NULL;
    } 
    

    重写主循环

    // Make buf big enough
    char buf[sizeof line[0] * 2];
    
    // Use size_t rather than int
    size_t i = 0;
    // Limit iteration count
    while (i < RECORD_NUMBER && fgets(buf, sizeof buf, Lfile) != NULL) {
      const char *p = buf;
      p = foo(p, line[i].country,    sizeof line[i].country,    ',');
      p = foo(p, line[i].city,       sizeof line[i].city,       ',');
      p = foo(p, line[i].areaCode,   sizeof line[i].areaCode,   ',');
      p = foo(p, line[i].population, sizeof line[i].population, '\n');
      if (p == NULL || *p != '\0') {
        printf("Bad line '%s'\n", buf);
        exit (-1);
      }
      i++;
    }
    

    【讨论】:

      【解决方案3】:

      我喜欢使用sscanf 进行此类解析。

      int i = 0;
      while (fgets(buf, 100, Lfile) != NULL) {
          char *pbuf = buf;
          int offset;
          pbuf += sscanf( pbuf, "%[^,],%n", line[i].country , &offset) ? offset : 1;
          pbuf += sscanf( pbuf, "%[^,],%n", line[i].city    , &offset) ? offset : 1;
          pbuf += sscanf( pbuf, "%[^,],%n", line[i].areaCode, &offset) ? offset : 1;
          pbuf += sscanf( pbuf, " %s",       line[i].population );
      
          printf("%s %s %s %s\n", line[i].country,
                 line[i].city, line[i].areaCode, line[i].population);
          i++;
      }
      

      也许这是较慢的代码,也许它会因格式错误的行而崩溃。也许它还有其他缺陷,但这就是我喜欢做这种解析的方式。

      注意:如果解析空白字段,字符指针将不会被设置为NULL。因此,初始化结构很重要。

      编辑:修复了处理空字段时的错误。

      【讨论】:

      • 这段代码实际上无法读取空字段:您应该初始化目标字段。如果该行不包含足够的, 并且任何字段长于目标数组,它将调用未定义的行为。
      • 是的,你是对的chqrlie!我在测试中工作,因为我已经初始化了结构,但是在发布中省略了初始化代码。 :-o
      • 如果buf[0] == ',',此代码会导致未定义的行为。 "%[^,]" 对输入长度没有限制。 “修复了处理空字段时的错误。” --> 不,它失败了。很多问题 - 在使用 line[i].city 之前没有测试 sscanf() 的结果,在不知道已设置的情况下使用 offset
      • 确实更微妙一点:如果该行缺少字段,将解析最后一个字段,并且由于缺少,而停止扫描,sscanf返回1offset 没有改变,所以pbuf 没有正确更新……sscanf 可以进行快速而肮脏的解析,但很难使其可靠。
      • @oysteijo:谢谢。您可以查看您当前的实现并尝试确定哪些输入可能导致您的scanfs 失败......这是一个非常好的练习。一旦你掌握了这一点,你就可以在其他程序员使用这些函数时让他陷入困境。
      猜你喜欢
      • 1970-01-01
      • 2020-11-05
      • 1970-01-01
      • 2021-05-06
      • 1970-01-01
      • 2020-06-14
      • 1970-01-01
      • 1970-01-01
      • 2017-10-21
      相关资源
      最近更新 更多