1. 程式人生 > >自制工具:CSV程式碼生成器:自動生成CSV檔案對應的C++實體類和欄位型別解析程式碼

自制工具:CSV程式碼生成器:自動生成CSV檔案對應的C++實體類和欄位型別解析程式碼

更有開發效率地使用CSV檔案

為了更有效率地使用CSV檔案,我製作了一個工具:Code程式碼生成器。

這個工具可以對CSV檔案進行簡單地配置,自動生成這個CSV檔案對應的C++資料結構和欄位型別解析函式程式碼。

工程專案只要加入這些自動生成的程式碼,就可以更方便地使用來自CSV配置檔案的資料。

用工具自動生程式碼,可以省去了手工編寫、手工維護那些大量的、無聊繁瑣的型別定義、資料轉換的程式碼的過程,

還可以防止手工程式設計可能的錯誤。

工具截圖如下:


CSV程式碼生成器的下載地址

程式的CSV資料夾下有一些CSV檔案,可供參考。

回顧一下之前寫的CSV類

資源下載頁面評論的反響還不錯。

無意中在網上也看到一些人基於我寫的CSV類進行擴充套件、修改、發表博文的,

這哥們貌似對我的類進一步做了封裝,支援更靈活、更細粒的資料訪問。

自從寫了那個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表的設定驅動了類程式碼的生成。

這剛好就是資料驅動的思想。

總結

自己動手豐衣足食。

寫完這個工具蠻高興的,可以給自己的工作帶來些許方便。

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

也許這個工具現在還不完善,在以後的實踐使用中,再慢慢完善它吧。