ライブラリはどのように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を読み込んで処理していました。
ただその処理の内容も関数が使われていたり型によって複雑になりパーサーをつくるのもなかなか大変そうでパッケージに感謝の心でいっぱいですね。