HOWTO · C++

在 C++ 中讀取 CSV 檔案

本文演示瞭如何在 C++ 中讀取 CSV 檔案

本頁內容

在 C++ 中讀取簡單的逗號分隔檔案時,可以使用 std::getline 一次讀取一行,再以第二次 std::getline 呼叫將該行拆分為欄位。這種做法適用於每個實體行都是一筆記錄,且逗號只用來分隔欄位的情況。下方範例會自行建立一個小型輸入檔,因此不必另外準備資料檔,就能編譯並執行。

使用 std::getline 和 std::istringstream 讀取 CSV 檔案

程式會先將兩筆記錄寫入 grades.csv,再開啟檔案讀取。外層的 std::getline 會將一個實體行讀入 std::string;接著,std::istringstream 將該行轉換為串流,而內層的 std::getline 則使用逗號作為分隔符,逐一擷取欄位。每筆記錄會存成一個 std::vector<std::string>,所有記錄則存放在另一個向量中。如此可保留原有順序;當各列只是依序排列時,也不必使用以整數為鍵的 map。

請執行這個自含式程式。程式碼及其中顯示的標籤與輸出維持英文:

本節中的程式碼以及顯示的標籤或輸出仍為英文。

#include <algorithm>
#include <cctype>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

std::string trimEnds(const std::string& value) {
    const auto first = std::find_if_not(value.begin(), value.end(),
        [](unsigned char ch) { return std::isspace(ch); });
    const auto last = std::find_if_not(value.rbegin(), value.rend(),
        [](unsigned char ch) { return std::isspace(ch); }).base();
    if (first >= last) {
        return "";
    }
    return std::string(first, last);
}

int main() {
    const std::string path = "grades.csv";
    {
        std::ofstream sample(path);
        if (!sample) {
            std::cerr << "Could not create input file\n";
            return 1;
        }
        sample << "Name, Score\nAda Lovelace, 98 \nGrace Hopper, 95\n";
    }

    std::ifstream input(path);
    if (!input) {
        std::cerr << "Could not open input file\n";
        return 1;
    }

    std::vector<std::vector<std::string>> rows;
    std::string record;
    while (std::getline(input, record)) {
        std::istringstream line(record);
        std::vector<std::string> fields;
        std::string field;
        while (std::getline(line, field, ',')) {
            fields.push_back(trimEnds(field));
        }
        rows.push_back(fields);
    }

    for (const auto& row : rows) {
        std::cout << row[0] << " | " << row[1] << '\n';
    }
    return 0;
}

以下是成功執行時提供的結果:

Name | Score
Ada Lovelace | 98
Grace Hopper | 95

建立檔案或開啟檔案失敗時,程式都會將錯誤訊息寫入 std::cerr,並回傳狀態碼 1;正常執行則回傳狀態碼 0。在自己的程式中,讀取前應先確認輸入串流是否成功開啟。如果檔案來自程式外部,請指出檔案路徑,並依應用程式的需求處理開啟失敗;不要假設檔案一定存在。

處理 CSV 欄位中的空白

是否處理空白是資料處理上的選擇,並非拆分欄位時會自動完成的步驟。trimEnds 只會移除每個擷取欄位開頭與結尾的空白,欄位內的空格則會保留,例如 Ada Lovelace 中的空格。呼叫 std::isspace 前先轉換為 unsigned char,可避免將值為負數的普通 char 傳入這個字元分類函式。

有些檔案會把逗號後的空格視為格式排版,另一些則會將它視為欄位內容的一部分。只有在符合你要保留的資料規範時,才應修剪空白。移除所有空白字元也會把姓名或地址中的字詞連在一起;修剪兩端則會改變前置或尾隨空白的內容。如果來源資料中的空白具有意義,請保留讀取到的欄位,或採用更精確的規則,例如只修剪 ASCII 空格。

拆分迴圈也會影響空欄位的處理方式:其他內容之間若有一對相鄰逗號,中間會產生空字串。行尾的逗號不會讓這個 std::getline 迴圈附加最後一個空欄位;如果輸入格式要求保留行尾空欄位,請明確處理。請依預期欄位決定要保留還是拒絕空值。如果某行的欄位數少於標頭列,應在索引該列之前明確處理;上方簡短的顯示迴圈假設兩個欄位都存在。

了解以分隔符解析的限制

這種方法會在每個實體行的每個逗號處切分內容,但不會解析 CSV 引號。例如,"Paris, France" 這樣的值會被切成兩段,儘管 CSV 讀取器應將引號內的文字視為單一欄位。引號欄位內的跳脫引號也不會被解碼,而欄位內的換行則會被誤認為記錄結尾。每筆記錄的欄位數也可能不同,因此不要假設每行的逗號數都相同。

如果輸入內容受控且簡單,並能確保每個實體行只有一筆記錄且不含引號括住的分隔欄位,就可以使用這種做法。對一般 CSV 檔案,請使用能依照實際格式規則處理引號與跳脫方式的 CSV 解析器。完成解析後,仍應驗證應用程式所需欄位的數量與內容。這裡示範的標準串流操作可讀取檔案與行,但本身不會實作完整的 CSV 格式語法。