CSV文件格式解析器的实现:从字符串Split到FSM



  本文分为5小节,基本上就是我刚接触CSV文件到思考、实践做一个CSV解析器的过程的还原。希望我的思路也能带领你一步步从浅到深认识CSV文件格式。

  1.简单的CSV解析器实现。

  2.简单实现的CSV解析器的问题

  3. CSV格式的定义

  4.用FSM(有限状态机)来做CSV格式解析。

  5.为什么使用CSV格式

  1.简单的CSV解析器实现。

  最近有一个需求,读取CSV格式的配置。CSV是CommaSeparated Value(逗号分隔值)的缩写,通常用文本表示数据。CSV格式数据的结构类似表格,不同的记录占用一行,一行中的字段用“,”(逗号)分隔,例如:

  名字,职业,工作经验(年),

  Siliphen Lee,软件工程师(码畜),5

  Edison Chou,游戏服务器端主程,1

  Deson,钢琴教师兼游戏策划,1

  … …

  咋一看,CSV格式比较简单。就是用行来分隔不同的记录,记录中用“,”逗号分隔不同的字段域。仅仅是这样考虑的话,那么编写CSV解析器也很简单了。就是字符串的分割而已。

  好,下面来动手实现下这个思路。C#等语言的字符串都有Split函数,C++的标准库却连这个很常用的函数都没,C++的标准库简直是弱爆了!而就算用了boost,因为boost接近std的风格,类似Split功能的函数,使用起来也比较麻烦。

  没办法,只好自己编写Split来实现CSV的解析了。对于CSV的解析功能,为了实现“组件化”,“复用”的理想,可以单独写一个Csv类,封装一些相关操作。以后在别的工程项目中,也可以直接把这个类拿过去用。

  CSV类,三下五除二,就编写好了。代码如下:


  头文件

  1. #pragmaonce
  2. #include<vector>
  3. #include<string>
  4. usingnamespacestd;
  5. classCsv
  6. {
  7. public:
  8. Csv();
  9. ~Csv();
  10. public:
  11. //载入一个CSV文件
  12. voidLoad(conststring&strFileName);
  13. //从字符串从解析
  14. voidParse(conststring&strText);
  15. public:
  16. /*
  17. 分割字符串
  18. str要分隔的字符串
  19. seperator分隔符
  20. Ret分割后的结果
  21. */
  22. staticvoidSplit(conststring&str,conststring&seperator,vector<string>&Ret);
  23. /*
  24. 读取整个文件的数据
  25. */
  26. staticvoidReadAll(conststring&strFileName,string&Data);
  27. public:
  28. vector<vector<string>>&GetGridData(){returnm_GridData;}
  29. private:
  30. //原始表格数据
  31. vector<vector<string>>m_GridData;
  32. };

  实现文件:
  1. #include"Csv.h"
  2. #include<stdio.h>
  3. Csv::Csv()
  4. {
  5. }
  6. Csv::~Csv()
  7. {
  8. }
  9. voidCsv::ReadAll(conststring&strFileName,string&Data)
  10. {
  11. //读取文件数据
  12. FILE*pFile=fopen(strFileName.c_str(),"rb");
  13. if(pFile==0)
  14. {
  15. return;
  16. }
  17. fseek(pFile,0,SEEK_END);
  18. longlen=ftell(pFile);
  19. char*pBuffer=newchar[len+1];
  20. fseek(pFile,0,SEEK_SET);
  21. fread(pBuffer,1,len,pFile);
  22. fclose(pFile);
  23. pBuffer[len]=0;
  24. Data.assign(pBuffer,len);
  25. delete[]pBuffer;
  26. }
  27. voidCsv::Load(conststring&strFileName)
  28. {
  29. stringData;
  30. ReadAll(strFileName,Data);
  31. Parse(Data);
  32. }
  33. voidCsv::Parse(conststring&strText)
  34. {
  35. //清除之前的数据
  36. m_GridData.clear();
  37. //分出行,分出字段。
  38. vector<string>ret;
  39. Split(strText,"\r\n",ret);
  40. for(size_ti=0;i<ret.size();++i)
  41. {
  42. vector<string>Fields;
  43. Split(ret[i],",",Fields);
  44. m_GridData.push_back(Fields);
  45. }
  46. }
  47. voidCsv::Split(conststring&str,conststring&seperator,vector<string>&Ret)
  48. {
  49. Ret.clear();
  50. size_tnStartPosFound=str.find(seperator,0);
  51. size_tnFieldStart=0;
  52. for(;nStartPosFound!=-1;nStartPosFound=str.find(seperator,nStartPosFound))
  53. {
  54. stringstrSub=str.substr(nFieldStart,nStartPosFound-nFieldStart);
  55. nStartPosFound=nStartPosFound+seperator.size();
  56. nFieldStart=nStartPosFound;
  57. Ret.push_back(strSub);
  58. }
  59. //加入最后一个字段
  60. if(nFieldStart<str.size())
  61. {
  62. stringstrSub=str.substr(nFieldStart,str.size()-nFieldStart);
  63. Ret.push_back(strSub);
  64. }
  65. }


  对代码做一些简要说明。设计Csv类首先考虑的就是“独立性”。Csv类不应该耦合(依赖)任何其他库,比如说:尽量避免使用Cocos2d-x,QT里面的函数。Cocos2d-x有跨平台的文件读取方法,QT有字符串的split函数。如果用了这些库的现成机制,会导致类的通用性下降。比如,在另一个非Cocos2d-x,非QT的项目中,就不能直接用了。

  同样地,从可移植性考虑。用VS编写读取文件,也不应该使用CreateFile, ReadFile等Win32 Api。fopen虽然不太好用,但由于其是C语言标准库的,移植性好,故用之。

  这里有一个问题需要注意下,用VS2013编辑和编译的话,可能会对fopen函数提示有错误。如下:


