C語言讀取寫入CSV文件 [三] 進階篇——讀取CSV文件


本系列文章目錄

[一] 基礎篇

[二] 進階篇——寫入CSV

[三] 進階篇——讀取CSV


處理讀取得到的數據

在基礎篇中,僅僅是將數據讀取出來然后輸出,並未將其轉換為相應的數據類型。對於整數,我們可以使用 atoi()atol()atoll() 函數分別將字符串轉換為 intlonglong long類型;對於浮點數,我們可以使用 atof() 函數將字符串轉換為 double 類型;而對於字符串,我們只需要使用 strdup() 進行復制一下即可。

利用結構體來保存數據

在同一個 CSV 中的數據是具有相關性的,因此最好的方式是將構建一個結構體,利用結構體的成員來記錄CSV文件不同列的數據。例如 CSV 文件內容如下:

ID,Name,Points
1,qwe,1.1
2,asd,2.200000

可以用如下的結構體進行記錄:

struct student {
    int id;
    char *name;
    double point;
};

結合上一小節處理讀取得到的數據,那么最后的代碼如下:

點擊查看3-1.c完整代碼
// 3-1.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char* get_field(char *line, int num);
char* remove_quoted(char *str);

struct student {
    int id;
    char *name;
    double point;
};

void print_student_info(struct student *stu);

int main()
{
    FILE *fp = fopen("tmp.csv", "r");
    if (fp == NULL) {
        fprintf(stderr, "fopen() failed.\n");
        exit(EXIT_FAILURE);
    }

    char row[80];
    char *token;

    fgets(row, 80, fp);
    char *header_1 = get_field(strdup(row), 1);
    char *header_2 = get_field(strdup(row), 2);
    char *header_3 = get_field(strdup(row), 3);
    printf("%s\t%s\t%s", header_1, header_2, header_3);

    char *tmp;
    struct student stu;
    while (fgets(row, 80, fp) != NULL) {
        tmp = get_field(strdup(row), 1);
        stu.id = atoi(tmp);

        tmp = get_field(strdup(row), 2);
        stu.name = strdup(tmp);

        tmp = get_field(strdup(row), 3);
        stu.point = atof(tmp);

        print_student_info(&stu);
    }

    fclose(fp);
    return 0;
}

char* get_field(char *line, int num)
{
    char *tok;
    tok = strtok(line, ",");
    for (int i = 1; i != num; i++) {
        tok = strtok(NULL, ",");
    }
    char *result = remove_quoted(tok);

    return result;
}

char* remove_quoted(char *str)
{
    int length = strlen(str);
    char *result = malloc(length + 1);
    int index = 0;
    for (int i = 0; i < length; i++) {
        if (str[i] != '\"') {
            result[index] = str[i];
            index++;
        }
    }
    result[index] = '\0';
    return result;
}

void print_student_info(struct student *stu)
{
    printf("%d\t%s\t%f\n", stu->id, stu->name, stu->point);
}

運行上述代碼得到的結果如下:

$ clang 3-1.c -o 3-1   
$ ./3-1 
ID      Name    Points
1       qwe     1.100000
2       asd     2.200000

識別被包裹的字段

[二] 進階篇——寫入CSV中提到過包裹的概念,包裹的主要作用是為了能夠讓字段中包含一些特殊字符(如逗號、雙引號等)。下面用包裹的字段中含有分隔符即逗號為例,來講解如何識別被包裹的字段。

