はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity, C#】ライブラリを用いずエクセルファイル「.xlsx」の中身から要素を取得する(Zip展開)

はじめに

今回はエクセルのファイル(.xlsx)からライブラリを用いずに中身を調べる手法について紹介をしたいと思います。

Unityでエクセルのデータを扱おうとすると、NPOIというライブラリを導入することが多いようですね。
www.nuget.org
github.com

ただ最近.xlsxはただの.zipファイルだよということを知りまして、面白そうなので自分でもやってみようかなと思いました。

思ったより簡単にできたので紹介します。

.xlsxについて

.xlsxzipで圧縮されており、展開するとxml等の中身が出てきます。

実際に以下のエクセルデータを作成し、展開してみました。

エクセルのデータ
エクセル名.xlsx
|---- _rels
|       |---- .rels
|---- [Content_Types].xml
|---- docProps
|       |---- app.xml
|       |---- core.xml
|---- xl
        |---- _rel
        |       |---- workbook.xml.rels
        |---- theme
        |       |---- theme1.xml
        |---- worksheets
        |       |---- sheet1.xml
        |---- sharedStrings.xml
        |---- styles.xml
        |---- workbook.xml

.xlsxOffice Open XMLというオフィス用のファイルフォーマットを採用しているファイルになります。

Office Open XML (OpenXML、OOXML) とは、XMLをベースとし、Microsoftが策定、最初はECMAで引き続きISO/IEC JTC1により標準化されたオフィススイート用ファイルフォーマットである。
...
Microsoft Office はデータを保存するにあたり独自のバイナリ形式を用いてきたが、バージョン12(Office 2007)からは、XMLで記述された規格を標準ファイル形式として採用した。それが Office Open XMLである。

Office Open XML - Wikipedia

SpreadsheetMLという奴ですね。

SpreadsheetML
Excelなど表計算のデータを記述するための言語。ワークブックの下に複数のワークシートが連なるという形で構成される。

Office Open XML - Wikipedia

C#で要素の一覧を取得する

エクセルに記述された文字列はxl/sharedStrings.xmlに入っているようですね。

// sharedStrings.xmlの中身
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="14" uniqueCount="14">
  <si>
    <t>これはA1です。</t>
    <phoneticPr fontId="1" />
  </si>
  <si>
    <t>これはA4です。</t>
    <phoneticPr fontId="1" />
  </si>
  <si>
    <t>これはB1です。</t>
    <phoneticPr fontId="1" />
  </si>
  <si>
    <t>B2</t>
    <phoneticPr fontId="1" />
  </si>
  <si>
    <t>A1</t>
    <phoneticPr fontId="1" />
  </si>
  <si>
    <t>A3</t>
    <phoneticPr fontId="1" />
  </si>
  <si>
    <t>A5</t>
    <phoneticPr fontId="1" />
  </si>
  <si>
    <t>A6</t>
    <phoneticPr fontId="1" />
  </si>
  <si>
    <t>A7</t>
    <phoneticPr fontId="1" />
  </si>
  <si>
    <t>A8</t>
    <phoneticPr fontId="1" />
  </si>
  <si>
    <t>B3</t>
    <phoneticPr fontId="1" />
  </si>
  <si>
    <t>B4</t>
    <phoneticPr fontId="1" />
  </si>
  <si>
    <t>B5</t>
    <phoneticPr fontId="1" />
  </si>
  <si>
    <t>C1</t>
    <phoneticPr fontId="1" />
  </si>
</sst>

この中身を取得したい場合はZipFileクラスを利用することで簡単にできます。
ZipFile クラス (System.IO.Compression) | Microsoft Learn

// 適当にStreamingAssetsの中に「Sample.xlsx」を入れておく
var zipPath = Application.streamingAssetsPath + "/Sample.xlsx";