CSV文件格式解析器的实现:从字符串Split到FSM


  这句英文大概是说:“fopen函数或者变量可能是不安全的。考虑使用fopen_s替代。要屏蔽这个错误,请使用_CRT_SECURE_NO_WARNINGS。”


  _CRT_SECURE_NO_WARNINGS怎样用呢?简单,在“项目属性 -> 配置属性 -> C/C++ -> 命令行 -> 其他选择”上,添加“/D _CRT_SECURE_NO_WARNING”。如下图:


CSV文件格式解析器的实现:从字符串Split到FSM


  添加完后,重新编译。这样,我们的简单的CSV解析器就写好了。运行调试一下,看看是否运行正确


CSV文件格式解析器的实现:从字符串Split到FSM


  OK,没问题。解析的结果,我们放在一个vector< vector< string > > 类型的数据结构中。这个数据结构比较灵活,可以模拟表示一个表格。实际上,vector< vector< string > > 也可以表示可动态调整大小的二维数组。


  2.简单实现的CSV解析器的问题


  之前用字符串处理方法split做的CSV解析器有问题吗?对于简单的数据,没问题。但仔细想下就会发现,字段之间用“,”英文逗号分隔,如果字段数据本身包含了逗号,怎么办?我们用Excel做下实验,看看Excel导出的CSV格式表格是怎样的。

  实验结果如下:


CSV文件格式解析器的实现:从字符串Split到FSM


  从以上结果可以看到,对于包含了“,”字段分隔符的字段,是用“”””双引号把这个字段包围起来。但如果字段数据本身也包含了“””引号,又怎么办呢?继续做实验,如下图


CSV文件格式解析器的实现:从字符串Split到FSM


  实验发现,如果字段数据本身存在“””引号,那么引号的前面也会放一个引号,前置的引号相当于C语言的字符串的转义字符。

  如果用一个字段表示一篇文章,有分段换行。那又是一个什么情况?做实验看看,实验结果如下:


CSV文件格式解析器的实现:从字符串Split到FSM


  复制一段文章或者是有段落的一段文本,然后粘贴到Excel的一个字段中,最后Excel另存为CSV格式文件。用记事本打开那个CSV格式文件。发现用记事本看不到换行,这是什么情况?用UltraEdit查看下二进制数据


