CSV格式檔案處理演算法
阿新 • • 發佈:2018-12-27
csv(Comma-Separated Values)是一種用逗號分隔來儲存表資料的格式。用回車來表示換行。例如有下表:
1997 | Ford | E350 | ac,abs,moon | 3000.00 |
1999 | Chevy | Venture "Extended Edition" | 4900.00 | |
1999 | chevy | Venture"Extended Edition, Very Large" | 5000.00 | |
1996 | Jeep | Grand Cherokee | MUST SELL! air moon roof,loaded | 4799.00 |
則儲存為:
1997,Ford,E350,"ac,abs,moon",3000.00 1999,Chevy,Venture?"Extended Edition",,4900.00 1999,chevy,"Venture""Extended Edition, Very Large""",,5000.00 1996,Jeep,Grand Cherokee,"MUST SELL! air moon roof,loaded",4799.00
csv的規則如下:
- 開頭是不留空,以行為單位。
- 可含或不含列名,含列名則居檔案第一行。
- 一行資料不跨行,無空行。若是有換行的資料,需要包含在雙引號內。如上面的MUST SELL
- 以半形逗號(即,)作分隔符,列為空也要表達其存在。
- 列內容如存在半形引號(即"),替換成半形雙引號("")轉義,即用半形引號(即"")將該欄位值包含起來。
- 檔案讀寫時引號,逗號操作規則互逆。
- 內碼格式不限,可為 ASCII、Unicode 或者其他。
- 不支援數字(這條規則我也不是很理解)
- 不支援特殊字元
基於上述的規則有如下的解析程式碼:
std::string tempCsvCell; //臨時單元,存入讀到的單元格資料 TYPE_CSV_ROW tempCsvRow; //代表一行資料 bool bFirstDoubleQuotes = false;//標誌單元的第一個字元是否為雙引號 bool bBeforeIsDoubleQuotes = false;//當前字元的前一個字元是否為雙引號 bool bBeforeIsX0D = false;//當前字元的前一個是否為回車符 int iMaxRowSize(0); //記錄最長那一行的長度。最後面的時候,所有行都按該長度對齊。 //先清空資料 m_data.clear(); //m_data是儲存最終結果的變數 for (int i(0); i < fileContent.size(); i++) //開始逐個字元解析,fileContent是整個檔案的緩衝區 { //讀取一個字元ch char &ch = fileContent.at(i); if (bFirstDoubleQuotes) {//單元是以雙引號開頭,後面可能有特殊字元 if (ch == '"') { bBeforeIsX0D = false; if (bBeforeIsDoubleQuotes) { //按轉義雙引號處理 tempCsvCell.append(1, (char)(ch)); bBeforeIsDoubleQuotes = false; } else { //表示這是一個單獨的雙引號 bBeforeIsDoubleQuotes = true; } } else { if (bBeforeIsDoubleQuotes) { //表示遇到了單獨的雙引號,那取消bFirstDoubleQueotes標誌。遇到逗號或者回車時,就可以入“行”了 bFirstDoubleQuotes = false; } bBeforeIsDoubleQuotes = false; if ('\r' == ch || '\n' == ch){ if (bFirstDoubleQuotes) { //遇到回車換行,但開頭是雙引號,只能當普通字元處理 tempCsvCell.append(1, (char)(ch)); } else if (false == bBeforeIsX0D) { //遇到回車換行,開頭不是雙引號,可以入“行”了,“行”入“表” tempCsvRow.push_back(tempCsvCell); m_data.push_back(tempCsvRow); iMaxRowSize = max(tempCsvRow.size(), iMaxRowSize); tempCsvRow.clear(); tempCsvCell.clear(); bFirstDoubleQuotes = false; } bBeforeIsX0D = (0x0d == ch); } else if (',' == ch) { bBeforeIsX0D = false; if (bFirstDoubleQuotes) { //遇到逗號,但因為開頭是雙引號,它只能當普通逗號處理 tempCsvCell.append(1, (char)(ch)); } else { //遇到逗號,開頭不是雙引號,可以入“行” bBeforeIsX0D = false; tempCsvRow.push_back(tempCsvCell); tempCsvCell.clear(); } } else { //遇到普通字元,直接入“行” bBeforeIsX0D = false; tempCsvCell.append(1, (char)(ch)); } } } else{ if (ch == '"') { bBeforeIsX0D = false; if (tempCsvCell.empty()) { // 空串,第一個是" bFirstDoubleQuotes = true; bBeforeIsDoubleQuotes = false; } else { tempCsvCell.append(1, (char)(ch)); continue; } } else { bBeforeIsDoubleQuotes = false; if ('\r' == ch || '\n' == ch){ if (false == bBeforeIsX0D) { tempCsvRow.push_back(tempCsvCell); m_data.push_back(tempCsvRow); iMaxRowSize = max(tempCsvRow.size(), iMaxRowSize); tempCsvRow.clear(); tempCsvCell.clear(); bFirstDoubleQuotes = false; bBeforeIsDoubleQuotes = false; } else { // 連續\r\n不考慮設定為新的行 } bBeforeIsX0D = (ch == 0x0d); } else if (',' == ch) { bBeforeIsX0D = false; tempCsvRow.push_back(tempCsvCell); tempCsvCell.clear(); } else { bBeforeIsX0D = false; tempCsvCell.append(1, (char)(ch)); } } } } //將最後一部分放入 if (false == tempCsvCell.empty()) { tempCsvRow.push_back(tempCsvCell); m_data.push_back(tempCsvRow); iMaxRowSize = max(tempCsvRow.size(), iMaxRowSize); tempCsvRow.clear(); tempCsvCell.clear(); }
當整個表被讀入後,基於第4條規則(列為空也要保留,表達其存在)。它可以看成一個m*n的矩陣。不存在有參差不齊的情況。如果有,在讀入時也會處理為m*n的對齊格式。後續的操作,也遵循這個規則。
在讀操作方面,有讀列,讀行,讀單元資料,讀列數,讀行數的操作。
在寫方面,有新增行,新增列,刪除行,刪除列,修改單元資料的操作。
標頭檔案《CSVReader.h》定義如下:
#pragma once #include <string> #include <vector> using namespace std; //定義CSV檔案的最大限制,單位位元組 #define MAX_FILE_BUFFER_SIZE 10*1024*1024 /*定義一個異常:越界*/ typedef struct __EXCEPTION_CSV_OUT_OF_RANGE: public std::logic_error { __EXCEPTION_CSV_OUT_OF_RANGE(const string &errMsgDescript) :logic_error(errMsgDescript){} }EXCEPTION_CSV_OUT_OF_RANGE; /*定義行和列資料型別*/ typedef vector<string> TYPE_CSV_ROW; typedef vector<string> TYPE_CSV_COLUMN; class CCSVReader { public: /*構造一個空的csv物件*/ CCSVReader(); ~CCSVReader(); /*讀取一個csv檔案 呼叫該函式,若讀取成功,會覆蓋當前資料 */ bool ReadFile(string sFilePath); /*儲存檔案 sFilePath:要儲存的檔名,可以是絕對路徑或相對路徑。 如果不填,則以ReadFile時的路徑來儲存。但如果沒呼叫過ReadFile,則儲存失敗 */ bool SaveFile(const string &sFilePath = ""); /*返回表格的總行數*/ size_t RowSize(); /*返回表格的總列數*/ size_t ColumnSize(); /*獲取指定位置的資料 iRow:行數,從0開始算起 iCol:列數,從0開始算起 若下標越界,丟擲異常 成功時返回讀取到的資料 */ string GetData(const int &iRow, const int &iCol) throw(EXCEPTION_CSV_OUT_OF_RANGE); /*設定或修改指定位置的資料 iRow:行數,從0開始算起 iCol:列數,從0開始算起 若下標越界,返回失敗false */ bool SetData(const int &iRow, const int &iCol, string &data); /*獲取一行的資料 iPos要獲取的行資料位置,從0開始取 返回:如果引數超有效範圍,丟擲異常 */ TYPE_CSV_ROW GetRow(const int &iPos) throw(EXCEPTION_CSV_OUT_OF_RANGE); /*獲取一列的資料 iPos要獲取的行資料位置,從0開始取 返回:如果引數超有效範圍,丟擲異常*/ TYPE_CSV_COLUMN GetColumn(const int &iPos) throw(EXCEPTION_CSV_OUT_OF_RANGE); /*追加一行資料*/ void AppendRow(TYPE_CSV_ROW &newRow); /*插入一行資料 iRowPos:要插入的位置,取值從0到row.size 若越界,則丟擲異常 */ void InsertRow(const size_t &iRowPos, TYPE_CSV_ROW &newRow) throw(EXCEPTION_CSV_OUT_OF_RANGE); /*追加一列資料*/ void AppendColumn(TYPE_CSV_COLUMN &newCol); /*插入一列資料 iColPos:要插入的位置,取值從0到column.size 若越界,則丟擲異常 */ void InsertColumn(const size_t &iColPos, TYPE_CSV_COLUMN &newCol) throw(EXCEPTION_CSV_OUT_OF_RANGE); /*移除一行資料 iPos:要移除的那一行資料的位置,從0開始取值 */ void RemoveRow(const int &iPos); /*移除一列資料 iPos:要移除的那一列資料的位置,從0開始取值 */ void RemoveColumn(const int &iPos); private: string m_sFilePath; vector< TYPE_CSV_ROW > m_data; private: //將一個單元資料轉換為CSV儲存格式 string forCSV(const string &src){ string ret; for (auto &c : src) { if (c == '"') { ret += "\"\""; } else { ret += c; } } return "\"" + ret + "\""; } //格式化輸出一段文字。緩衝區上限為1024位元組,注意不要超出。 string Format(LPSTR szFormat, ...) { int iBufLen(1024); char *bufTmp(nullptr); //格式化引數 va_list vl; va_start(vl, szFormat); bufTmp = new char[iBufLen]; ZeroMemory(bufTmp, iBufLen); _vsnprintf(bufTmp, iBufLen, szFormat, vl); va_end(vl); string ret(bufTmp); delete[] bufTmp; return ret; } };
實現的《CSVReader.cpp》檔案如下:
#include "stdafx.h"
#include "CSVReader.h"
#include <fstream>
CCSVReader::CCSVReader()
{
}
CCSVReader::~CCSVReader()
{
}
bool CCSVReader::ReadFile(string sFilePath)
{
//檢查檔名長度
if (sFilePath.size() == 0)
{
return false;
}
//去掉檔名兩端的雙引號
if (sFilePath.at(0) == '"')
{
sFilePath.erase(0, 1);
}
if (sFilePath.at(sFilePath.size() - 1) == '"')
{
sFilePath.erase(sFilePath.size() - 1, 1);
}
//開啟檔案
std::ifstream file;
file.open(sFilePath, std::ios::in | ios::binary);
if (file.fail())
{
file.close();
return false;
}
//獲取檔案長度
streampos begin = file.tellg(); //獲得開頭的位置
file.seekg(0, ios::end); //定位到檔案尾部
streampos end = file.tellg(); //獲得尾部的位置
long fileLength = (long)(end - begin); //利用streampos的"-"操作得到檔案的位元組長度
if (fileLength > MAX_FILE_BUFFER_SIZE)
{
file.close();
return false;
}
//讀取檔案內容
vector<char> fileContent(fileLength);
file.seekg(0);
file.read(&(fileContent[0]), fileContent.size());
/*解析演算法的思路如下:
首先明確“單元”的概念,對應於excel的單元格。
一個單元格中的內容如果是普通字元,則在CSV檔案中是直接儲存的。
一個單元格中的內容如果包含有逗號,雙引號,回車這三種特殊字元,則必須用雙引號圍起來。並且雙引號要轉義為兩個雙引號
解析的時候,是以單位格為目標進行的。邏輯思路如下:
1.逐字元讀取,儲存到ch中,並對ch和相關狀態進行判斷處理
2.如果ch是第一個字元,且不為雙引號,可按步驟3來處理.否則到步驟4
3.連續讀取字元到臨時單元中,直到:
遇到逗號就將臨時單元存入“行”中。回到步驟1。
遇到的是回車換行,則將臨時單元存入“行”,再將“行”放入“表”中。回到步驟1
4.連續讀取,遇到任何字元都存入臨時單元中,連續出現的一對雙引號通過轉義為一個雙引號也存入單元中。
直到遇到單獨存在的雙引號。視為單元讀取結束。讀取下一個字元:
若為逗號,則單元存入行中。回到步驟1
若為回車換行,則單元入行,行入表,回到步驟1
若都不是,可報錯。或相容性演算法是繼續納入單元中,直到遇逗號或回車換行。然後回到步驟1
*/
std::string tempCsvCell; //臨時單元,存入讀到的單元格資料
TYPE_CSV_ROW tempCsvRow; //代表一行資料
bool bFirstDoubleQuotes = false;//標誌單元的第一個字元是否為雙引號
bool bBeforeIsDoubleQuotes = false;//當前字元的前一個字元是否為雙引號
bool bBeforeIsX0D = false;//當前字元的前一個是否為回車符
int iMaxRowSize(0); //記錄最長那一行的長度。最後面的時候,所有行都按該長度對齊。
//先清空資料
m_data.clear(); //m_data就是上面所說的表
for (int i(0); i < fileContent.size(); i++) //開始逐個字元解析
{
//讀取一個字元ch
char &ch = fileContent.at(i);
if (bFirstDoubleQuotes) {//單元是以雙引號開頭,後面可能有特殊字元
if (ch == '"') {
bBeforeIsX0D = false;
if (bBeforeIsDoubleQuotes) { //按轉義雙引號處理
tempCsvCell.append(1, (char)(ch));
bBeforeIsDoubleQuotes = false;
}
else { //表示這是一個單獨的雙引號
bBeforeIsDoubleQuotes = true;
}
}
else {
if (bBeforeIsDoubleQuotes) { //表示遇到了單獨的雙引號,那取消bFirstDoubleQueotes標誌。遇到逗號或者回車時,就可以入“行”了
bFirstDoubleQuotes = false;
}
bBeforeIsDoubleQuotes = false;
if ('\r' == ch || '\n' == ch){
if (bFirstDoubleQuotes) { //遇到回車換行,但開頭是雙引號,只能當普通字元處理
tempCsvCell.append(1, (char)(ch));
}
else if (false == bBeforeIsX0D) { //遇到回車換行,開頭不是雙引號,可以入“行”了,“行”入“表”
tempCsvRow.push_back(tempCsvCell);
m_data.push_back(tempCsvRow);
iMaxRowSize = max(tempCsvRow.size(), iMaxRowSize);
tempCsvRow.clear();
tempCsvCell.clear();
bFirstDoubleQuotes = false;
}
bBeforeIsX0D = (0x0d == ch);
}
else if (',' == ch) {
bBeforeIsX0D = false;
if (bFirstDoubleQuotes) { //遇到逗號,但因為開頭是雙引號,它只能當普通逗號處理
tempCsvCell.append(1, (char)(ch));
}
else { //遇到逗號,開頭不是雙引號,可以入“行”
bBeforeIsX0D = false;
tempCsvRow.push_back(tempCsvCell);
tempCsvCell.clear();
}
}
else { //遇到普通字元,直接入“行”
bBeforeIsX0D = false;
tempCsvCell.append(1, (char)(ch));
}
}
}
else{
if (ch == '"') {
bBeforeIsX0D = false;
if (tempCsvCell.empty()) {
// 空串,第一個是"
bFirstDoubleQuotes = true;
bBeforeIsDoubleQuotes = false;
}
else {
tempCsvCell.append(1, (char)(ch));
continue;
}
}
else {
bBeforeIsDoubleQuotes = false;
if ('\r' == ch || '\n' == ch){
if (false == bBeforeIsX0D) {
tempCsvRow.push_back(tempCsvCell);
m_data.push_back(tempCsvRow);
iMaxRowSize = max(tempCsvRow.size(), iMaxRowSize);
tempCsvRow.clear();
tempCsvCell.clear();
bFirstDoubleQuotes = false;
bBeforeIsDoubleQuotes = false;
}
else {
// 連續\r\n不考慮設定為新的行
}
bBeforeIsX0D = (ch == 0x0d);
}
else if (',' == ch) {
bBeforeIsX0D = false;
tempCsvRow.push_back(tempCsvCell);
tempCsvCell.clear();
}
else {
bBeforeIsX0D = false;
tempCsvCell.append(1, (char)(ch));
}
}
}
}
//將最後一部分放入
if (false == tempCsvCell.empty()) {
tempCsvRow.push_back(tempCsvCell);
m_data.push_back(tempCsvRow);
iMaxRowSize = max(tempCsvRow.size(), iMaxRowSize);
tempCsvRow.clear();
tempCsvCell.clear();
}
file.close();
for (int i(0); i < m_data.size(); i++) //對齊所有的行
{
if (m_data[i].size() < iMaxRowSize)
{
m_data[i].resize(iMaxRowSize);
}
}
return true;
}
bool CCSVReader::SaveFile(const string &sFilePath/* = ""*/)
{
string tmpFilePath = sFilePath.size() ? sFilePath : m_sFilePath;
if (tmpFilePath.size() == 0)
{
return false;
}
//寫入檔案
std::ofstream fileOut;
fileOut.open(tmpFilePath, ios::out | ios::trunc);
if (fileOut.bad())
{
return false;
}
for (auto &row : m_data)
{
for (int i(0); i < row.size(); i++)
{
//轉換為csv格式的內容
fileOut << forCSV(row[i]) << (i == row.size() - 1 ? "" : ",");
}
fileOut << endl;
}
fileOut.close();
return true;
}
/*返回表格的總行數*/
size_t CCSVReader::RowSize()
{
return m_data.size();
}
/*返回表格的總列數*/
size_t CCSVReader::ColumnSize()
{
if (m_data.size() == 0)
{
return 0;
}
return m_data[0].size();
}
/*獲取指定位置的資料
iRow:行數,從0開始算起
iCol:列數,從0開始算起
若索引溢位,丟擲異常
*/
string CCSVReader::GetData(const int &iRow, const int &iCol)
{
if (iRow >= m_data.size() || iCol >= m_data[iRow].size())
{
throw EXCEPTION_CSV_OUT_OF_RANGE(Format("下標[%d, %d]越界", iRow, iCol));
}
else
{
return m_data[iRow][iCol];
}
}
/*設定指定位置的資料
iRow:行數,從0開始算起
iCol:列數,從0開始算起
若索引溢位,返回false
*/
bool CCSVReader::SetData(const int &iRow, const int &iCol, string &data)
{
if (iRow < 0 || iCol < 0 || m_data.size() <= iRow || m_data[iRow].size() <= iCol)
{
return false;
}
m_data[iRow][iCol] = data;
return true;
}
/*獲取一行的資料
iPos要獲取的行資料位置,從0開始取
返回:如果引數超有效範圍,丟擲異常
*/
TYPE_CSV_ROW CCSVReader::GetRow(const int &iPos){
if (iPos < 0 || iPos >= m_data.size())
{
throw EXCEPTION_CSV_OUT_OF_RANGE(Format("行下標[%d]越界", iPos));
}
return m_data[iPos];
}
/*獲取一列的資料
iPos要獲取的行資料位置,從0開始取
返回:如果引數超有效範圍,丟擲異常*/
TYPE_CSV_COLUMN CCSVReader::GetColumn(const int &iPos)
{
if (iPos < 0 || m_data.size() == 0 || iPos >= m_data[0].size())
{
throw EXCEPTION_CSV_OUT_OF_RANGE(Format("列索引[%d]越界", iPos));
}
TYPE_CSV_COLUMN col;
for (auto &row : m_data)
{
col.push_back(row[iPos]);
}
return col;
}
/*追加一行資料*/
void CCSVReader::AppendRow(TYPE_CSV_ROW &newRow)
{
InsertRow(m_data.size(), newRow);
}
void CCSVReader::InsertRow(const size_t &iRowPos, TYPE_CSV_ROW &newRow)
{
if (iRowPos < 0 || iRowPos >= m_data.size())
{
throw EXCEPTION_CSV_OUT_OF_RANGE(Format("行下標[%d]越界", iRowPos));
}
//要把每一行的資料長度同步為 max(newRow.size(), mdata[0].size())
if (m_data.size() != 0 && m_data[0].size() > newRow.size()) //擴充套件newRow的長度
{
newRow.resize(m_data[0].size());
}
else if (m_data.size() != 0 && m_data[0].size() < newRow.size()) //擴充套件現有行的長度
{
for (int i(0); i < m_data.size(); i++)
{
m_data[i].resize(newRow.size());
}
}
//如果一行都沒有則插在開頭位置
//如果溢位則在最後,否則計算指定位置
auto iter = (m_data.size() == 0) ? m_data.begin() :
(iRowPos == m_data.size() ? m_data.end() : m_data.begin() + iRowPos);
m_data.insert(iter, newRow);
}
/*追加一列資料*/
void CCSVReader::AppendColumn(TYPE_CSV_COLUMN &newCol)
{
InsertColumn(m_data[0].size(), newCol);
}
/*插入一列資料
iColPos:要插入資料的位置
插入資料會影響到現有的行數。函式會將行數同步為max(newCol.size(), mdata.size())
*/
void CCSVReader::InsertColumn(const size_t &iColPos, TYPE_CSV_COLUMN &newCol)
{
if (iColPos < 0
||
(m_data.size() == 0 && iColPos != 0)
||
(m_data.size() != 0 && iColPos > m_data[0].size())
)
{
throw EXCEPTION_CSV_OUT_OF_RANGE(Format("列索引[%d]越界", iColPos));
}
int iCurRowLength = m_data.size() == 0 ? 0 : m_data[0].size();//追加新行的初始長度
for (int i(0); i < max(newCol.size(), m_data.size()); i++)
{
//獲取要插入本行的資料,如果已經超出newCol範圍,則插入空字串
string value = i < newCol.size() ? newCol[i] : "";
//檢查現有的行數,不足要追加空行
if (m_data.size() <= i)
{
TYPE_CSV_ROW newRow(iCurRowLength);
m_data.push_back(newRow);
}
auto iter = iColPos < m_data[i].size() ? (m_data[i].begin() + iColPos) : m_data[i].end();
m_data[i].insert(iter, value);
}
}
/*移除一行資料
iPos:要移除的那一行資料的位置,從0開始取值
若索引溢位,則執行結果無效
*/
void CCSVReader::RemoveRow(const int &iPos)
{
if (iPos >= 0 && iPos < m_data.size())
{
m_data.erase(m_data.begin() + iPos);
}
}
/*移除一列資料
iPos:要移除的那一列資料的位置,從0開始取值
*/
void CCSVReader::RemoveColumn(const int &iPos)
{
for (int i(0); i < m_data.size(); i++)
{
if (iPos >= 0 && iPos < m_data[i].size()){
m_data[i].erase(m_data[i].begin() + iPos);
}
}
}
使用時,將這兩個檔案放入工程中,然後引用標頭檔案就可以了。如果VS報C4996警告,請在工程中忽略該警告即可。
或者可以從這個地方下載:https://download.csdn.net/download/learner_/10516197
PS:forCSV的實現其實是偷懶了,將所有單元都加雙引號處理。