您的位置:首页 > 编程语言 > C语言/C++

自制工具:CSV代码生成器:自动生成CSV文件对应的C++实体类和字段类型解析代码

2015-06-19 16:00 946 查看
本文乃Siliphen原创,转载请注明出处:http://blog.csdn.net/stevenkylelee

更有开发效率地使用CSV文件

为了更有效率地使用CSV文件,我制作了一个工具:Code代码生成器。

这个工具可以对CSV文件进行简单地配置,自动生成这个CSV文件对应的C++数据结构和字段类型解析函数代码。

工程项目只要加入这些自动生成的代码,就可以更方便地使用来自CSV配置文件的数据。

用工具自动生代码,可以省去了手工编写、手工维护那些大量的、无聊繁琐的类型定义、数据转换的代码的过程,

还可以防止手工编程可能的错误。

工具截图如下:



CSV代码生成器的下载地址

http://download.csdn.net/detail/stevenkylelee/8546461

程序的CSV文件夹下有一些CSV文件,可供参考。

回顾一下之前写的CSV类

上次发的CSV解析类:《CSV文件格式解析器的实现:从字符串Split到FSM

/article/7771368.html

资源下载页面评论的反响还不错。

无意中在网上也看到一些人基于我写的CSV类进行扩展、修改、发表博文的,

比如:《Cocos数据篇[3.4](5)
——CSV文件解析》

/article/4603102.html

这哥们貌似对我的类进一步做了封装,支持更灵活、更细粒的数据访问。

自从写了那个CSV解析类后,我就在后来的几个cocos2d-x的项目中一直用它。

为了使用简单方便,我都是在程序运行初时,把所有的csv表读入内存保留,

用的时候查表获取数据,而不是需要时再读取磁盘。

这样做的好处不仅是使用简单,而且也加快了访问速度,

实时读取磁盘的速度太慢,如果csv表加密了的话,还要经过一层解密,

这些都可能会造成延迟卡顿。

若表太大不适合常驻内存,那么可能已经不适合用csv存储了,

应该考虑用Sqlite等数据库。

我的CSV类设计的接口很简单。

主要就是一个Parse解析函数,可以从内存中解析CSV数据。

一个GetGrid函数,返回解析的结果。

解析结果是一个vector< vecotr< string > > 类型的数据结构,

用来模拟二维数组,表示原始的CSV网格数据。

CSV类遵循SRP(单一职责原则),它的用处就是对CSV数据流进行解析。

在实际项目中使用自己的CSV类的总结

最早使用CSV类,我是写了一个函数,

把 vector< vecotr< string > > 原始CSV表数据转换成

unordered_map< string , unordered_map< string , string > > 来用。

这样的数据结构表示了它是一种用Key来访问内容的结构。

最外层的map的key是一个记录的key,

内层的map的key是这条记录的字段名。

图示如下:



对于以上的表,要访问Id为12的记录的TaskName字段的值可以这样做:

[cpp] view
plaincopyprint?





unordered_map< string , unordered_map< string , string > > Table ;

.... // 调用函数,把vector< vector< stirng > > 原始CSV表数据转换成Table的类型。

Table[ "12" ][ "TaskName" ] ; // 访问数据

约定某一列为记录的Key,某一行为字段的Key进行数据转换。

数据转换的好处是,让代码更清晰,适应性更强。

要获取具体的值,不依赖于这个值所在的CSV表中的单元格位置。

只要索引的Key不变,单元格的位置改变是不影响的,

还是可以通过Key索引到内容。

在游戏项目中,经常会通过工厂函数创建出实体。

这些实体会根据配置的数据进行初始化。

我传给工厂Create函数的就是这个实体在CSV表中某条记录的Key,

表示用这条记录来创建实体。

因为之前的2级映射的数据结构是无类型的,所有的字段值都是string类型,

所以,在工厂函数中,就有了大量的数据类型转换函数,

atoi , atof , Split等把数据转换后再填到实体上。

结果,实体工厂函数塞满了大量的类型转换,把string转换成各种不同的类型。

有一天我突然觉得,某些CSV表字段太多,工厂函数实在太长,

每次创建一个实体,都要进行大量的数据类型转换,会影响性能。

转换应该只进行一次。

考虑了一段时间,我觉得应该为每个CSV表手工定义一个数据结构,预先转换好字段的值。

对于像这样的表:



应该有一个对应的数据结构:

例如 一个对应的头文件应该如下

[cpp] view
plaincopyprint?





#include <string>

using namespace std ;

// 道具信息数据结构

class PropInfo

{

public:

// 字段的ID

string Id ;

// 字段的备注

string Remark ;

// 使用说明

int UseTip ;

// 价格

int Price ;

} ;

// 道具信息表。道具信息数据结构的集合

