ライブラリはどのようにExcelをCSVにしているのか
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.xlsxをunzip -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 1のEcma Office Open XML Part 1 - Fundamentals And Markup Language Reference.pdfのL.2.1 Workbookを見れば概要がわかります。
Excelのシートはどのようにデータを持っているのか
シートは以下のようにデータをもっていたので、このデータをxlフォルダ以下から取得できればよさそうです。

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にあり以下のようになります。
<?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という値だとわかります。
<?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.relsのTargetのAttributeに絶対パスが指定されていたからでした。Excelのソフトで保存するとほぼここは相対パスが指定されるようですが、ライブラリやパッケージによって生成されたファイルではここが絶対パスで指定されるようです。とりあえずPRを作ってDart側ではこれを参照するようにしました。
まとめ
なんとなく使っているExcelからCSVの変換も中身を見ればなんのことはないXMLを読み込んで処理していました。 ただその処理の内容も関数が使われていたり型によって複雑になりパーサーをつくるのもなかなか大変そうでパッケージに感謝の心でいっぱいですね。