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的实现其实是偷懒了,将所有单元都加双引号处理。