using var archive = ZipFile.Open(zipPath, ZipArchiveMode.Update);
var entry = archive.GetEntry("xl/sharedStrings.xml");
if (entry != null)
{
    using var reader = new StreamReader(entry.Open());
    Debug.Log(reader.ReadToEnd());
}
出力

どのセルの要素かを調べる

sharedStrings.xmlにはセルに記述された文字列の一覧が格納されています。

異なるセルに重複した文字列を記述したとしても、sharedStrings.xmlには一つしか書き込まれません。賢いですね。

ではどのセルにどの文字列が書かれているかといった情報はどこにあるのかというと、xl/worksheets/sheet〇.xml(sheet名と一致するわけではないよう)に格納されています。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x14ac xr xr2 xr3" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" xmlns:xr2="http://schemas.microsoft.com/office/spreadsheetml/2015/revision2" xmlns:xr3="http://schemas.microsoft.com/office/spreadsheetml/2016/revision3" xr:uid="{EEFCE81E-973B-AF43-8966-800BEF7D3AA3}">
  <dimension ref="A1:C8" />
  <sheetViews>
    <sheetView tabSelected="1" workbookViewId="0">
      <selection activeCell="C1" sqref="C1" />
    </sheetView>
  </sheetViews>
  <sheetFormatPr baseColWidth="10" defaultRowHeight="20" />
  <cols>
    <col min="1" max="1" width="35.42578125" customWidth="1" />
    <col min="2" max="2" width="26.85546875" customWidth="1" />
  </cols>
  <sheetData>
    <row r="1" spans="1:3">
      <c r="A1" t="s">
        <v>0</v>
      </c>
      <c r="B1" t="s">
        <v>2</v>
      </c>
      <c r="C1" t="s">
        <v>13</v>
      </c>
    </row>
    <row r="2" spans="1:3">
      <c r="A2" t="s">
        <v>4</v>
      </c>
      <c r="B2" t="s">
        <v>3</v>
      </c>
    </row>
    <row r="3" spans="1:3">
      <c r="A3" t="s">
        <v>5</v>
      </c>
      <c r="B3" t="s">
        <v>10</v>
      </c>
    </row>
    <row r="4" spans="1:3">
      <c r="A4" t="s">
        <v>1</v>
      </c>
      <c r="B4" t="s">
        <v>11</v>
      </c>
    </row>
    <row r="5" spans="1:3">
      <c r="A5" t="s">
        <v>6</v>
      </c>
      <c r="B5" t="s">
        <v>12</v>
      </c>
    </row>
    <row r="6" spans="1:3">
      <c r="A6" t="s">
        <v>7</v>
      </c>
    </row>
    <row r="7" spans="1:3">
      <c r="A7" t="s">
        <v>8</v>
      </c>
    </row>
    <row r="8" spans="1:3">
      <c r="A8" t="s">
        <v>9</v>
      </c>
    </row>
  </sheetData>
  <phoneticPr fontId="1" />
  <pageMargins left="0.7" right="0.7" top="0.75" bottom="0.75" header="0.3" footer="0.3" />
</worksheet>


このvタグで囲まれた数字が、sharedStrings.xmlに格納されていた文字列の順番に該当します。

// 適当にStreamingAssetsの中に「Sample.xlsx」を入れておく
var zipPath = Application.streamingAssetsPath + "/Sample.xlsx";

using var archive = ZipFile.Open(zipPath, ZipArchiveMode.Update);
var sharedStrings = archive.GetEntry("xl/sharedStrings.xml");
if (sharedStrings != null)
{
    using var reader = new StreamReader(sharedStrings.Open());
    Debug.Log(reader.ReadToEnd());
}

var sheet1 = archive.GetEntry("xl/worksheets/sheet1.xml");
if (sheet1 != null)
{
    using var reader = new StreamReader(sheet1.Open());
    Debug.Log(reader.ReadToEnd());
}
出力結果

補足

xmlの解析は色々と手法があり、派閥もあるらしいので今回は割愛します。