Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 18.926 phút đọc

Đọc và ghi CSV

Sau bài này bạn sẽ làm được

  • Nêu ba quy tắc thoát của CSV theo RFC 4180
  • Viết hàm ghi một trường có thoát dấu nháy đúng
  • Viết bộ tách CSV xử lý được dấu phẩy và xuống dòng trong trường
  • Chỉ ra vì sao tách bằng strtok với dấu phẩy là sai

CSV trông như định dạng đơn giản nhất trên đời: tách bằng dấu phẩy, hết chuyện. Rồi một sinh viên tên là Nguyen Van A, Jr. xuất hiện, và cả tệp của bạn lệch cột từ đó về sau.

#Ba quy tắc của CSV

RFC 4180 mô tả CSV bằng vài quy tắc ngắn. Ba quy tắc dưới đây là toàn bộ những gì cần nhớ, và mọi lỗi CSV đều đến từ việc bỏ qua một trong ba.

Quy tắcTrường gốcGhi trong tệp
1. Trường có dấu phẩy phải bọc trong dấu nháy képa,b"a,b"
2. Trường có dấu nháy kép phải bọc, và mỗi dấu nháy thành hainoi "xin chao""noi ""xin chao"""
3. Trường có ký tự xuống dòng phải bọc trong dấu nháy képhai dong"hai dong"

#Ghi một trường

csv-ghi.c
#include <stdio.h>
#include <string.h>

/* Ghi mot truong CSV, tu bao dau nhay khi can. */
int csv_ghi_truong(FILE *f, const char *s) {
    int can_nhay = (*s == '\0')
                   || strpbrk(s, ",\"\n\r") != NULL
                   || s[0] == ' '
                   || s[strlen(s) - 1] == ' ';

    if (!can_nhay) return fputs(s, f) == EOF ? -1 : 0;

    if (fputc('"', f) == EOF) return -1;

    for (const char *p = s; *p; ++p) {
        if (*p == '"' && fputc('"', f) == EOF) return -1;   /* nhan doi */
        if (fputc(*p, f) == EOF) return -1;
    }

    return fputc('"', f) == EOF ? -1 : 0;
}

/* Ghi ca mot dong */
int csv_ghi_dong(FILE *f, const char *truong[], size_t n) {
    for (size_t i = 0; i < n; ++i) {
        if (i > 0 && fputc(',', f) == EOF)  return -1;
        if (csv_ghi_truong(f, truong[i]) != 0) return -1;
    }

    return fputs("\r\n", f) == EOF ? -1 : 0;
}
terminal
./thu-ghi-csv
goc: [a]
 ghi: a
goc: []
 ghi: ""
goc: [a,b]
 ghi: "a,b"
goc: [noi "xin chao"]
 ghi: "noi ""xin chao"""
goc: [ co khoang trang ]
 ghi: " co khoang trang "
goc: [hai
dong]
 ghi: "hai
dong"
goc: [binh thuong]
 ghi: binh thuong

#Bộ tách CSV

csv-tach.c
#include <stddef.h>

/* Tach mot dong CSV TAI CHO. Tra ve so truong tim duoc.
   ra[i] tro vao chinh bo dem dong, da bo dau nhay
   va da giai thoat dau nhay kep.

   Chuoi vao phai GHI DUOC. */
size_t csv_tach(char *dong, char *ra[], size_t toi_da) {
    size_t n = 0;
    char  *p = dong;

    while (n < toi_da) {
        char *ghi = p;             /* con tro ghi, luon chay sau hoac bang p */

        ra[n++] = ghi;

        if (*p == '"') {                        /* truong co dau nhay */
            ++p;

            for (;;) {
                if (*p == '\0') break;          /* dau nhay khong dong */

                if (*p == '"') {
                    if (p[1] == '"') {          /* hai nhay lien: mot nhay that */
                        *ghi++ = '"';
                        p += 2;

                        continue;
                    }

                    ++p;                        /* dau nhay dong */

                    break;
                }

                *ghi++ = *p++;
            }
        } else {                                /* truong tran */
            while (*p != ',' && *p != '\0' && *p != '\n' && *p != '\r')
                *ghi++ = *p++;
        }

        int het = (*p != ',');

        if (*p == ',') ++p;

        *ghi = '\0';                            /* ket thuc truong vua ghi */

        if (het) break;
    }

    return n;
}
terminal
./thu-tach-csv
a,b,c                        -> 3 truong: [a] [b] [c]
a,,c                         -> 3 truong: [a] [] [c]
,,                           -> 3 truong: [] [] []
"a,b",c                      -> 2 truong: [a,b] [c]
"noi ""xin chao""",x         -> 2 truong: [noi "xin chao"] [x]
"",""                        -> 2 truong: [] []
Nguyen Van A,20210001,8.25   -> 3 truong: [Nguyen Van A] [20210001] [8.25]