CSV文件格式解析器的实现:从字符串Split到FSM


  发现,回车换行(/r/n),会被Excel替换成/n换行。而用记事本程序打开是看不到/n换行效果的。

  如果用记事本程序编辑CSV文件,敲入回车换行(/r/n),然后用Excel打开,会怎样?如下图:


CSV文件格式解析器的实现:从字符串Split到FSM


  可以看到用记事本程序编辑CSV文件,人工按下回车键,会键入回车换行(/r/n)数据。用Excel打开的话,也显示了分行的效果。

  做了一些实验得出的结论是:CSV一个字段的数据是可以表示存在“,”字段分隔符的。如果有逗号字段分隔符存在,那么整个字段就会用双引号包围起来。如果字段数据本身有引号,那么会在之前放一个前置的引号表示转义,并且该字段也会用双引号包围起来。用双引号包围起来的字段数据里面,也可以有回车换行数据。

  这样,问题来了。我们用回车换行来分割出行,再用逗号分割出一行中不同的字段。这种简单的方法无法区分一个逗号是字段本身的数据,还是用来作为字段分隔符的。也无法区分一个回车换行,是字段里面的数据,还是用来分隔行的。


  3. CSV格式的定义


  上面是用Excel另存为CSV文件格式做实验来摸索CSV格式和一些显示特性。其实CSV是有格式规范的。

  关于CSV格式的定义,可以参考这里: 一篇百度文档的CSV格式定义,IETF上的CSV格式定义的文档


  4.用FSM(有限状态机)来做CSV格式解析。


  了解CSV格式的定义后,我们知道,要写一个完善的CSV解析器,不能简单地用字符串的Split方法了。

  我们的目标是:写一个CSV类,能正确解析Excel导出的任何CSV文件!也许有人会认为,用正则表达式可以搞定。用正则有几个问题,第一,C++的正则表达式通常都依赖于一些第三方库,QT,Boost等,C++11的正则也不是在所有的编译器上都实现了,这很大地影响了我们的CSV类通用性。第二,对于语法格式分析,正则不是万能的,至少文章作者本人是很难写出能解析CSV的正则。

  这里介绍一个很强大的方法:FSM(Finite State Machine有限状态机)。我第一次接触FSM是在罗森(KennethH.Rosen)著的《离散数学及其应用》。关于FSM的概念,可以百度下,看看百度百科的解释。也可以看看*的解释:http://zh.wikipedia.org/wiki/%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA

  FSM的应用很广,电路、游戏开发和编译原理等都会涉及。这里,我们就使用FSM来做CSV的解析。使用FSM一般是先画状态图,然后编码实现,调试,修改,反复这个过程。我画的CSV解析FSM状态图如下:


CSV文件格式解析器的实现:从字符串Split到FSM


  从上图,可以看到FSM还可以做语法检查。实际上《编译原理》中的词法分析的扫描器,通常就是基于FSM。这个FSM要解析Excel导出的任何CSV格式文件应该是足够了。为了简化实现,不考虑一些语法容错。

  由于代码较多。我就不贴完整实现代码了。用我们之前实现的简单Csv类来改,就是砍掉Split函数,然后,重写Parse函数。


  大概步骤是:


  先定义状态


  1. //定义状态
  2. enumStateType
  3. {
  4. //新字段开始
  5. NewFieldStart,
  6. //非引号字段
  7. NonQuotesField,
  8. //引号字段
  9. QuotesField,
  10. //字段分隔
  11. FieldSeparator,
  12. //引号字段中的引号
  13. QuoteInQuotesField,
  14. //行分隔符字符1,回车
  15. RowSeparator,
  16. //语法错误
  17. Error,
  18. };

  然后,写一个for循环,从头到尾遍历CSV文件数据,把遍历到的字符“喂给”FSM。FSM用一个Switch-Case语句实现。遍历结束后,再判断下状态,做一些相应处理。算法截图如下:

CSV文件格式解析器的实现:从字符串Split到FSM

  做下测试,读取效果如何。先用Excel构造一个复杂的表格,字段里面包含“, 半角逗号”,““ 引号”,“\r\n 回车换行符”。如下所示:

CSV文件格式解析器的实现:从字符串Split到FSM

  导出CSV文件。用Csv类读一下。结果如下图:

CSV文件格式解析器的实现:从字符串Split到FSM

  OK。大功告成!一个相对完善的Csv类完成了!我们可是用编译原理涉及的技术搞定的哦,赞!

  我的Csv类完整实现(包括整个VS工程和测试CSV文件)可以在这里下载到:http://download.csdn.net/detail/stevenkylelee/7697315

  如果发现我的类读取Excel导出的CSV文件有什么bug。请联系我。我改!:)

  5.为什么使用CSV格式

  我思考一段时间,总结的理由有如下几点:

  1.CSV文件格式占用空间比较小,是文本文件。

  2.CSV文件可以用记事本打开,编辑修改方便。同时也可以用Excel打开。

  3.游戏项目中,策划通常喜欢用Excel做数值和配置。Excel可以直接另存为CSV文件。

  4.配置也可以用xml。Excel同样可以导出xml文件格式。xml不错,但C++的标准库没有xml的读取方法。通常C++项目读xml需要依赖第三方库。例如:TinyXml之类的。

  2016.01.06 更新:

  在把游戏做iOS移植的时候发现,在 Mac OS X 系统下,原先在 windows 创建的 csv 文件会被 mac 系统的某些工具修改:把一行结尾的“\r\n”改成“\n”。因为之前都是针对windows excel 创建的 csv 文件格式进行解析,没有预料到这种情况。所以第一版的 csv 解析器会对仅以“\n”结尾的 csv 格式解析出错。

  初步修改。v2 版本可以解析行结尾是“\n”或“\r\n”的 csv 格式数据。

  下载地址:http://download.csdn.net/detail/stevenkylelee/9393397

  本文分为5小节,基本上就是我刚接触CSV文件到思考、实践做一个CSV解析器的过程的还原。希望我的思路也能带领你一步步从浅到深认识CSV文件格式。

  1.简单的CSV解析器实现。

  2.简单实现的CSV解析器的问题

  3. CSV格式的定义

  4.用FSM(有限状态机)来做CSV格式解析。

  5.为什么使用CSV格式

  1.简单的CSV解析器实现。

  最近有一个需求,读取CSV格式的配置。CSV是CommaSeparated Value(逗号分隔值)的缩写,通常用文本表示数据。CSV格式数据的结构类似表格,不同的记录占用一行,一行中的字段用“,”(逗号)分隔,例如:

  名字,职业,工作经验(年),

  Siliphen Lee,软件工程师(码畜),5

  Edison Chou,游戏服务器端主程,1

  Deson,钢琴教师兼游戏策划,1

  … …

  咋一看,CSV格式比较简单。就是用行来分隔不同的记录,记录中用“,”逗号分隔不同的字段域。仅仅是这样考虑的话,那么编写CSV解析器也很简单了。就是字符串的分割而已。

  好,下面来动手实现下这个思路。C#等语言的字符串都有Split函数,C++的标准库却连这个很常用的函数都没,C++的标准库简直是弱爆了!而就算用了boost,因为boost接近std的风格,类似Split功能的函数,使用起来也比较麻烦。

  没办法,只好自己编写Split来实现CSV的解析了。对于CSV的解析功能,为了实现“组件化”,“复用”的理想,可以单独写一个Csv类,封装一些相关操作。以后在别的工程项目中,也可以直接把这个类拿过去用。

  CSV类,三下五除二,就编写好了。代码如下:

  头文件

  1. #pragmaonce
  2. #include<vector>
  3. #include<string>
  4. usingnamespacestd;
  5. classCsv
  6. {
  7. public:
  8. Csv();
  9. ~Csv();
  10. public:
  11. //载入一个CSV文件
  12. voidLoad(conststring&strFileName);
  13. //从字符串从解析
  14. voidParse(conststring&strText);
  15. public:
  16. /*
  17. 分割字符串
  18. str要分隔的字符串
  19. seperator分隔符
  20. Ret分割后的结果
  21. */
  22. staticvoidSplit(conststring&str,conststring&seperator,vector<string>&Ret);
  23. /*
  24. 读取整个文件的数据
  25. */
  26. staticvoidReadAll(conststring&strFileName,string&Data);
  27. public:
  28. vector<vector<string>>&GetGridData(){returnm_GridData;}
  29. private:
  30. //原始表格数据
  31. vector<vector<string>>m_GridData;
  32. };
  实现文件:
  1. #include"Csv.h"
  2. #include<stdio.h>
  3. Csv::Csv()
  4. {
  5. }
  6. Csv::~Csv()
  7. {
  8. }
  9. voidCsv::ReadAll(conststring&strFileName,string&Data)
  10. {
  11. //读取文件数据
  12. FILE*pFile=fopen(strFileName.c_str(),"rb");
  13. if(pFile==0)
  14. {
  15. return;
  16. }
  17. fseek(pFile,0,SEEK_END);
  18. longlen=ftell(pFile);
  19. char*pBuffer=newchar[len+1];
  20. fseek(pFile,0,SEEK_SET);
  21. fread(pBuffer,1,len,pFile);
  22. fclose(pFile);
  23. pBuffer[len]=0;
  24. Data.assign(pBuffer,len);
  25. delete[]pBuffer;
  26. }
  27. voidCsv::Load(conststring&strFileName)
  28. {
  29. stringData;
  30. ReadAll(strFileName,Data);
  31. Parse(Data);
  32. }
  33. voidCsv::Parse(conststring&strText)
  34. {
  35. //清除之前的数据
  36. m_GridData.clear();
  37. //分出行,分出字段。
  38. vector<string>ret;
  39. Split(strText,"\r\n",ret);
  40. for(size_ti=0;i<ret.size();++i)
  41. {
  42. vector<string>Fields;
  43. Split(ret[i],",",Fields);
  44. m_GridData.push_back(Fields);
  45. }
  46. }
  47. voidCsv::Split(conststring&str,conststring&seperator,vector<string>&Ret)
  48. {
  49. Ret.clear();
  50. size_tnStartPosFound=str.find(seperator,0);
  51. size_tnFieldStart=0;
  52. for(;nStartPosFound!=-1;nStartPosFound=str.find(seperator,nStartPosFound))
  53. {
  54. stringstrSub=str.substr(nFieldStart,nStartPosFound-nFieldStart);
  55. nStartPosFound=nStartPosFound+seperator.size();
  56. nFieldStart=nStartPosFound;
  57. Ret.push_back(strSub);
  58. }
  59. //加入最后一个字段
  60. if(nFieldStart<str.size())
  61. {
  62. stringstrSub=str.substr(nFieldStart,str.size()-nFieldStart);
  63. Ret.push_back(strSub);
  64. }
  65. }

  对代码做一些简要说明。设计Csv类首先考虑的就是“独立性”。Csv类不应该耦合(依赖)任何其他库,比如说:尽量避免使用Cocos2d-x,QT里面的函数。Cocos2d-x有跨平台的文件读取方法,QT有字符串的split函数。如果用了这些库的现成机制,会导致类的通用性下降。比如,在另一个非Cocos2d-x,非QT的项目中,就不能直接用了。

  同样地,从可移植性考虑。用VS编写读取文件,也不应该使用CreateFile, ReadFile等Win32 Api。fopen虽然不太好用,但由于其是C语言标准库的,移植性好,故用之。

  这里有一个问题需要注意下,用VS2013编辑和编译的话,可能会对fopen函数提示有错误。如下:

CSV文件格式解析器的实现:从字符串Split到FSM

  这句英文大概是说:“fopen函数或者变量可能是不安全的。考虑使用fopen_s替代。要屏蔽这个错误,请使用_CRT_SECURE_NO_WARNINGS。”

  _CRT_SECURE_NO_WARNINGS怎样用呢?简单,在“项目属性 -> 配置属性 -> C/C++ -> 命令行 -> 其他选择”上,添加“/D _CRT_SECURE_NO_WARNING”。如下图:

CSV文件格式解析器的实现:从字符串Split到FSM

  添加完后,重新编译。这样,我们的简单的CSV解析器就写好了。运行调试一下,看看是否运行正确

CSV文件格式解析器的实现:从字符串Split到FSM

  OK,没问题。解析的结果,我们放在一个vector< vector< string > > 类型的数据结构中。这个数据结构比较灵活,可以模拟表示一个表格。实际上,vector< vector< string > > 也可以表示可动态调整大小的二维数组。

  2.简单实现的CSV解析器的问题

  之前用字符串处理方法split做的CSV解析器有问题吗?对于简单的数据,没问题。但仔细想下就会发现,字段之间用“,”英文逗号分隔,如果字段数据本身包含了逗号,怎么办?我们用Excel做下实验,看看Excel导出的CSV格式表格是怎样的。

  实验结果如下:

CSV文件格式解析器的实现:从字符串Split到FSM

  从以上结果可以看到,对于包含了“,”字段分隔符的字段,是用“”””双引号把这个字段包围起来。但如果字段数据本身也包含了“””引号,又怎么办呢?继续做实验,如下图

CSV文件格式解析器的实现:从字符串Split到FSM

  实验发现,如果字段数据本身存在“””引号,那么引号的前面也会放一个引号,前置的引号相当于C语言的字符串的转义字符。

  如果用一个字段表示一篇文章,有分段换行。那又是一个什么情况?做实验看看,实验结果如下:

CSV文件格式解析器的实现:从字符串Split到FSM

  复制一段文章或者是有段落的一段文本,然后粘贴到Excel的一个字段中,最后Excel另存为CSV格式文件。用记事本打开那个CSV格式文件。发现用记事本看不到换行,这是什么情况?用UltraEdit查看下二进制数据

CSV文件格式解析器的实现:从字符串Split到FSM

  发现,回车换行(/r/n),会被Excel替换成/n换行。而用记事本程序打开是看不到/n换行效果的。

  如果用记事本程序编辑CSV文件,敲入回车换行(/r/n),然后用Excel打开,会怎样?如下图:

CSV文件格式解析器的实现:从字符串Split到FSM

  可以看到用记事本程序编辑CSV文件,人工按下回车键,会键入回车换行(/r/n)数据。用Excel打开的话,也显示了分行的效果。

  做了一些实验得出的结论是:CSV一个字段的数据是可以表示存在“,”字段分隔符的。如果有逗号字段分隔符存在,那么整个字段就会用双引号包围起来。如果字段数据本身有引号,那么会在之前放一个前置的引号表示转义,并且该字段也会用双引号包围起来。用双引号包围起来的字段数据里面,也可以有回车换行数据。

  这样,问题来了。我们用回车换行来分割出行,再用逗号分割出一行中不同的字段。这种简单的方法无法区分一个逗号是字段本身的数据,还是用来作为字段分隔符的。也无法区分一个回车换行,是字段里面的数据,还是用来分隔行的。

  3. CSV格式的定义

  上面是用Excel另存为CSV文件格式做实验来摸索CSV格式和一些显示特性。其实CSV是有格式规范的。

  关于CSV格式的定义,可以参考这里: 一篇百度文档的CSV格式定义,IETF上的CSV格式定义的文档

  4.用FSM(有限状态机)来做CSV格式解析。

  了解CSV格式的定义后,我们知道,要写一个完善的CSV解析器,不能简单地用字符串的Split方法了。

  我们的目标是:写一个CSV类,能正确解析Excel导出的任何CSV文件!也许有人会认为,用正则表达式可以搞定。用正则有几个问题,第一,C++的正则表达式通常都依赖于一些第三方库,QT,Boost等,C++11的正则也不是在所有的编译器上都实现了,这很大地影响了我们的CSV类通用性。第二,对于语法格式分析,正则不是万能的,至少文章作者本人是很难写出能解析CSV的正则。

  这里介绍一个很强大的方法:FSM(Finite State Machine有限状态机)。我第一次接触FSM是在罗森(KennethH.Rosen)著的《离散数学及其应用》。关于FSM的概念,可以百度下,看看百度百科的解释。也可以看看*的解释:http://zh.wikipedia.org/wiki/%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA

  FSM的应用很广,电路、游戏开发和编译原理等都会涉及。这里,我们就使用FSM来做CSV的解析。使用FSM一般是先画状态图,然后编码实现,调试,修改,反复这个过程。我画的CSV解析FSM状态图如下:

