はじめに
みんなが大好きなExcel、きっとGo言語でも生成したいという要望があるかと思う。
探してみるとxlsxというライブラリがあったのでそれを使ってExcelファイル出力を試してみる。
なお、goexcelという上記ライブラリをラップして使いやすくしたライブラリもあるようだが、excelライブラリの変更に追随出来ていないようで、この記事執筆時点はうまく動かない。
Excelファイルの新規作成
xlsxライブラリのREADMEにサンプルコードがあるので引用する。
package main
import (
"fmt"
"github.com/tealeg/xlsx"
)
func main() {
var file *xlsx.File
var sheet *xlsx.Sheet
var row *xlsx.Row
var cell *xlsx.Cell
var err error
file = xlsx.NewFile()
sheet, err = file.AddSheet("Sheet1")
if err != nil {
fmt.Printf(err.Error())
}
row = sheet.AddRow()
cell = row.AddCell()
cell.Value = "I am a cell!"
err = file.Save("MyXLSXFile.xlsx")
if err != nil {
fmt.Printf(err.Error())
}
}
なお、下記のように*xlsx.Sheet.AddRow()
や*xlsx.Row.AddCell()
を使わなくても内部でやってくれるので問題ない。
package main
import (
"fmt"
"github.com/tealeg/xlsx"
)
func main() {
var file *xlsx.File
var sheet *xlsx.Sheet
var err error
file = xlsx.NewFile()
sheet, err = file.AddSheet("Sheet1")
if err != nil {
fmt.Printf(err.Error())
}
sheet.Cell(0, 0).Value = "I am a A1 cell!"
sheet.Cell(0, 1).Value = "I am a A2 cell!"
sheet.Cell(1, 1).Value = "I am a B2 cell!"
err = file.Save("MyXLSXFile.xlsx")
if err != nil {
fmt.Printf(err.Error())
}
}
既存Excelファイルの読み込み
こちらもxlsxライブラリのREADMEにサンプルコードがあるので引用する。
package main
import (
"fmt"
"github.com/tealeg/xlsx"
)
func main() {
excelFileName := "/home/tealeg/foo.xlsx"
xlFile, err := xlsx.OpenFile(excelFileName)
if err != nil {
...
}
for _, sheet := range xlFile.Sheets {
for _, row := range sheet.Rows {
for _, cell := range row.Cells {
text, _ := cell.String()
fmt.Printf("%s\n", text)
}
}
}
}
xlsx.OpenFile()
で返ってくるのはxlsx.NewFile()
と同じく*xlsx.File
なので、あとは良しなにやることができる。
また、[]byte
型として読み込み済みのファイルを読み込むxlsx.OpenBinary()
、io.ReaderAt
型を使って読み込むxlsx.OpenReaderAt()
なども存在する。
Excelファイルの内容をスライスとして読み込む
xlsx.OpenFile()
等は*xlsx.File
型を返すが、ファイルの中身を読み込んで[][][]string
型のスライスとして返してくれるxlsx.FileToSlice()
というメソッドがある。
package main
import (
"fmt"
"github.com/tealeg/xlsx"
)
func main() {
slice, err := xlsx.FileToSlice("test.xlsx")
if err != nil {
fmt.Printf(err.Error())
}
fmt.Println(slice[0][1][2]) // 1シート目のC2の内容を出力
}
単純に読み込むだけであればスライスとして読み込んだほうが後々楽なケースも多いだろう。
また、*xlsx.File
型で読み込んだとしても*xlsx.File.ToSlice()
メソッドを使えば同様に[][][]string
型のスライスを返してくれる。
フォーマット指定
ソースを読んだ限りでは後からフォーマット指定をすることが出来ず、値をセットするときにフォーマットも指定する方法しかないようだ。
メソッド | 分類 |
---|---|
func (c *Cell) SetBool(b bool) |
文字列(真 = "1" / 偽 = "0") |
func (c *Cell) SetDate(t time.Time) |
日付("mm-dd-yy") |
func (c *Cell) SetDateTime(t time.Time) |
日付 + 時間("m/d/yy h:mm") |
func (c *Cell) SetDateTimeWithFormat(n float64, format string) |
日付のフォーマット指定 |
func (c *Cell) SetFloat(n float64) |
数値 |
func (c *Cell) SetFloatWithFormat(n float64, format string) |
数値のフォーマット指定 |
func (c *Cell) SetFormula(formula string) |
数式 |
func (c *Cell) SetInt(n int) |
数値 |
func (c *Cell) SetInt64(n int64) |
数値 |
func (c *Cell) SetString(s string) |
文字列 |
func (c *Cell) SetValue(n interface{}) |
標準 |
*xlsx.Cell.SetDateTimeWithFormat()
はtime.Time
型を渡すことができず、自前でExcelの日付シリアル値を求めてfloat64
型で渡す必要がある。
*xlsx.Cell.SetDate()
及び*xlsx.Cell.SetDateTime()
についてはどのタイムゾーンを設定していたとしても内部で良しなにやって出力してくれるようだ。
(内部処理についてはこの辺りを参照)
*xlsx.Cell.SetFormula()
はExcel VBAのFormula
プロパティ相当(A1形式で指定するもの)で、R1C1形式を指定しても実行時にはエラーとならないが、出力されたExcelファイルを開くと「ファイルが壊れている」と表示される。
セル値の読み込み
メソッド | 備考 |
---|---|
func (c *Cell) Bool() bool |
下記「Boolの判定」参照 |
func (c *Cell) Float() (float64, error) |
|
func (c *Cell) FormattedValue() (string, error) |
期待通りにならないケースあり |
func (c *Cell) Formula() string |
設定された式を返す |
func (c *Cell) GetNumberFormat() string |
|
func (c *Cell) GetTime(date1904 bool) (t time.Time, err error) |
下記「日付シリアル値の基準日」参照 |
func (c *Cell) Int() (int, error) |
|
func (c *Cell) Int64() (int64, error) |
|
func (c *Cell) String() (string, error) |
Boolの判定
Office Open XMLの仕様を軽く流し読みしても真偽値については出てこず、Excelの書式設定にも真偽値はないため、恐らくこのライブラリ固有のものだと思われる。
判定は以下の流れで行われる。
- セルの値が
*xlsx.Cell.SetBool()
でセットされたものの場合、"1"であればtrue
、それ以外はfalse
- セルの表示形式が「数値」か「標準」の場合、"0"であれば
false
、それ以外はtrue
- 上記以外の場合、空欄であれば
false
、それ以外はtrue
日付シリアル値の基準日
日付シリアル値とは、基準日からの経過日数のことである。
UNIX時間がUTC 1970年1月1日 0時0分0秒を基準とした経過秒数だが、それを経過日数で表し、コンマ以下を時刻に割り当てたものと理解すれば分かりやすいのではないだろうか。
この基準日だが、Excelでは2種類の基準日が存在する。
- 1900年1月1日 0時0分0秒
- 1904年1月1日 0時0分0秒
実を言うとこのエントリを書くまで基準日に1904年のものがあるということを知らなかったのだが、調べてみると随分とややこしいことになっているようだ。
標準では1900年のほうが基準日となっているのだが、こちらを基準とした場合、本来存在しない「1900年2月29日」なる日付が出てしまう(シリアル値=60)。
これはどうやらLotus 1-2-3との互換性を維持するためのもののようで、Lotus 1-2-3が実際にあったかどうかはさておき、4年に1度うるう年が来るという大雑把な設計をしていたためにこういう状況になっているようだ。
なお、このようなことが起きるのは「1900年2月29日」のみであり、以降はちゃんとうるう年を判定してくれる。
このように本来存在しない日付がある場合、シリアル値を計算してセルに値を入れ込む場合に誤差が出てしまう。
そういったことに対応するためにもうひとつの基準日である「1904年」というものが存在するようだ。
*xlsx.Cell.GetTime()
の引数date1904
はfalse
を渡すと「1900年」を基準日とし、true
を渡すと「1904年」を基準日としてtime.Time
型に直してくれる。
また、上記の基準日をシリアル値に直すと「1」になることも注意が必要である。
*xlsx.Cell.SetDateTimeWithFormat()
はtime.Time
型を受け付けずfloat64
で渡す必要があるため自分で計算する必要があるが、私のようにまぬけにも基準日を「0」と勘違いして計算するとずれが出てしまうことになる。
スタイル設定
*xlsx.Cell.GetStyle()
で既存スタイル定義を取得するか、xlsx.NewStyle()
で新規スタイル定義を作成する。
// スタイルの適用方法
style := xlsx.NewStyle()
sheet.Cell(0, 0).SetStyle(style)
// xlsx.Style型の定義
type Style struct {
Border Border
Fill Fill
Font Font
ApplyBorder bool
ApplyFill bool
ApplyFont bool
ApplyAlignment bool
Alignment Alignment
NamedStyleIndex *int
}
罫線
type Border struct {
Left string
LeftColor string
Right string
RightColor string
Top string
TopColor string
Bottom string
BottomColor string
}
単純に細い黒線を引きたいだけであればLeft
、Right
、Top
、Bottom
に"thin"
、*Color
に"FF000000"
を入れて*xlsx.Style.ApplyBorder
をtrue
にすれば適用される。
style := xlsx.NewStyle()
borderStyle = "thin"
borderColor = "FF000000"
style.ApplyBorder = true
style.Border.Left = borderStyle
style.Border.LeftColor = borderColor
style.Border.Right = borderStyle
style.Border.RightColor = borderColor
style.Border.Top = borderStyle
style.Border.TopColor = borderColor
style.Border.Bottom = borderStyle
style.Border.BottomColor = borderColor
またxlsx.NewBorder()
で一気に指定することもできるが、こちらでやると色指定ができない。
style := xlsx.NewStyle()
style.ApplyBorder = true
style.Border = xlsx.newBorder("thin", "thin", "thin", "thin")
背景色
type Fill struct {
PatternType string
BgColor string
FgColor string
}
恐らくRGB指定しか対応していないと思われる。
PatternType
は以下のものが設定できる。
2017/01/24 綴りが間違っていたり一部大文字に直さなければ動かない部分があったため修正
- darkDown
- darkGray
- darkGrid
- darkHorizontal
- darkTrellis
- darkUp
- darkVertical
- lightDown
- lightGray
- lightGrid
- lightHorizontal
- lightTrellis
- lightUp
- lightVertical
- gray0625
- gray125
- mediumGray
- solid
- none
デフォルトでは"none"
が設定されているため、単純に塗りつぶしたければ"solid"
を指定すること。
*xlsx.Style.ApplyFill
はデフォルトではfalse
だが、そのままでも適用されるようだ。
フォント指定
type Font struct {
Size int
Name string
Family int
Charset int
Color string
Bold bool
Italic bool
Underline bool
}
デフォルトでは12ptのVerdanaが設定されている。
フォント名は"MS Pゴシック"
のように日本語指定でも通るようだ(もちろん"MS PGothic"
でも通る)。
Charset
は恐らくこの辺りの16進数を10進数に直してセットすればよいのだろうが、私がすぐ確認できるフォントでは特にセットしなくても問題なかった。
また、デフォルトのフォントを指定する場合はxlsx.SetDefaultFont()
を利用する。
xlsx.SetDefaultFont(11, "MS Pゴシック") // 11ptのMS Pゴシックをデフォルトフォントとして指定
xlsx.SetDefaultFont()
はxlsx.NewStyle()
や*xlsx.Cell.GetStyle()
を呼ぶ前に設定しなければならないので注意。
セル内の文字位置関係
type Alignment struct {
Horizontal string
Indent int
ShrinkToFit bool
TextRotation int
Vertical string
WrapText bool
}
それぞれ型に合わせて値を入れていけば設定される。
こちらで試した限り、*xlsx.Style.ApplyAlignment
はデフォルトでfalse
となっているが、false
のままでも適用されるようだ。
Vertical
は"top"
、"center"
、"bottom"
を、Horizontal
は"left"
、"center"
、"right"
、または"general"
を設定する。
デフォルトではVertical
が"bottom"
、Horizontal
が"general"
となる。
ShrinkToFit
とWrapText
は順序がどうであれWrapText
のほうが優先されるようだ(これはExcelの仕様?)。
TextRotation
はExcel上の表現は-90~90となっているが、golangから設定する場合マイナス指定は無視されるため、91~180を指定する必要がある。
なお180を超えた場合、181~254はExcelの書式設定を開こうとすると「-90 から 90 の範囲でなければなりません」と怒られてしまう。
255は書式設定上の数値は0となるが、表示上は中央寄せして縦書きにしたような状態となる。
256からは再び0を指定した状態に戻るようだ。
セルのスタイル
*xlsx.Style.NamedStyleIndex
にint
型のポインタを入れることで利用できるように見せかけている。
Office Open XMLでいうcellStyle要素のxfId属性を指すものなのだが、現状使ってはいけない項目のようだ。
新規作成時はセルのスタイルを作ることが出来ないため特に気にしなくてもよいが、セルのスタイルが定義されている既存ファイルを読み込む際にExcelが設定したものを壊してしまい、出力したファイルをExcelで開く際に「ファイルが壊れている」と怒られる羽目になってしまった。
既存ファイルを読み込む際にはセルのスタイルを定義しないよう気をつける必要がある。
範囲指定してスタイル適用
そんなものはない。
背景色や罫線を範囲指定で一気に適用したいケースがあるが、私は愚直にforループで適用させることにした。
// A1 ~ D5までに同一スタイルを適用させる場合
startRow := 0
startCol := 0
endRow := 3
endCol := 4
for r := startRow; r <= endRow; r++ {
for c := startCol; c <= endCol; c++ {
sheet.Cell(r, c).SetStyle(style)
}
}
Excel VBAでいうRange
オブジェクトのようなものが欲しいと思うが、この辺りは要提案だろうか。
セルのマージ
*xlsx.Cell.Merge()
で行う。
sheet.Cell(0, 0).Merge(1, 1) // A1~B2をマージ
sheet.Cell(3, 4).Merge(1, 1) // D5~E6をマージ
上記のように指定セルから見て右下セルの相対位置を指定してマージする。
列幅の指定
*xlsx.Sheet.SetColWidth()
で行う。
sheet.SetColWidth(0, 0, 10.0) // A列
sheet.SetColWidth(1, 3, 15.0) // B~D列
上記のように指定する範囲の列幅を指定できる。
なお、上記のように10.0
を指定してExcelを生成してみたが、開いてみると9.38pt(80px)
で表示されている。
9.0
で8.38(72px)
、11.0
で10.38(88px)
、12.0
で11.38(96px)
となるので1.0
= 8px
として数値を決めると良さそうだ。
ファイルの保存
*xlsx.File.Save()
または*xlsx.File.Write()
行う。
var err error
var writer io.Writer
// パスを指定して保存
err = file.Save('/path/to/file.xlsx')
// io.Writer型に保存
writer = &bytes.Buffer{}
err = file.Write(writer)
*xlsx.File
はxlsx.OpenFile()
で開いた場合であってもファイルパスを保持しないため、上書き保存する場合でもファイルパスを指定する必要がある。
*xlsx.File.Write()
についてはio.Writer
を実装したものであれば何でも渡せるので、恐らくこちらを使ったほうが柔軟に利用できるだろう。
おわりに
xlsx
ライブラリの機能を一通り書き出してみたが、セルのスタイルを破壊してしまうのには注意が必要だが、新規作成からテンプレートを用いた出力まで一通り対応できるようだ。
途中にも書いたが範囲指定が出来るものが欲しいと思うが、今のところ独自に型を定義してそれっぽく動くようにして凌いでいる。