本系列文章目錄
處理讀取得到的數據
在基礎篇中,僅僅是將數據讀取出來然后輸出,並未將其轉換為相應的數據類型。對於整數,我們可以使用 atoi()
、atol()
、atoll()
函數分別將字符串轉換為 int
、long
、long 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