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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/StevenKyleLee/article/details/44686807

 

本文乃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

http://blog.csdn.net/stevenkylelee/article/details/38440663

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


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

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

http://shahdza.blog.51cto.com/2410787/1614802

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


自从写了那个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字段的值可以这样做:

 

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表手工定义一个数据结构,预先转换好字段的值。

 

对于像这样的表:

 

 

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

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

#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表第一行第一列单元格的内容进行判断。

 

代码如下:


头文件:


#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
		) ;


};

实现文件:


#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++代码如下:

 

/*
本代码由“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表的设置驱动了类代码的生成。

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


总结


自己动手丰衣足食。

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

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

也许这个工具现在还不完善,在以后的实践使用中,再慢慢完善它吧。

 

 

 

 

 

 

猜你喜欢

转载自blog.csdn.net/StevenKyleLee/article/details/44686807