unordered_map< string , Prop > TableProp ;

这个想法冒出后,在新的小项目中,

我就为每个CSV表手工建立了对应的XXX.h头文件,

头文件中,包含了这个表表示的实体class的定义,

并且配套了一个解析转换函数,

把从CSV原始数据中读入的string值转换成各种不同的实体字段的类型,然后对应赋值。

在程序运行初时,就把表类型都转换成对应的专属class数据结构,

原先的Create工厂函数,就消除了对字段做类型转换的职责。

这样干了一段时间后,发现用于配置的CSV表不断增多,

并且时常会在原有的表上进行增加,删除字段。

一直手工增加,维护那些对应的class类,写类定义,写类字段的解析转换代码,

让我感觉有点不太科学。

这些大量、无聊,重复、无技术含量的工作占用了我的精力和时间,

我的精力应该集中在更加高层的设计和算法的实现上。

观察自己写的那些代码,突然想到:这些代码是否可以用工具来自动生成和维护?

身为深圳华强北第一程序猿



拒绝做码工!

拒绝写那些有规律的重复无聊的代码!

几年前在的一家公司就有尝试用过《动软.Net代码生成器》来做项目。

我不妨自己设计一个CSV代码生成器来替我做那些劳动。

CSV代码生成器对CSV表数据的规定

有了做这个工具的想法后,腾出了一些时间,用了几天设计和实现。

并且对表的结构做了一些定义,以便于能让工具正确作用在其上。

表数据有2种风格排列:字段横着排。字段竖着排。

字段横着排如下图:



第2行是字段的注释。

第3行是字段名。

从第4开始往后,是记录。记录是竖着堆叠的。

字段竖着排如下图:



第1列从第2行开始是字段的注释。

第2列从第2行开始是字段名。

从第3列第2行开始,是记录。记录横着向右排列。

这2种风格的表排列都是等价的,只是在Excel中看起来不同。

表的第一行用作保留。可以表示表本身的一些数据。

第一行第一列,目前有2种可能的取值:

FieldOrientation=Landscape

FieldOrientation=Portrait

前者表示CSV表的字段是横向排列的,也就是第一种风格。

后者表示CSV表的字段是纵向排列的,第二种风格。

我写了一个类,输入CSV的原始数据,可以转换成逻辑上的用关键字索引的

unordered_map< string , unordered_map< string , string > > 结构,

内部通过CSV表第一行第一列单元格的内容进行判断。

代码如下:

头文件:

[cpp] view
plaincopyprint?





#pragma once

#include <string>

#include <unordered_map>

#include <vector>

using namespace std ;

typedef unordered_map< string , unordered_map< string , string > > TableWithKey ;

/*

CSV原始网格型数据转换器。

*/

class CsvRawGridDataConvert

{

public:

CsvRawGridDataConvert( );

~CsvRawGridDataConvert( );

public :

//转换成带关键字索引的表

static void ToTableWithKey( const vector< vector< string > >& GridData ,

unordered_map< string , unordered_map< string , string > >& Ret ) ;

private :

/*

处理横向风格的表格

KeyColumnIndex 指定主键列

ColumnHeaderIndex 指定列头的行索引

DataStartIndex 数据列开始的索引

*/

static void ProcessLandscape(

const vector< vector< string > >& GridData ,

unordered_map< string , unordered_map< string , string > >& Ret ,

int KeyColIdx = 0 ,

int HeaderRowIdx = 0 ,

int RecordStartRowIdx = 1

) ;

/*

处理纵向风格的表格

*/

static void ProcessPortrait(

const vector< vector< string > >& GridData ,

unordered_map< string , unordered_map< string , string > >& Ret ,

int KeyRowIdx = 0 ,

int HeaderColIdx = 0 ,

int RecordRowIndex = 1

) ;

};

实现文件:

[cpp] view
plaincopyprint?





#include "CsvRawGridDataConvert.h"

CsvRawGridDataConvert::CsvRawGridDataConvert( )

{

}

CsvRawGridDataConvert::~CsvRawGridDataConvert( )

{

}

void CsvRawGridDataConvert::ToTableWithKey( const vector< vector< string > >& GridData , unordered_map< string , unordered_map< string , string > >& Ret )

{

// 通过 0,0 单元格判断表类型

auto str = GridData[ 0 ][ 0 ] ;

if ( str == "FieldOrientation=Landscape" )

{

ProcessLandscape( GridData , Ret , 0 , 2 , 3 ) ;

}

else if ( str == "FieldOrientation=Portrait" )

{

ProcessPortrait( GridData , Ret , 1 , 1 , 1 ) ;

}

}

void CsvRawGridDataConvert::ProcessLandscape( const vector< vector< string > >& GridData ,

unordered_map< string , unordered_map< string , string > >& Ret ,

int KeyColIdx ,

int HeaderRowIdx ,

int RecordStartRowIdx )

{

Ret.clear( ) ;

// 获取列名

const auto& ColHeader = GridData[ HeaderRowIdx ] ;

for ( size_t row = RecordStartRowIdx ; row < GridData.size( ) ; ++row )

{

const string& Key = GridData[ row ][ KeyColIdx ] ;

auto& Row = Ret[ Key ] ;

for ( size_t col = 0 ; col < GridData[ row ].size( ) ; ++col )

{

const string& ColName = ColHeader[ col ] ;

Row[ ColName ] = GridData[ row ][ col ] ;

}

// end for

}

// end for

}

void CsvRawGridDataConvert::ProcessPortrait( const vector< vector< string > >& GridData ,

unordered_map< string , unordered_map< string , string > >& Ret ,

int KeyRowIdx ,

int HeaderColIdx ,

int RecordRowIndex )

{

Ret.clear( ) ;

for ( size_t row = RecordRowIndex ; row < GridData.size( ) ; ++row )

{

const auto& KeyRecord = GridData[ KeyRowIdx ][ RecordRowIndex ] ;

const string& Key = GridData[ row ][ HeaderColIdx ] ;

for ( size_t col = 0 ; col < GridData[ row ].size( ) ; ++col )

{

Ret[ KeyRecord ][ Key ] = GridData[ row ][ col ] ;

}

}

}

CSV代码生成器的使用

首先点击菜单“CSV文件 -> 打开CSV文件” 打开一个按照上述规定的CSV表。

可以打开多个不同的CSV表,这些CSV表以Tab页的形式排列。

然后可以设置每个表的每个字段的代码生成配置。

如下图:



上图中,可以设置字段的注释,字段的类型,字段的解析函数 等。

然后点“输出 -> 输出代码”

可以把这个CSV文件表示的C++类代码给生成出来。

生成的C++代码如下:

[cpp] view
plaincopyprint?





/*

本代码由“CSV代码生成器”生成。

该软件作者:Siliphen(李锋)

CSDN Blog:http://blog.csdn.net/stevenkylelee

*/

#pragma once

#include "SiliphenCodeGenHeader.h"

class Sprite

{

public:

/*

字段ID

*/

string Id ;

/*

文件名

*/

string FileName ;

/*

位置

*/

Point Position ;

/*

透明度

*/

float Opacity ;

/*

缩放

*/

float Scale ;

/*

本地Z序

*/

int LocalZOrder ;

/*

全局Z序

*/

int GlobalZOrder ;

/*

锚点

*/

Point AnchorPoint ;

};

// CSV数据表转换器

class CsvTableConvertSprite

{

public:

// CSV数据表转换成实体数据表

static void Convert( const unordered_map< string , unordered_map< string , string > >& csvTable , unordered_map< string , Sprite >& Table )

{

const string* pStr = 0 ;

for ( auto it = csvTable.begin( ) , end = csvTable.end( ) ; it != end ; ++it )

{

const auto& Ci = it->second ;

Sprite item ;

pStr = &Ci.find( "Id" )->second ;

Parser::ParseString( *pStr , item.Id ) ;

pStr = &Ci.find( "FileName" )->second ;

Parser::ParseString( *pStr , item.FileName ) ;

pStr = &Ci.find( "Position" )->second ;

Parser::ParsePoint( *pStr , item.Position ) ;

pStr = &Ci.find( "Opacity" )->second ;

Parser::ParseFloat( *pStr , item.Opacity ) ;

pStr = &Ci.find( "Scale" )->second ;

Parser::ParseFloat( *pStr , item.Scale ) ;

pStr = &Ci.find( "LocalZOrder" )->second ;

Parser::ParseInt( *pStr , item.LocalZOrder ) ;

pStr = &Ci.find( "GlobalZOrder" )->second ;

Parser::ParseInt( *pStr , item.GlobalZOrder ) ;

pStr = &Ci.find( "AnchorPoint" )->second ;

Parser::ParsePoint( *pStr , item.AnchorPoint ) ;

Table[ item.Id ] = item ;

} // end for

}

};

以上生成的代码实际上和cocos2d-x的Sprite类名冲突了。

可以设置生成的实体类名,比如,加一个前缀:CiSprite,CfgSprite什么的。

Ci:ConfigItem 配置项。Cfg:Config。

应该为这些自动生成的类加一个统一的前缀或者后缀,防止名字冲突。

我个人比较喜欢前缀的做法,这会让有相同前缀的东西以列表形式在一起显示时排列很整齐。

工具只生成头文件,字段类型解析转换的实现代码也放到头文件中。

Remark字段没生成出来,因为设置中,没有勾选“是否生成代码”。

每个字段从string的解析函数也生成了。

但解析的过程,是调用一些Parser::ParseXXX方法。

每个头文件都会有一句:#include "SiliphenCodeGenHeader.h"。

每次“输出代码”都会复制程序的“Prefabs”文件夹下的所有文件到目标目录中。

这个 SiliphenCodeGenHeader.h 头文件就是从 Prefabs文件夹下复制的。

如果用户想修改SiliphenCodeGenHeader.h 里面的内容,可以到程序的Prefabs文件夹下修改原始模板。

SiliphenCodeGenHeader.h头文件中,会包含#include "Parser.h",

这个 Parser.h 有一些默认的Parser::ParseXXX 方法的实现。我自己编写的默认实现 ^_^

如果用户设置的字段是一种工具不知道的类型,那怎么自动生成代码呢?

比如有一个是UserCustom类型的字段。

在“类型”一栏中输入用户自定义的类型名。

在”解析函数“一栏中输入自己实现的解析函数名。

如下图:



解析函数名的签名应该是:void ( const string& str , 用户自定义类型& Ret )

在工具的Prefabs文件夹下编写MyParser类,

实现 staitcvoid ParseUserCustom(const string& str,UserCustom& Ret ) 函数。

然后在 SiliphenCodeGenHeader.h 中加上一句 #include "MyParser.h" 包含自己写的类的头文件就可以了。

工具会自动在Convert函数中生成调用 MyParser::ParseUserCustom 的语句。

其实,我的工具类似QT的代码生成系统。

输出代码的话,会把所有打开的CSV文件的一并输出代码到目标目录下,如下图:



用户配置完每个CSV文件的每个字段后,希望保存这些配置以便于下次使用。

有时候,一个CSV表增加了一些字段,会想用工具再生成一次代码,

而之前的配置过的字段不想再重新配置。

用户只需要点“配置->保存配置”,保存下当前的配置即可。

工具会记下,当前打开了多少个CSV文件,这些CSV文件的字段是如何配置的。

当一些CSV表结构改变时,从CSV中删除的字段不会在工具中显示,

从CSV表中增加的字段会显示出来,使用默认的配置。

使用工具生成的代码的流程

Setp 1 :

先用指定平台的文件读取函数,把整个CSV文件从磁盘读到内存。

比如C语言的fopen,Win32的CreateFile , QT,cocos2d-x 引擎的文件读取函数。

Setp2 :

使用我的CSV类的Parse方法解析内存的CSV数据流。

也许CSV文件需要加密,用户自己处理解密再把明文数据传给CSV类解析。

这一步会得到CSV原始网格数据,数据结构是:vector< vector< string > >

Setp3 :

把 Setp 2 的结果转换为使用关键字Key来索引数值的结构:

unordered_map< string , unordered_map< string , string > >

使用这种数据结构的代码更易读、易维护。

这一步会把各种不同数据组织的表转换成统一的格式,屏蔽差异方便下一步处理。

上面我提供了一份自己的实现。

Setp4.

把 Setp 3 的结果,传给工具自动生成的解析函数。

这一步会输出另一种指定结构的表,记录的字段有专有的数据类型。

到了这一步,表数据才是程序最终能直接使用的数据类型。

工具其实只是做第4步的自动化工作。

注意,工具要求CSV表具备指定的格式,

也就是上面说的纵向和横向排列的格式。

编写、使用这个工具的意义

在新的小项目中,我用自己的工具,生成了总共1000多行无趣的C++代码。

以后CSV表改变,也是用工具来重新生成一遍代码,不人工修改。

也就是说,表结构的维护也交给工具了。

手工去编写那些代码虽然不难,也可能并不费时。

但会耗掉人的精力和时间。

想象一下,如果某个表增加了一个字段,那么,我们要去修改2个地方:

1.去那个表对应的class中增加一个字段。

2.去那个表对应的转换函数中增加对这个字段的转换代码的调用。

项目的文件稍微多点后,要找到表对应的class文件也需要一点精力。

干嘛要把精力放在这种事上呢,能省一点就算一点。

CSV表的字段注释修改了,那么也要去代码中手工修改那个字段的注释。

用工具只是点点鼠标的事,更加方便维护。

用工具其实更切合数据驱动的编程思想。

类可以通过CSV表的定义自动生成出来,

也就是说,CSV表的设置驱动了类代码的生成。

这刚好就是数据驱动的思想。

总结

自己动手丰衣足食。

写完这个工具蛮高兴的,可以给自己的工作带来些许方便。

使用工具也是一种流程化的建立,流程化是能提高生产力的。

也许这个工具现在还不完善,在以后的实践使用中,再慢慢完善它吧。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: