1. 程式人生 > >【Unity3D】 讀寫 CSV 資料表

【Unity3D】 讀寫 CSV 資料表

【先說點廢話】

哈哈哈哈好久沒發文章不知道大家有沒有想我,這一大段時間鬼知道我經歷了什麼,弄比賽、備戰考研、各種求職各種做簡歷、弄畢業設計、租房子。。。等等等,做了一大堆都沒做太好哈哈哈,不過好在找到了心儀的工作,之後會繼續保持更新,把一些技術分享給大家,一起進步一起學習。以後如果有時間也特別想分享一下我這段日子的經歷,十分的難能可貴。好了話不多說,接下來進入正題吧!

【需要了解的知識基礎】

什麼是CSV檔案?

逗號分隔值(Comma-Separated Values,CSV,有時也稱為字元分隔值,因為分隔字元也可以不是逗號),其檔案以純文字形式儲存表格資料(數字和文字)。純文字意味著該檔案是一個字元序列,不含必須像二進位制數字那樣被解讀的資料。——

百度百科

簡單來說,CSV 檔案可以直接通過檔案內容來獲取表格資料。那逗號分割值是什麼意思呢?比如我們需要學生的資料表,包含學生的學號,姓名,性別等級這些資料,通常我們會用 Excel 來記錄,它的表現形式類似於下表:

學號 姓名 性別
1001 張三 boy
1002 李四 girl
1003 王五 none

(請不要問我none是什麼性別(●ˇ∀ˇ●))

那 CSV 檔案的形式又是什麼樣子呢?我們在 Excel 中做一個如上表格,然後另存為 CSV 格式檔案,用 NotePad++ 開啟,會發現他是如下樣式

學號,姓名,性別
1001
,張三,boy 1002,李四,girl 1003,王五,none

可以看到檔案中所有的值都主要以逗號分隔開,每一行資料攜帶換行符。(當然,逗號不一定是唯一的分割方式,但是是比較常用。這裡以“,”這個字元分割為例)

為什麼要使用CSV檔案?

目前總結出來的優點就有以下三點,但是在認識優點的同時也要認識到這種方式所存在的不足,權衡利弊後進行選擇。

優點:
1. 結構簡單,易於理解;
2. 解析文字和還原文字的方式較為簡潔高效;
3. 可以輕鬆轉換為 Excel 的 .xls 檔案,亦可以利用 Excel 以表格的方式進行查閱。相比 .xls 檔案,其本身由於只儲存文字而不包含表格中的公式等其他附帶資訊,在相同的檔案內容下 CSV 檔案可以具有更小的檔案體積。

缺點:
1. 相比於二進位制檔案,由於是純文字儲存,體積會比較大;
2. 雖然由於資料格式參差不齊,具備基本的安全性,但破解的風險依舊很高。

使用CSV檔案要注意什麼?

  1. 可以在 Excel 中建立儲存為 CSV 檔案,但是後續對 CSV 檔案操作最好用 Notepad++ 等文字編輯器來開啟,最好使用 Notepad++,詳細原因看了之後的幾點就懂啦;
  2. 使用 Notepad++ 開啟 CSV 檔案後,需要將其轉碼為 UTF-8 格式,這樣才能保證檔案中的中文被正確顯示,而 Excel 儲存的檔案均不是 UTF-8 編碼格式的;
  3. 在 Notepad++ 中開啟 CSV 檔案後,會發現多了一個空行,這是 Excel 的儲存所導致的。我個人習慣把這個空行刪掉,以便於我程式中計算檔案中的真實行數。

【又到了緊張刺激的分析環節了】

沒有勇氣看程式碼? 可能分析你都沒有勇氣看完。。。

表的結構分析

在準備環節中已經對 CSV 檔案有了初步的介紹,簡短的總結下 CSV 檔案的結構:
1. 第一行是表中資料物件包含的所有屬性的屬性名稱,即鍵名;
2. 從第二行開始,每一行都是一個獨立的資料物件,包含了所有屬性的屬性值;
3. 值與值之間使用逗號分隔。

資料模型分析

主鍵:用於唯一標識一個物件的鍵值,比如學號可以唯一找到一條學生資料,假設這個學生的學號是12345,那麼“學號”就是主鍵的名稱,而“12345”就是主鍵值,一般簡稱為主鍵。——by 亦澤