因為被包裹的字段中存在逗號,若再用 strtok() 函數來進行解析,則會將包裹的字段截斷。因此處理方式應該為逐個去遍歷字符串,當出現雙引號(")時,作一個標記,直到再遇到下一個雙引號時取消標記。編寫了一個名為 char** get_field_arr(char *line) 的解析函數,返回的是一個字符串數組。在只給定某行CSV的字符串時,無法確定其存在的字段數量,進而無法分配合適的空間供保存結果,因此還需要另一個 int count_field(char *line) 函數來計算的字段數量。

處理字段開頭和結尾處的空格和制表符

在本文中,我們采用 RFC 4180 標准中的規定,需要保留字段開頭和結尾處的空格和制表符,具體實現上比不保留這些字符容易很多,只需要把空格和制表符視為普通的字符一樣,進行保存即可。最后的代碼如下:

點擊查看3-2.c完整代碼
// 3-2.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int count_field(const char *line); 
char** get_field_arr(const char *line);

struct student {
    int id;
    char *name;
    double point;
};

void print_student_info(struct student *stu);

int main()
{
    const char *line = "  \"4\",def,\"4.4\"  \0";

    int count = count_field(line);
    char **result = get_field_arr(line);
    printf("--- Parse line result ---\n");
    for (int i = 0; i < count; i++) {
        printf("result[%d] = %s\n", i, result[i]);
    }

    struct student stu;
    stu.id = atoi(result[0]);
    stu.name = strdup(result[1]);
    stu.point = atof(result[2]);
    print_student_info(&stu);
    
    return 0;
}

int count_field(const char *line) {
    const char *p_line = line;
    int count = 1, is_quoted = 0;

    for (; *p_line != '\0'; p_line++) {
        if (is_quoted) {
            if (*p_line == '\"') {
                if (p_line[1] == '\"') {
                    p_line++;
                    continue;
                }
                is_quoted = 0;
            }
            continue;
        }

        switch(*p_line) {
            case '\"':
                is_quoted = 1;
                continue;
            case ',':
                count++;
                continue;
            default:
                continue;
        }
    }

    if (is_quoted) {
        return -1;
    }

    return count;
}

char** get_field_arr(const char *line) {
    int count = count_field(line);
    if (count == -1) {
        return NULL;
    }

    char **buf = malloc(sizeof(char*) * (count+1));
    if (buf == NULL) {
        return NULL;
    }
    char **pbuf = buf;

    char *tmp = malloc(strlen(line)+1);
    if (tmp == NULL) {
        free(buf);
        return NULL;
    }
    *tmp = '\0';
    char *ptmp = tmp;

    const char *p_line = line;
    int is_quoted = 0, is_end = 0;

    for (; ; p_line++) {
        if (is_quoted) {
            if (*p_line == '\0') {
                break;
            }

            if (*p_line == '\"') {
                if (p_line[1] == '\"') {
                    *ptmp++ = '\"';
                    p_line++;
                    continue;
                }
                is_quoted = 0;
            }
            else {
                *ptmp++ = *p_line;
            }

            continue;
        }

        switch(*p_line) {
            case '\"':
                is_quoted = 1;
                continue;
            case '\0':
                is_end = 1;
            case ',':
                *ptmp = '\0';
                *pbuf = strdup(tmp);

                if (*pbuf == NULL) {
                    for (pbuf--; pbuf >= buf; pbuf--) {
                        free(*pbuf);
                    }
                    free(buf);
                    free(tmp);
                    return NULL;
                }

                pbuf++;
                ptmp = tmp;

                if (is_end) {
                    break;
                } else {
                    continue;
                }

            default:
                *ptmp++ = *p_line;
                continue;
        }

        if (is_end) {
            break;
        }
    }

    *pbuf = NULL;
    free(tmp);
    return buf;
}

void print_student_info(struct student *stu)
{
    printf("--- Student info ---\n");
    printf("%d\t%s\t%f\n", stu->id, stu->name, stu->point);
}

代碼的運行結果如下所示:

$ clang 3-2.c -o 3-2
$ ./3-2                          
--- Parse line result ---
result[0] =   4
result[1] = def
result[2] = 4.4  
--- Student info ---
4       def     4.400000

其他分隔符

[二] 進階篇——寫入CSV中的最后,也提到在某些國家的CSV文件中,可能會使用分號(;)來作為分隔符,那么我們在解析CSV時只需要把原本判斷逗號(,)的語句改變為分號(;)即可

使用庫

最后,解析CSV文件更好地策略是使用別人已經寫好的庫,不要重復發明輪子!例如libcsv,其就是使用純 ANSI C 寫成的庫,具體的安裝方式可參考其主頁,使用方式可以通過閱讀其手冊來進行了解,此處不再贅述。


如果想要了解偏基礎的 C 語言讀取寫入 CSV 文件的內容,歡迎閱讀:[一] 基礎篇

如果想要了解進階的 C 語言寫入 CSV 文件的內容,歡迎閱讀:[二] 進階篇——寫入CSV


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM