"a,b",c;"He said ""Hi""",c;針對**雙引號包裹、無轉義雙引號、無跨行**的簡化 CSV(常見導出格式),實現流式逐行解析:
// 統計子串出現次數
int count_substr(const char* str, const char* sub) {
int cnt = 0;
for (int i = 0; str[i]; ++i) {
int j = i, k = 0;
for (; str[j] == sub[k] && str[j]; ++j, ++k);
if (!sub[k]) ++cnt;
}
return cnt;
}
// 查找子串從 pos 開始的位置
int find_substr(const char* str, const char* sub, int pos) {
for (int i = pos; str[i]; ++i) {
int j = i, k = 0;
for (; str[j] == sub[k] && str[j]; ++j, ++k);
if (!sub[k]) return i;
}
return -1;
}
// 讀取一行 CSV 到 vector
char* read_csv_line(std::vector& fields, FILE* fp) {
char buf[1024];
char* ret = fgets(buf, sizeof(buf), fp);
if (!ret) return nullptr;
std::string line(buf);
int quote_cnt = count_substr(line.c_str(), "\"\"");
if (quote_cnt > 0) {
int pos = find_substr(line.c_str(), "\"\"", 0);
int start = 1; // 跳過開頭引號
while (pos > 0) {
fields.push_back(line.substr(start, pos - start));
start = pos + 3; // 跳過 "" 及後面的逗號
pos = find_substr(line.c_str(), "\"\"", pos + 2);
}
// 最後一字段(去除結尾引號及換行)
int end = line.find('\n');
if (end == std::string::npos) end = line.size();
fields.push_back(line.substr(start, end - start - 1)); // -1 去尾引號
}
return ret;
}
int main() {
FILE* fp = fopen("data.csv", "r");
if (!fp) return 1;
std::vector row;
while (read_csv_line(row, fp)) {
for (size_t i = 0; i < row.size(); ++i)
std::cout << "Field[" << i << "] = " << row[i] << '\n';
row.clear();
}
fclose(fp);
}
data.csv"aaa","bbb","123"
"ccc","ddd","456"
"eee","fff","789"
| 限制 | 改進 |
|---|---|
| 固定緩衝區 1024 字節 | 動態擴容或用 std::getline |
不支持轉義雙引號 "" | 狀態機解析:進入引號模式,遇 "" 輸出 " |
| 不支持跨行字段 | 狀態機跨 fgets 保持引號狀態 |
| 不支持無引號字段 | 按逗號分割,修剪空白 |
std::getline + 狀態機自寫完整解析器(約 100 行)。手寫 CSV 解析器是理解文本處理、狀態機、流式 IO 的好練習。但**生產代碼請用經過測試的庫**,邊界情況極多(編碼、BOM、異常換行、巨大字段)。本文代碼僅供教學、簡單場景或無依賴環境參考。