資料物件類:

  1. 資料物件類應提供所有屬性值的讀取功能,而除主鍵之外還應提供所有的屬性值的寫入功能,以便對資料物件的讀寫操作。
  2. 我們需要通過屬性名稱來獲取對應的屬性值。基於這樣的鍵值關係,我們需要使用 Dictionary(即字典)型別來儲存所有的屬性名稱和對應的屬值;
  3. 構造物件的三個引數:
    (1) 由於主鍵值為只讀模式,所以需要在構造資料物件時來設定主鍵值,並提供一個屬性供外部訪問其主鍵;
    (2) 由於所有的鍵值(除主鍵)可以修改,而鍵名不可以修改或者增加刪除,所以所有的鍵值對兒(除主鍵)也需要在構造物件時初始化;
    (3) 由於第二個引數中不包含主鍵的鍵名,但是要保證資料物件的標籤(用所有的鍵名有序構造)是完整的,所以第三個引數要傳入所有的鍵名組成的陣列來構造標籤。
  4. 為了使資料的讀寫更簡單高效,可改寫 this 關鍵來構建與 Dictionary 類類似的讀寫方式,其應隱式包含 GetValue 和 SetValue 兩個方法;
  5. 重寫 ToString() 方法,提供物件的資料內容便於測試;
  6. 繼承 IEnumerable 介面,實現遍歷資料物件中所有鍵值對的方法。

資料表類:

  1. 資料表類應該提供所有資料物件的讀取功能,所有的資料物件應該都是隻讀的,但是以資料物件為單位新增或者刪除。
  2. 可以通過每條資料的主鍵來訪問到具體的資料物件,從而訪問該資料物件的其他屬性值;
  3. 與資料物件類相似的,可使用改寫 this 關鍵字來提供更簡單高效的讀寫模式;
  4. 資料表物件應該具有但不限於增、刪、改、查四個功能;
  5. 資料表物件應具有獲取標籤的方法,即在構造資料表物件時記錄資料表所有的鍵名,並提供獲取簽名的方法。該方法的演算法應與資料物件簽名的演算法保持一致;
  6. 資料表物件應具有將抽象資料結構轉換為文字字串的方法共外部使用,方便外部直接獲取資料物件的所對應的文字值而無須考慮其中的演算法;
  7. 資料表類應提供靜態方法通過表名和檔案內容用於構造資料表物件,方法內實現將文字內容解析為資料表物件的演算法並返回;
  8. 基於上述的需求,構造資料表物件時應該提供兩個引數:表名和鍵名陣列;
  9. 重寫 ToString() 方法,提供資料表的字串形式用於測試;
  10. 繼承 IEnumerable 介面,實現遍歷資料表中所有資料物件的方法。

讀、寫演算法分析

好了,寫了一大推,有點累,但是還沒完,我們還要分析一下主要演算法是如何實現的,其他方法就不分析了主要是一些邏輯校驗之類的,到時候直接看程式碼就好啦。廢話不多說,繼續繼續~

我們再來重新溫習一下所涉及到的檔案格式,一般策劃給的都是基於 Excel 的類似於下面這種格式(從這裡開始的表需要記住,在程式碼中我會使用這個表來進行測試)

編號 姓名 年齡 性別
1001 張三 20
1002 李四 20
1003 王五 12 不詳
1004 ABC 100 male

當我們改存為 CSV 檔案時,就會變成這個樣子(別忘了之前講過的幾點注意事項!)

編號,姓名,年齡,性別
1001,張三,20,男
1002,李四,40,女
1003,王五,12,不詳
1004,ABC,100,male

文字字串轉化為資料表物件

首先我們觀察下我們的文字內容,一共有五行,第一行是所有的鍵名,第二行開始每一行都是一個數據物件,對應著鍵名都有四個屬性。那麼我們可以先把他們以行為單位進行拆分,得到一個關於行的陣列,第一行單獨用作鍵名陣列,從第二行開始,每一行構造一個數據物件,然後儲存在資料表物件當中。

資料表物件轉化為文字字串

相當於反過來思考,首先鍵名行是比較特殊的,單獨拿出來寫;從第二行開始,我們可以迴圈去將所有資料物件的所有屬性值拼接出來寫入。

檔案的讀寫

可以使用 System.IO 中的方法來操作。我這裡採用的是 Stream 的方式來操作,因為文字中含有中文,所以使用 Stream 搭配 File 類中靜態方法的方式時較為簡單。

【程式碼來啦!】

資料物件類 CSVDataObject