#Vì sao strtok không dùng được

strtok với dấu phẩy
char *t = strtok(dong, ",");

while (t) {
    xu_ly(t);
    t = strtok(NULL, ",");
}
Bộ tách viết riêng
char  *ra[16];
size_t n = csv_tach(dong, ra, 16);

for (size_t i = 0; i < n; ++i) xu_ly(ra[i]);
Vấn đề của strtokVí dụKết quả
Gộp nhiều dấu phân cách liên tiếp thành mộta,,cRa 2 trường thay vì 3, mọi cột sau lệch đi
Bỏ qua dấu phân cách ở đầu và cuối,,Ra 0 trường thay vì 3
Không hiểu dấu nháy"a,b",cRa 3 trường: "a b" c
Giữ trạng thái tĩnhTách hai dòng lồng nhauHai vòng lặp phá nhau, xem Bài 11.5
terminal
# Cùng dữ liệu, hai bộ tách
./so-sanh
dong: a,,c
  strtok  -> 2 truong: [a] [c]        <- MAT MOT COT
  csv_tach-> 3 truong: [a] [] [c]

dong: "a,b",c
  strtok  -> 3 truong: ["a] [b"] [c]  <- CAT NHAM
  csv_tach-> 2 truong: [a,b] [c]

#CSV trong thực tế

Dòng tiêu đề

/* Doc dong dau, tim vi tri tung cot theo TEN chu khong theo THU TU */
int doc_tieu_de(char *dong, int *vt_mssv, int *vt_ten, int *vt_diem) {
    char  *cot[32];
    size_t n = csv_tach(dong, cot, 32);

    *vt_mssv = *vt_ten = *vt_diem = -1;

    for (size_t i = 0; i < n; ++i) {
        if      (strcmp(cot[i], "mssv")   == 0) *vt_mssv = (int)i;
        else if (strcmp(cot[i], "ho_ten") == 0) *vt_ten  = (int)i;
        else if (strcmp(cot[i], "diem")   == 0) *vt_diem = (int)i;
    }

    if (*vt_mssv < 0 || *vt_ten < 0 || *vt_diem < 0) {
        fprintf(stderr, "Thieu cot bat buoc trong dong tieu de\n");

        return -1;
    }

    return 0;
}

/* Nho cach nay, tep co them cot moi hay doi thu tu cot
   van doc duoc, mien la ba cot bat buoc con do. */

Dấu BOM của UTF-8

/* Excel ghi tep UTF-8 kem BOM: ba byte EF BB BF o dau tep.
   Neu khong bo di thi ten cot dau tien thanh "\xEF\xBB\xBFmssv"
   va moi phep strcmp deu that bai. */

static void bo_bom(char *dong) {
    unsigned char *b = (unsigned char *)dong;

    if (b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF)
        memmove(dong, dong + 3, strlen(dong + 3) + 1);
}

Tự làm thử

  1. Cài csv_ghi_truong và kiểm tra nó với bảy trường hợp ở bảng kết quả trên.
  2. Cài csv_tach và chạy nó với a,,c, ,, và "a,b",c.
  3. Ghi ra rồi đọc lại một trăm bản ghi có chứa dấu phẩy, dấu nháy và khoảng trắng đầu cuối, xác nhận chúng khớp nhau từng ký tự.
  4. So sánh kết quả của strtok và csv_tach trên cùng một dòng có trường rỗng.
  5. Viết bộ tách đầy đủ bằng máy trạng thái ba trạng thái, xử lý được ký tự xuống dòng trong trường.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Ba quy tắc: bọc dấu nháy khi có dấu phẩy, nhân đôi dấu nháy bên trong, và bọc khi có ký tự xuống dòng.
  • Khi ghi thì bọc thừa không bao giờ sai. Bọc cả trường rỗng và trường có khoảng trắng ở đầu hoặc cuối.
  • Bộ tách dùng hai con trỏ, ghi luôn chạy sau đọc, nên giải thoát được tại chỗ mà không cấp phát thêm.
  • strtok gộp dấu phân cách liên tiếp và không hiểu dấu nháy, nên nó làm lệch cột một cách âm thầm.
  • Luôn kiểm tra số cột của mỗi dòng và từ chối dòng sai số cột.