ExcelではCSVとXLSXの中間的なファイルフォーマットとしてXMLスプレッドシートがサポートされています。これをF#で読み取ってみます。
この記事の方法の使用例です。
- 不揃いなデータをコンピュテーション式で処理 2016.06.30
XMLスプレッドシート
スプレッドシートをデータソースとして、専用ライブラリ(ClosedXMLなど)を使わないで読み込むことを考えます。Excelでよく使われるファイル形式を検討します。
- CSV: 改行やエスケープを考慮しないケースでは処理が簡単ですが、装飾を加えることはできません。
- XLS: 複雑なバイナリ形式のためパースが困難です。
- XLSX: ZIPの中にXMLが入っています。XMLだけ処理する分にはあまり難しくはありませんが、ZIPから展開する手間があります。
XLSXのうちセルのデータが格納されているXMLだけを単独で抜き出せば、扱いやすさでは理想的に思えます。それに近いファイル形式がXMLスプレッドシートです。
※ スキーマ(XMLタグなど)はXLSXとは異なります。あくまでイメージです。
XMLスプレッドーシートにはオートシェイプが格納できないなどの制限がありますが、今回はオートシェイプは扱わないため不問とします。
サンプル
次のようなデータがあったとします。
社名 | 会長 | 社長 | 副社長 | 専務 |
---|---|---|---|---|
A社 | 山田 | 佐藤 | 鈴木 | 小林 |
B社 | 伊藤 | 池田 | ||
C社 | 山本 | 高橋 |
これをExcelで「XML スプレッドシート 2003」形式を指定して保存します。そのままだと複雑なので、プロパティなどを削って単純化したものを示します。
<?xml version="1.0"?>
<?mso-application progid="Excel.Sheet"?>
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
xmlns:html="http://www.w3.org/TR/REC-html40">
<Worksheet ss:Name="Sheet1">
<Table>
<Row>
<Cell><Data ss:Type="String">社名</Data></Cell>
<Cell><Data ss:Type="String">会長</Data></Cell>
<Cell><Data ss:Type="String">社長</Data></Cell>
<Cell><Data ss:Type="String">副社長</Data></Cell>
<Cell><Data ss:Type="String">専務</Data></Cell>
</Row>
<Row>
<Cell><Data ss:Type="String">A社</Data></Cell>
<Cell><Data ss:Type="String">山田</Data></Cell>
<Cell><Data ss:Type="String">佐藤</Data></Cell>
<Cell><Data ss:Type="String">鈴木</Data></Cell>
<Cell><Data ss:Type="String">小林</Data></Cell>
</Row>
<Row>
<Cell><Data ss:Type="String">B社</Data></Cell>
<Cell ss:Index="3"><Data ss:Type="String">伊藤</Data></Cell>
<Cell ss:Index="5"><Data ss:Type="String">池田</Data></Cell>
</Row>
<Row>
<Cell><Data ss:Type="String">C社</Data></Cell>
<Cell><Data ss:Type="String">山本</Data></Cell>
<Cell><Data ss:Type="String">高橋</Data></Cell>
</Row>
</Table>
</Worksheet>
</Workbook>
実際のパースのときには不要なタグは無視すれば良いので、単純化したものを想定してプログラムを組んでも問題ありません。
パース
XmlReaderで読み込みます。XmlReaderはプル型パーサーで、特徴については以下の記事を参照してください。
必要なタグを頭出しして、閉じられるまで内部のタグを繰り返し読みます。<Cell>
タグでは空白セルをスキップするためss:Index
が指定されていることに注意します。要素をfor
で処理するため、シーケンスを返します。
#r "System.Xml"
open System
open System.IO
open System.Text
open System.Xml
let startElement (xr:XmlReader) (n:string) =
xr.NodeType = XmlNodeType.Element && xr.Name = n
let endElement (xr:XmlReader) (n:string) =
xr.NodeType = XmlNodeType.EndElement && xr.Name = n
let each (xr:XmlReader) (n:string) = seq {
let parent = xr.Name
while xr.Read() && not (endElement xr parent) do
if startElement xr n then yield xr }
let find (xr:XmlReader) (n:string) =
each xr n |> Seq.isEmpty |> not
let openXml (xml:string) =
let fs = new FileStream(xml, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
let sr = new StreamReader(fs, Encoding.UTF8)
let xr = new XmlTextReader(sr)
find xr "Workbook" |> ignore
xr
let cells xr = seq {
let col = ref 1
for _ in each xr "Cell" do
let i = xr.GetAttribute "ss:Index"
if i <> null then
let i' = Convert.ToInt32 i
while !col < i' do
yield ""
col := !col + 1
yield if find xr "Data" then xr.ReadString() else ""
col := !col + 1 }
let rows xr = seq {
for _ in each xr "Row" do
yield cells xr |> Seq.toList }
let worksheets xr = seq {
for _ in each xr "Worksheet" do
yield xr.GetAttribute "ss:Name" }
これを使ってサンプルを読み込みます。
#load "XmlSpreadsheetReader.fsx"
open XmlSpreadsheetReader
[<EntryPoint>] do
use xr = openXml "Book1.xml"
for sheet in worksheets xr do
printfn "%s" sheet
for row in rows xr do
printfn "%A" row
Sheet1
["社名"; "会長"; "社長"; "副社長"; "専務"]
["A社"; "山田"; "佐藤"; "鈴木"; "小林"]
["B社"; ""; "伊藤"; ""; "池田"]
["C社"; "山本"; "高橋"]
セルの幅を修正したり、色や罫線を加えたりしても、データはそのまま読めます。印刷用に整形を施した状態でデータを保守できるというのがミソです。