// ------------------------------ //
// Product Name : CSV_Read&Write
// Company Name : MOESTONE
// Author  Name : Eazey Wang
// Create  Data : 2017/12/16
// ------------------------------ //

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class CSVDataObject : IEnumerable
{
    /// <summary>
    /// 此值作為資料物件的唯一標識,只能通過此屬性獲取到唯一標識
    /// 無法通過 '資料物件[主鍵名]' 的方式來獲取
    /// </summary>
    public string ID { get { return _major; } }
    private readonly string _major;

    /// <summary>
    /// 一條資料應包含的所有的鍵名
    /// </summary>
    public string[] AllKeys { get { return _allKeys; } }
    private readonly string[] _allKeys;

    private Dictionary<string, string> _atrributesDic;

    /// <summary>
    /// 初始化,獲取唯一標識與除主鍵之外所有屬性的鍵與值
    /// </summary>
    /// <param name="major"> 唯一標識,主鍵 </param>
    /// <param name="atrributeDic"> 除主鍵值外的所有屬性鍵值字典 </param>
    public CSVDataObject(string major, Dictionary<string, string> atrributeDic, string[] allKeys)
    {
        _major = major;
        _atrributesDic = atrributeDic;
        _allKeys = allKeys;
    }

    /// <summary>
    /// 獲取資料物件的簽名,用於比較是否與資料表的簽名一致
    /// </summary>
    /// <returns> 資料物件的簽名 </returns>
    public string GetFormat()
    {
        string format = string.Empty;
        foreach (string key in _allKeys)
        {
            format += (key + "-");
        }
        return format;
    }

    public string this[string key]
    {
        get { return GetValue(key); }
        set { SetKey(key, value); }
    }

    private void SetKey(string key, string value)
    {
        if (_atrributesDic.ContainsKey(key))
            _atrributesDic[key] = value;
        else
            Debug.LogError("The data not include the key.");
    }

    private string GetValue(string key)
    {
        string value = string.Empty;

        if (_atrributesDic.ContainsKey(key))
            value = _atrributesDic[key];
        else
            Debug.LogError("The data not include value of this key.");

        return value;
    }

    public override string ToString()
    {
        string content = string.Empty;

        if (_atrributesDic != null)
        {
            foreach (var item in _atrributesDic)
            {
                content += (item.Key + ": " + item.Value + ".  ");
            }
        }
        return content;
    }

    public IEnumerator GetEnumerator()
    {
        foreach (var item in _atrributesDic)
        {
            yield return item;
        }
    }
}

資料表類 CSVTable


