ライブラリはどのようにExcelをCSVにしているのか

by

@wapa5pow

ExcelのシートをCSVにしたくなったとき各プログラム言語でライブラリやパッケージが提供されており苦労せず変換できると思います。
さて、そのパッケージはどのようにExcelからCSV用のデータを取り出し変換しているのでしょうか。
この記事ではExcel、ここでは.xlsxの拡張子のファイルがどのような形式でデータを保持しており、どのようにすればCSV用のデータを取り出せるか確認します。

Excelの.xlsxはどのようなファイルか

Microsoft公式のExcelファイルの説明を確認すると以下のように書いてあります。

Excel 2010 および Excel 2007 の既定の XML ベース ファイル形式。 Microsoft Visual Basic for Applications (VBA) マクロ コードや Microsoft Office Excel 4.0 マクロ シート (.xlm) は保存できません。

そうです。実は.xlsxはXMLの集まりでzipで圧縮されているのです。

ためしに以下のbook.xlsxというファイルをExcelで作ります。

book

book.xlsxunzip -d book book.xlsxのコマンドなどで、unzipして解凍します。
すると以下のようなファイル構成になっています。

.
├── [Content_Types].xml        Content-type item
├── _rels                      Package-relationship item
├── docProps
│   ├── app.xml                Application-Defined File Properties part
│   └── core.xml               Core File Properties part
└── xl
    ├── _rels
    │   └── workbook.xml.rels  Part-relationship item
    ├── calcChain.xml          Calculation Chain part
    ├── sharedStrings.xml      Shared String Table part
    ├── styles.xml             Styles part
    ├── theme
    │   └── theme1.xml         Theme part
    ├── workbook.xml           Workbook part
    └── worksheets
        └── sheet1.xml         Worksheet parts

docPropsにはファイルがどのアプリケーションによって作られたかや、ファイルが作成・更新された日時などの情報が入っています。ここをみればこのファイルがExcelでつくられたかライブラリで機械的に作られたかある程度判別できそうです。

xlフォルダ以下にはExcelのシート内のデータなどの情報がはいっています。

Office Open XMLのWikipediaにかかれている通り、このフォーマットはECMAによりECMA-376として定められています。
Excelに関してはECMA-376 Part 1Ecma Office Open XML Part 1 - Fundamentals And Markup Language Reference.pdfL.2.1 Workbookを見れば概要がわかります。

Excelのシートはどのようにデータを持っているのか

シートは以下のようにデータをもっていたので、このデータをxlフォルダ以下から取得できればよさそうです。

book

xl以下の各XMLがどのような意味をもっているかは先ほどのEcma Office Open XML Part 1 - Fundamentals And Markup Language Reference.pdfのPDFを開き、18. SpreadsheetML Reference Materialに詳しく書いてあります。

xl/_rels/workbook.xml.relsをまず見ると、以下のようになっています。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
	<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml" />
	<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml" />
	<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml" />
	<Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain" Target="calcChain.xml" />
	<Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml" />
</Relationships>

シートの情報はworksheets/sheet1.xmlにあり以下のようになります。

title=worksheets/sheet1.xml
<?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="{2F0FA96E-3A81-344B-A4FA-B898EA0AF0B6}">
	<dimension ref="A1:B3" />
	<sheetViews>
		<sheetView tabSelected="1" workbookViewId="0">
			<selection activeCell="B2" sqref="B2" />
		</sheetView>
	</sheetViews>
	<sheetFormatPr baseColWidth="10" defaultRowHeight="16" x14ac:dyDescent="0.2" />
	<sheetData>
		<row r="1" spans="1:2" x14ac:dyDescent="0.2">
			<c r="A1" t="s">
				<v>0</v>
			</c>
			<c r="B1" t="s">
				<v>1</v>
			</c>
		</row>
		<row r="2" spans="1:2" x14ac:dyDescent="0.2">
			<c r="A2">
				<v>1</v>
			</c>
			<c r="B2">
				<f>A2+2</f>
				<v>3</v>
			</c>
		</row>
		<row r="3" spans="1:2" x14ac:dyDescent="0.2">
			<c r="A3" s="1">
				<v>44603</v>
			</c>
			<c r="B3" s="1">
				<v>44604</v>
			</c>
		</row>
	</sheetData>
	<pageMargins left="0.7" right="0.7" top="0.75" bottom="0.75" header="0.3" footer="0.3" />
</worksheet>

もうCSVに変換できそうですが、<c>タグの中身を知る必要があります。これはPDFの18.3.1.4 c (Cell)に記載があります。
worksheets/sheet1.xmlで言うと、<c>タグにあるAttributeは以下の意味になります。

  • r(Reference): どのセルを指しているか
  • t(Cell Data Type): セルのデータタイプ。 18.18.11 ST_CellType (Cell Type)に詳細が記載されている
    • b(Boolean)
    • d(Date)
    • e(Error)
    • inlineStr (Inline String)
    • n (Number)
    • s (Shared String)
    • str (String)
  • s(Style Index): 表示形式

例えば、A1セルt="s"<v>が0になっています。xl/sharedStrings.xmlを見ると以下のようになっており、indexの0はaという値だとわかります。

title=xl/sharedStrings.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="2" uniqueCount="2">
	<si>
		<t>a</t>
	</si>
	<si>
		<t>b</t>
	</si>
</sst>

B2セルA2+2という関数でしたが<v>というタグがあるのでそのまま見ればよさそうです。

A3セル<v>は44603でこれは1899年12月30日からの経過日です。以下のようにするとたしかに2022年2月11日となっています。

$ date -d 'Dec 30 1899 + 44603 days'
金  2 11 00:00:00 JST 2022

ここでなぜ1899年12月30日となっていて、1900年1月1日ではないかというと1900年をうるう年としてしまうバグをLotus 1-2-3から引き継いだからだそうです。おもしろい。

なぜ調べようと思ったのか

ここまででなんとなくExcelからライブラリやパッケージがどのようにしてCSVにしているか見えてきました。
筆者がExcelのフォーマットに関して興味をもった理由は、Dartでspreadsheet_decoderというパッケージを使っておりこれでとあるExcelファイルを読み込んだらエラーになったからでした。
原因は先ほど出てきたxl/_rels/workbook.xml.relsTargetのAttributeに絶対パスが指定されていたからでした。Excelのソフトで保存するとほぼここは相対パスが指定されるようですが、ライブラリやパッケージによって生成されたファイルではここが絶対パスで指定されるようです。とりあえずPRを作ってDart側ではこれを参照するようにしました。

まとめ

なんとなく使っているExcelからCSVの変換も中身を見ればなんのことはないXMLを読み込んで処理していました。
ただその処理の内容も関数が使われていたり型によって複雑になりパーサーをつくるのもなかなか大変そうでパッケージに感謝の心でいっぱいですね。