1. 程式人生 > >CSV格式檔案處理演算法

CSV格式檔案處理演算法

    csv(Comma-Separated Values)是一種用逗號分隔來儲存表資料的格式。用回車來表示換行。例如有下表:

1997FordE350ac,abs,moon3000.00
1999ChevyVenture "Extended Edition"4900.00
1999chevyVenture"Extended Edition, Very Large"5000.00
1996JeepGrand 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的規則如下:
  1. 開頭是不留空,以行為單位。
  2.  可含或不含列名,含列名則居檔案第一行。
  3.  一行資料不跨行,無空行。若是有換行的資料,需要包含在雙引號內。如上面的MUST SELL
  4.  以半形逗號(即,)作分隔符,列為空也要表達其存在。
  5. 列內容如存在半形引號(即"),替換成半形雙引號("")轉義,即用半形引號(即"")將該欄位值包含起來。
  6. 檔案讀寫時引號,逗號操作規則互逆。
  7. 內碼格式不限,可為 ASCII、Unicode 或者其他。
  8. 不支援數字(這條規則我也不是很理解)
  9. 不支援特殊字元

基於上述的規則有如下的解析程式碼:

	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的實現其實是偷懶了,將所有單元都加雙引號處理。