CSV文件格式解析器的实现:从字符串Split到FSM

  从上图,可以看到FSM还可以做语法检查。实际上《编译原理》中的词法分析的扫描器,通常就是基于FSM。这个FSM要解析Excel导出的任何CSV格式文件应该是足够了。为了简化实现,不考虑一些语法容错。

  由于代码较多。我就不贴完整实现代码了。用我们之前实现的简单Csv类来改,就是砍掉Split函数,然后,重写Parse函数。

  大概步骤是:

  先定义状态

  1. //定义状态
  2. enumStateType
  3. {
  4. //新字段开始
  5. NewFieldStart,
  6. //非引号字段
  7. NonQuotesField,
  8. //引号字段
  9. QuotesField,
  10. //字段分隔
  11. FieldSeparator,
  12. //引号字段中的引号
  13. QuoteInQuotesField,
  14. //行分隔符字符1,回车
  15. RowSeparator,
  16. //语法错误
  17. Error,
  18. };


  然后,写一个for循环,从头到尾遍历CSV文件数据,把遍历到的字符“喂给”FSM。FSM用一个Switch-Case语句实现。遍历结束后,再判断下状态,做一些相应处理。算法截图如下:


CSV文件格式解析器的实现:从字符串Split到FSM


  做下测试,读取效果如何。先用Excel构造一个复杂的表格,字段里面包含“, 半角逗号”,““ 引号”,“\r\n 回车换行符”。如下所示:


CSV文件格式解析器的实现:从字符串Split到FSM


  导出CSV文件。用Csv类读一下。结果如下图:


CSV文件格式解析器的实现:从字符串Split到FSM


  OK。大功告成!一个相对完善的Csv类完成了!我们可是用编译原理涉及的技术搞定的哦,赞!

  我的Csv类完整实现(包括整个VS工程和测试CSV文件)可以在这里下载到:http://download.csdn.net/detail/stevenkylelee/7697315

  如果发现我的类读取Excel导出的CSV文件有什么bug。请联系我。我改!:)


  5.为什么使用CSV格式


  我思考一段时间,总结的理由有如下几点:


  1.CSV文件格式占用空间比较小,是文本文件。

  2.CSV文件可以用记事本打开,编辑修改方便。同时也可以用Excel打开。

  3.游戏项目中,策划通常喜欢用Excel做数值和配置。Excel可以直接另存为CSV文件。

  4.配置也可以用xml。Excel同样可以导出xml文件格式。xml不错,但C++的标准库没有xml的读取方法。通常C++项目读xml需要依赖第三方库。例如:TinyXml之类的。


  2016.01.06 更新:


  在把游戏做iOS移植的时候发现,在 Mac OS X 系统下,原先在 windows 创建的 csv 文件会被 mac 系统的某些工具修改:把一行结尾的“\r\n”改成“\n”。因为之前都是针对windows excel 创建的 csv 文件格式进行解析,没有预料到这种情况。所以第一版的 csv 解析器会对仅以“\n”结尾的 csv 格式解析出错。


  初步修改。v2 版本可以解析行结尾是“\n”或“\r\n”的 csv 格式数据。


  下载地址:http://download.csdn.net/detail/stevenkylelee/9393397