// ------------------------------ //
// Product Name : CSV_Read&Write
// Company Name : MOESTONE
// Author  Name : Eazey Wang
// Create  Data : 2017/12/16
// ------------------------------ //

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CSVTable : IEnumerable
{
    /// <summary>
    /// 獲取表名
    /// </summary>
    public string Name { get { return _name; } }
    private string _name;

    /// <summary>
    /// 獲取表中的所有屬性鍵
    /// </summary>
    public List<string> AtrributeKeys { get { return _atrributeKeys; } }
    private List<string> _atrributeKeys;

    /// <summary>
    /// 儲存表中所有資料物件
    /// </summary>
    private Dictionary<string, CSVDataObject> _dataObjDic;

    /// <summary>
    /// 構造方法
    /// </summary>
    /// <param name="tableName"> 表名 </param>
    public CSVTable(string tableName, string[] attributeKeys)
    {
        _name = tableName;

        // init 
        _atrributeKeys = new List<string>(attributeKeys);
        _dataObjDic = new Dictionary<string, CSVDataObject>();
    }

    /// <summary>
    /// 獲取資料表物件的簽名,用於比較是否與資料物件的簽名一致
    /// </summary>
    /// <returns> 資料表物件的簽名 </returns>
    public string GetFormat()
    {
        string format = string.Empty;
        foreach (string key in _atrributeKeys)
        {
            format += (key + "-");
        }
        return format;
    }

    /// <summary>
    /// 提供類似於鍵值對的訪問方式便捷獲取和設定資料物件
    /// </summary>
    /// <param name="key"> 資料物件主鍵 </param>
    /// <returns> 資料物件 </returns>
    public CSVDataObject this[string dataMajorKey]
    {
        get { return GetDataObject(dataMajorKey); }
        set { AddDataObject(dataMajorKey, value); }
    }

    /// <summary>
    /// 新增資料物件, 並將資料物件主鍵新增到主鍵集合中
    /// </summary>
    /// <param name="dataMajorKey"> 資料物件主鍵 </param>
    /// <param name="value"> 資料物件 </param>
    private void AddDataObject(string dataMajorKey, CSVDataObject value)
    {
        if (dataMajorKey != value.ID)
        {
            Debug.LogError("所設物件的主鍵值與給定主鍵值不同!設定失敗!");
            return;
        }

        if (value.GetFormat() != GetFormat())
        {
            Debug.LogError("所設物件的的簽名與表的簽名不同!設定失敗!");
            return;
        }

        if (_dataObjDic.ContainsKey(dataMajorKey))
        {
            Debug.LogError("表中已經存在主鍵為 '" + dataMajorKey + "' 的物件!設定失敗!");
            return;
        }

        _dataObjDic.Add(dataMajorKey, value);
    }

    /// <summary>
    /// 通過資料物件主鍵獲取資料物件
    /// </summary>
    /// <param name="dataMajorKey"> 資料物件主鍵 </param>
    /// <returns> 資料物件 </returns>
    private CSVDataObject GetDataObject(string dataMajorKey)
    {
        CSVDataObject data = null;

        if (_dataObjDic.ContainsKey(dataMajorKey))
            data = _dataObjDic[dataMajorKey];
        else
            Debug.LogError("The table not include data of this key.");

        return data;
    }

    /// <summary>
    /// 根據資料物件主鍵刪除對應資料物件
    /// </summary>
    /// <param name="dataMajorKey"> 資料物件主鍵 </param>
    public void DeleteDataObject(string dataMajorKey)
    {
        if (_dataObjDic.ContainsKey(dataMajorKey))
            _dataObjDic.Remove(dataMajorKey);
        else
            Debug.LogError("The table not include the key.");     
    }

    /// <summary>
    /// 刪除所有所有資料物件
    /// </summary>
    public void DeleteAllDataObject()
    {
        _dataObjDic.Clear();
    }

    /// <summary>
    /// 獲取資料表物件的文字內容
    /// </summary>
    /// <returns> 資料表文本內容 </returns>
    public string GetContent()
    {
        string content = string.Empty;

        foreach(string key in _atrributeKeys)
        {
            content += (key + ",").Trim();
        }
        content = content.Remove(content.Length - 1);

        if (_dataObjDic.Count == 0)
        {
            Debug.LogWarning("The table is empty, fuction named 'GetContent()' will just retrun key's list.");
            return content;
        } 

        foreach (CSVDataObject data in _dataObjDic.Values)
        {
            content += "\n" + data.ID + ",";
            foreach (KeyValuePair<string,string> item in data)
            {
                content += (item.Value + ",").Trim();
            }
            content = content.Remove(content.Length - 1);
        }

        return content;
    }  

    /// <summary>
    /// 迭代表中所有資料物件
    /// </summary>
    /// <returns> 資料物件 </returns>
    public IEnumerator GetEnumerator()
    {
        if (_dataObjDic == null)
        {
            Debug.LogWarning("The table is empty.");
            yield break;
        }

        foreach (var data in _dataObjDic.Values)
        {
            yield return data;
        }
    }

    /// <summary>
    /// 獲得資料表內容
    /// </summary>
    /// <returns> 資料表內容 </returns>
    public override string ToString()
    {
        string content = string.Empty;

        foreach(var data in _dataObjDic.Values)
        {
            content += data.ToString() + "\n";
        }

        return content;
    }

    /// <summary>
    /// 通過資料表名字和資料表文本內容構造一個數據表物件
    /// </summary>
    /// <param name="tableName"> 資料表名字 </param>
    /// <param name="tableContent"> 資料表文本內容 </param>
    /// <returns> 資料表物件 </returns>
    public static CSVTable CreateTable(string tableName, string tableContent)
    {
        string content = tableContent.Replace("\r", "");
        string[] lines = content.Split('\n');
        if (lines.Length < 2)
        {
            Debug.LogError("The csv file is not csv table format.");
            return null;
        }

        string keyLine = lines[0];
        string[] keys = keyLine.Split(',');
        CSVTable table = new CSVTable(tableName, keys);

        for (int i = 1; i < lines.Length; i++)
        {
            string[] values = lines[i].Split(',');
            string major = values[0].Trim();
            Dictionary<string, string> tempAttributeDic = new Dictionary<string, string>();
            for (int j = 1; j < values.Length; j++)
            {
                string key = keys[j].Trim();
                string value = values[j].Trim();
                tempAttributeDic.Add(key, value);
            }
            CSVDataObject dataObj = new CSVDataObject(major, tempAttributeDic, keys);
            table[dataObj.ID] = dataObj;
        }

        return table;
    }
}

