【嘮叨】
在一個遊戲中,通常會有很多怪物、關卡、技能等數據,這些數據不可能在代碼裏寫死,一般都會使用配置文件保存,使用時再加載到內存。
CSV文件編輯起來十分簡單,所以常常用來配置遊戲的數據信息。
本文要介紹的是:如何讀取CSV文件格式的數據。
【擴展閱讀】
http://baike.baidu.com/subview/468993/5926031.htm (CSV百度百科)
http://blog.csdn.net/stevenkylelee/article/details/38309147 (CSV文件格式解析器的實現)
http://www.benmutou.com/archives/1634 (Lua 讀取Csv文件)
【CSVParser解析器下載】
https://github.com/shahdza/Cocos_LearningTest/tree/master/CSVParser
【CSV簡介】
這位大牛的博文寫得很好:
http://blog.csdn.net/stevenkylelee/article/details/38309147
1、什麼是CSV?
CSV,即逗號分隔值(Comma-Separated Values)。有時也稱爲字符分隔值,因爲分隔字符也可以不是逗號,可以是分號;),其文件以純文本形式存儲表格數據(數字和文本)。
這種文件格式經常用來作爲不同程序之間的數據交互的格式。
CSV格式數據的結構類似表格,不同的記錄佔用一行,一行中的字段用“,”(逗號)分隔。
例如:
2、編輯CSV文件
在MacOS系統上 ,可以使用 Numbers 來創建表格文件,然後再保存爲csv格式的文件即可。
在Windows系統上,可以使用 Excel 來編輯csv文件。
當然你也可以使用其他csv編輯軟件。
(1)使用Numbers軟件,可以很方便的編輯表格數據。
(2)保存爲csv格式文件。
(3)打開導出的csv文件,可以發現每條數據佔一行,並且每一行的數據用逗號分割。
3、CSV格式規則
(1)開頭是不留空 ,以行爲單位。
(2)每條記錄佔一行,以逗號爲分隔符。列爲空也要表達其存在。
(3)可含或不含列名,如果含列名則居文件第一行。
(4)一行數據不跨行,無空行。
(5)字段中包含有逗號符,該字段必須用雙引號括起來。
(6)字段中包含有換行符,該字段必須用雙引號括起來。
(7)字段前後包含有空格,該字段必須用雙引號括起來。( a b c ==> "a b c")
(8)字段中的雙引號,用兩個雙引號表示。( 我說:"abc"。 ==> 我說:""abc""。 )
(9)字段中如果有雙引號,該字段必須用雙引號括起來。( 我說:"abc"。 ==> "我說:""abc""。" )
PS:中文的逗號、雙引號不需要用雙引號包起來。
可以看看如下的例子:
4、爲什麼使用CSV?
(1)CSV文件格式佔用空間比較小,是文本文件。
(2)CSV文件可以用記事本打開,編輯修改方便。同時也可以用Excel打開。
(3)遊戲項目中,策劃通常喜歡用 Excel 做數值和配置。 Excel可以直接另存爲CSV文件。
(4)配置也可以用xml。Excel同樣可以導出xml文件格式。xml不錯,但C++的標準庫沒有xml的讀取方法。通常C++項目讀xml需要依賴第三方庫。例如:TinyXml之類的。
【CSV解析】
這裏介紹一下C++版本的CSV解析。
注意:CSV文件的編碼格式,如在手遊中一般使用 UTF-8 編碼格式。
PS:笨木頭寫過Lua版讀取CSV數據。參見:http://www.benmutou.com/archives/1634
以下CSV解析的代碼,是在這篇博文的基礎上,進行的修改。
> 參考代碼: http://blog.csdn.net/stevenkylelee/article/details/38309147
使用C/C++標準庫編寫的,不依賴Cocos2d-x裏面的任何一個類或函數。
這樣的好處是,增強了CSV解析的通用性,即使在編寫控制檯應用程序,也可以正常使用。
1、CSVParser.h
(1)命名空間:CSVParser
(2)Row類 :一行的數據記錄。已重載 [] 運算符,可以通過“鍵值對”方式獲取數據值。
(3)Csv類 :解析csv文件。已重載 [] 運算符,可以像數組一樣獲取數據值。
// #pragma once #include <vector> #include <string> using namespace std; namespace CSVParser { // 每一行的記錄 class Row { public: Row() { } ~Row() { } void push_back(const string& value) { m_values.push_back(value); } void setHeader(const vector<string>* header) { m_header = header; } public: // 每行數據有多少字段 unsigned int size() const { return m_values.size(); } // 運算符 [] 重載 string& operator[](unsigned int key) { if (key < size()) return m_values[key]; throw "can't return this value (doesn't exist)"; } // 運算符 [] 重載 string& operator[](const string& key) { vector<string>::const_iterator it; int pos = 0; for (it = (*m_header).begin(); it != (*m_header).end(); it++) { if (key == *it) return m_values[pos]; pos++; } throw "can't return this value (doesn't exist)"; } private: const vector<string>* m_header; vector<string> m_values; }; class Csv { public: Csv(const string& filename); ~Csv(); // 解析csv文件 void Parse(const string& filename); // 錯誤信息 const string& getErrorInfo() const { return m_strErrorInfo; } // 獲取列頭字段 vector<string> getHeader() const { return m_header; } // 獲取總行數 unsigned int getRowCount() const { return m_content.size(); } // 獲取總列數 unsigned int getColumnCount() const { return m_header.size(); } // 運算符 [] 重載 Row& operator[](unsigned int key); private: // 讀取整個文件的數據 void Load(const string& filename, string& Data); // 設置列頭字段,用於[]運算符,以鍵值對方式獲取數據值 void setHeader(); private : // 原始表格數據 vector<Row> m_content; // 所有行的數據(包含列頭) vector<string> m_header; // 列頭字段 // 錯誤信息 string m_strErrorInfo; }; } //
2、CSVParser.cpp
類的實現代碼如下:
// #include "CSVParser.h" namespace CSVParser { Csv::Csv(const string& filename) { Parse(filename); } Csv::~Csv() { } void Csv::Load(const string& filename, string& Data) { // 讀取文件數據 FILE* pFile = fopen(filename.c_str(), "rb"); if (!pFile) { return; } fseek(pFile, 0, SEEK_END); long len = ftell(pFile); char* pBuffer = new char[len + 1]; fseek(pFile, 0, SEEK_SET); fread(pBuffer, 1, len, pFile); fclose(pFile); pBuffer[len] = 0; Data.assign(pBuffer, len); delete[] pBuffer; } void Csv::Parse(const string& filename) { // 清除之前的數據 m_content.clear(); m_strErrorInfo.clear(); string text; Load(filename, text); if (text.size() == 0) { return; } // 定義狀態 enum StateType { NewFieldStart, // 新字段開始 NonQuotesField, // 非引號字段 QuotesField, // 引號字段 FieldSeparator, // 字段分隔 QuoteInQuotesField, // 引號字段中的引號 RowSeparator, // 行分隔符字符1,回車 Error, // 語法錯誤 }; Row Fields = Row(); string strField; // 設置初始狀態 StateType State = NewFieldStart; for (int i = 0, size = text.size(); i < size; ++i) { const char& ch = text[i]; switch (State) { case NewFieldStart: { // 新字段開始 if (ch == '"') { State = QuotesField; } else if (ch == ',') { Fields.push_back(""); State = FieldSeparator; } else if (ch == '\r' || ch == '\n') { m_strErrorInfo = "語法錯誤:有空行"; State = Error; } else { strField.push_back(ch); State = NonQuotesField; } } break; case NonQuotesField: { // 非引號字段 if (ch == ',') { Fields.push_back(strField); strField.clear(); State = FieldSeparator; } else if (ch == '\r') { Fields.push_back(strField); State = RowSeparator; } else { strField.push_back(ch); } } break; case QuotesField: { // 引號字段 if (ch == '"') { State = QuoteInQuotesField; } else { strField.push_back(ch); } } break; case FieldSeparator: { // 字段分隔 if (ch == ',') { Fields.push_back(""); } else if (ch == '"') { strField.clear(); State = QuotesField; } else if (ch == '\r') { Fields.push_back(""); State = RowSeparator; } else { strField.push_back(ch); State = NonQuotesField; } } break; case QuoteInQuotesField: { // 引號字段中的引號 if (ch == ',') { // 引號字段閉合 Fields.push_back(strField); strField.clear(); State = FieldSeparator; } else if (ch == '\r') { // 引號字段閉合 Fields.push_back(strField); State = RowSeparator; } else if (ch == '"') { // 轉義 strField.push_back(ch); State = QuotesField; } else { m_strErrorInfo = "語法錯誤: 轉義字符 \" 不能完成轉義 或 引號字段結尾引號沒有緊貼字段分隔符"; State = Error; } } break; case RowSeparator: { // 行分隔符字符1,回車 if (ch == '\n') { m_content.push_back(Fields); Fields = Row(); // Fields.clear(); strField.clear(); State = NewFieldStart; } else { m_strErrorInfo = "語法錯誤: 行分隔用了回車 \\r。但未使用回車換行 \\r\\n "; State = Error; } } break; case Error: { // 語法錯誤 return; } break; default: break; } } // end for switch (State) { case NewFieldStart: { // Excel導出的CSV每行都以/r/n結尾。包括最後一行 } break; case NonQuotesField: { Fields.push_back(strField); m_content.push_back(Fields); } break; case QuotesField: { m_strErrorInfo = "語法錯誤: 引號字段未閉合"; } break; case FieldSeparator: { Fields.push_back(""); m_content.push_back(Fields); } break; case QuoteInQuotesField: { Fields.push_back(strField); m_content.push_back(Fields); } break; case RowSeparator: { } break; case Error: { } break; default: break; } setHeader(); } void Csv::setHeader() { m_header.clear(); for (int i = 0; i < m_content[0].size(); i++) { m_header.push_back(m_content[0][i]); } for (int i = 0; i < m_content.size(); i++) { m_content[i].setHeader(&m_header); } } Row& Csv::operator[](unsigned int key) { if (key < m_content.size()) return m_content[key]; throw "can't return this row (doesn't exist)"; } } //
3、使用方法
(1)csv文件數據
在MacOS系統上,使用 Numbers 編輯的表格,並導出爲csv格式。
(2)解析csv文件,獲取數據
// #include <stdio.h> #include <iostream> using namespace std; //[1] 引入頭文件、命名空間 #include "CSVParser.h" using namespace CSVParser; int main() { //[2] csv文件完整路徑 string path = "/soft/cocos2d-x-3.4/projects/Demo34/Resources/testCSV.csv"; //[3] 解析csv文件 Csv csv = Csv(path.c_str()); //[4] 獲取總行數(包含列頭)、總列數 printf("總共有 %d 行\n", csv.getRowCount()); printf("總共有 %d 列\n", csv.getColumnCount()); //[5] 獲取所有數據(第0行爲列頭字段) // csv.getRowCount() : 數據總行數(包含列頭) for (int i =0; i < csv.getRowCount(); i++) { // csv[i].size() : 每條數據有多少字段 for (int j = 0; j < csv[i].size(); j++) { printf("%s,", csv[i][j].c_str()); } puts(""); } //[6] 也可以根據列頭名稱,獲取數據 printf("%s\n", csv[2]["備註"].c_str()); //[7] 獲取某一行數據 Row row = csv[4]; printf("%s\n", row["姓名"].c_str()); return 0; } //
(3)運行結果