技術と趣味の亜空間

主にゲームプログラミングとその周辺に関する記事を不定期で上げていきます

【Unity】エクセルのマスタデータ変換ツールを作ったときの備忘録

概要

過去にUnityでスタンドアロンなエクセルのマスタデータ変換ツールを作ったときの備忘録。

はじめに

マスターデータがエクセルで管理されており、それをクライアントへはCSV形式(拡張子はtxtだが)で、サーバーへはJson形式で渡す作りになっていた。
プランナーはそれぞれのフォーマットに変換するためにネット上の変換サイトを経由してエクセルファイルを1つ1つ変換するという面倒な作業をしていた。ゆくゆくはどっちかに形式を統一したい感はあるが、しばらくは外注メインの開発なため、どちらでも即変換できるツールをUnityで作成。

ポイントとしては、

  • プランナーがUnityを触る必要が無いようにスタンドアロンなアプリ(Win/OSX)にした
  • PC上にある複数エクセルファイルを指定して読み込み対応
  • 変換はCVS/JSONを選択可能。出力先のディレクトリも自由に選べる
  • 変換設定ファルのインポート・エキスポート、アプリ上での動的な編集機能

です。
ファイル読み込み周りを別のエンジニア、UIと変換部分を私が作成しました。

エクセルデータの読み込み

初めはExcel Importerを使う気満々でしたが、残念ながらマスターデータのフォーマットが違いすぎ && そもそもフォーマット自体がバラバラなため断念しました。
なのでこのエクセルインポーターのコア部分である、エクセルファイルの読み書きが可能なプラグインNOPIだけを利用することにしました。

github.com

NOPIを用いることで .xls および .xlsx ファイルを読み込めるようになります。
以下は読み込みコードです。
.xls と .xlsx で読み込む形式が違う点に注意します。

    // パス(OSX)
    var inputPath = "/Users/myname/Documents/HogeHoge.xlsx";
        
    using(FileStream stream = File.Open(inputPath, FileMode.Open, FileAccess.Read, FileShare.Read)) {
        IWorkbook book = null;
        string extension = Path.GetExtension(inputPath);
        // 拡張子ごとのワークブックを取得
        if(extension == ".xls") {
            book = new HSSFWorkbook(stream);
        } else if(extension == ".xlsx") {
            book = new XSSFWorkbook(stream);
        }
    }

読み込みエラーも考慮してtry-catchとかも入れると良い感じ。

セルデータの取得方法

読み込んだエクセルのワークブックから各セルのデータを取得します。
まずはワークブックからシート(ISheet)を取得します。取得はGetSheetAt(sheetIndex)で取得できます。

// 全シート取得
for(var sheetIndex = 0; sheetIndex < book.NumberOfSheets; sheetIndex++) {
    ISheet sheet = book.GetSheetAt(sheetIndex);
}

このISheetから各セルの情報(ICell)を取得します。

    for(var cellIndex = row.FirstCellNum; cellIndex < row.LastCellNum; cellIndex++) {
        ICell cell = row.GetCell(cellIndex);
        if(cell == null) {
            continue;
        }
    }

セルデータの取り出し方ですが、データの型によって抽出方法が異なります。
具体例として、下記のようにそれぞれのタイプを見て出し方を指定します。

    private string getCellString(ICell cell, CellType cellType) {
        switch(cellType) {
            // 文字列型
            case CellType.String:
                return cell.StringCellValue;
            // 数値型(日付の場合もここに入る)
            case CellType.Numeric:
                // セルが日付情報かを判定
                if(DateUtil.IsCellDateFormatted(cell)) {
                    // 日付型
                    return cell.DateCellValue.ToString("yyyy/MM/dd HH:mm:ss");
                } else {
                    // 数値型
                    return cell.NumericCellValue.ToString();
                }
            // bool型(文字列でTrueとか入れておけばbool型として扱われた)
            case CellType.Boolean:
                return cell.BooleanCellValue.ToString();
            // 入力なし
            case CellType.Blank:
                return cell.ToString();
            // 数式
            case CellType.Formula:
                // 下記で数式の文字列が取得される
                // return cell.CellFormula.ToString();
            
                // 数式の元となったセルの型を取得して同様の処理を行う
                return getCellString(cell, cell.CachedFormulaResultType);
            // エラー
            case CellType.Error:
                var error = cell.ErrorCellValue.ToString();
                Debug.LogError("Error Cell! : " + error);
                return error;
            // 型不明なセル
            case CellType.Unknown:
            default:
                return "";
        }
    }

気をつける点

1. Win/OSXディレクトリパス

  • ルート( C:\~/User/~
  • スラッシュ( \/ )

2. 空欄セルにデータが入っているという誤検出

たとえ該当のセルの値をDelキーなんかで消しても検知されるという謎の恐ろしい現象。
この場合は、エラーの疑わしいエクセルファイルのセルを丸ごと削除することで一応は解決できる。
このようなエラーが出る原因に以下が考えられる。
・現在は空白のセルにもともと何らかの値・文字列が入力されていた。
・空白セルだが、「太字」や「文字色」等、スタイル設定がエクセルのメニューで施されている。
作った時は原因が特定できなかったので、定義済みのセルをカウントするPhysicalNumberOfCellsプロパティで空欄を判定をするという超ゴリ押しでやってましたが、現在はLINQで全て空欄だったらスキップするようにしています。空欄かどうかは上記のCellTypeで判定できます。

    // 行を取得
    IRow row = sheet.GetRow(rowIndex);
    if(row == null) {
        continue;
    }             
    // 対象の行の列がすべて空欄の場合はスキップ
    var isAllEmpty = row.Cells.All(c => c.CellType == CellType.Blank);
    if(isAllEmpty) {
        continue;
    }

3. 【Win】ExcelデータがEditor上では読み込めるのにビルドすると読み込めない

windowsで、Excelデータの読み込みがunity上で確認したら何事もなかったのに、いざビルドしてアプリ化すると読み込めないという問題が発生。
エラー出力すると下記のように表示されました:

NotSupportedException: CodePage 437 not supported ...

437はUSのコードページということなので、文字コードの違いによるエラーであることが分かります。
NOPIには ICSharpCode.SharpZipLib.dll が含まれているので、こいつのDefaultCodePageの設定をUTF8のコードページに指定してあげることで解決しました。

    void Start() {
        ICSharpCode.SharpZipLib.Zip.ZipConstants.DefaultCodePage = System.Text.Encoding.UTF8.CodePage;
    }