資料管理類 DataManager


// ------------------------------ //
// Product Name : CSV_Read&Write
// Company Name : MOESTONE
// Author  Name : Eazey Wang
// Create  Data : 2017/12/18
// ------------------------------ //

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.IO;
using System;


public class DataManager : MonoBehaviour
{
    string _loadPath;
    string _savePath;

    [SerializeField] private string _fileName = "TestTable";
    private const string EXTENSION = ".csv";

    [SerializeField] private Button _saveBtn;
    [SerializeField] private Button _loadBtn;
    [SerializeField] private Text _display;

    private CSVTable _table;

    // Bind Component
    void Awake()
    {
        _loadPath = Application.dataPath + "/Load/";
        _savePath = Application.dataPath + "/Save/";

        _saveBtn.onClick.AddListener(Save);
        _loadBtn.onClick.AddListener(Load);
    }

    /// <summary>
    /// 載入檔案
    /// </summary>
    private void Load()
    {
        if (!Directory.Exists(_loadPath))
        {
            Debug.LogError("The file not be found in this path. path:" + _loadPath);
            return;
        }

        string fullFileName = _loadPath + _fileName + EXTENSION;
        StreamReader sr;
        sr = File.OpenText(fullFileName);
        string content = sr.ReadToEnd();
        sr.Close();
        sr.Dispose();

        _table = CSVTable.CreateTable(_fileName, content);

        // 新增測試
        Test();
    }

    /// <summary>
    /// 儲存檔案
    /// </summary>
    private void Save()
    {
        if (_table == null)
        {
            Debug.LogError("The table is null.");
            return;
        }
        string tableContent = _table.GetContent();

        if(!Directory.Exists(_savePath))
        {
            Debug.Log("未找到路徑, 已自動建立");
            Directory.CreateDirectory(_savePath);
        }
        string fullFileName = _savePath + _fileName + EXTENSION;

        StreamWriter sw;
        sw = File.CreateText(fullFileName);
        sw.Write(tableContent);
        sw.Close();
        sw.Dispose();

        _table = null;
        _display.text = "Save.";
    }

    /// <summary>
    /// 測試方法
    /// </summary>
    private void Test()
    {
        // 顯示所有資料(以除錯格式顯示)
        Debug.Log(_table.ToString());

        // 顯示所有資料(以儲存格式顯示)
        _display.text = _table.GetContent();

        // 拿到某一資料
        _display.text += "\n" + "1001的年齡: " + _table["1001"]["年齡"];
        // 拿到資料物件
        _display.text += "\n" + "1002的資料: " + _table["1002"].ToString();
        // 修改某一資料
        _table["1003"]["年齡"] = "10000";
        _display.text += "\n" + "1003新的年齡: " + _table["1003"]["年齡"];

        // 新增一條資料
        CSVDataObject data = new CSVDataObject("1005",
            new Dictionary<string, string>()
            {
                { "姓名","hahaha" },
                { "年齡","250" },
                { "性別","隨便吧" },
            },
            new string[] { "編號", "姓名", "年齡", "性別" });
        _table[data.ID] = data;
        _display.text += "\n" + "新新增的1005的資料: " + _table["1005"].ToString();

        // 刪除資料
        _table.DeleteDataObject("1001");
        _table.DeleteDataObject("1002");
        _display.text += "\n" + "刪了兩個之後:" + "\n" + _table.GetContent();

        // 刪除所有資料
        _table.DeleteAllDataObject();
        _display.text += "\n" + "還剩下:" + "\n" + _table.GetContent();
    }
}

效果截圖

在場景中新增測試的按鈕和文字框,與指令碼關聯。將資料檔案放在程式碼中所給的路徑下(可以構建儲存的目錄)點選 Load 按鈕即可出線程式碼中測試函式的輸出效果。
點選 Load 後加載出來的效果

【完整專案地址】

github:CSV 檔案的讀寫示例工程 —— By 亦澤
PS:如果你覺得對你有很大幫助的話麻煩給我的專案點個星,這樣可以讓更多的開發者看到哦~