【Go: excelize】ExcelデータベースをSQLに移行するサンプルApp📖【年末大掃除】
この記事はGo Advent Calendar 2021 - Goに参加しています。
この記事は何
モダンな開発に勤しまれる諸先輩方には無縁のことかと思いますが、世の中にはまだまだExcelで管理されているデータベースのようなものが存在いたしやす。Excelは悪くございやせん、規模が小さいうちは何も問題ございやせん、ただしそのデータベースがもし肥大化してきたら一度考え直すときかもしれやせん。
そんなこんなで2021年ももう終わり。ここらで一つお掃除してみませんか?という記事にございます。
(ここからは普通の口調にします。。。)
この記事で扱う内容
- Excelを扱うためのツールの選定
- サンプルアプリケーションの構成
- 実装上のTips
なお最終的なサンプルアプリケーションは下記repoにありますので、適宜ご参照下さい。
Excelを扱うためのツールの選定
結論としては、本記事ではGo言語のパッケージであるexcelizeを活用していきます。
選定にあたっての考え方を記します。
要件(非要件含む)
- エクセルに含まれるテキスト・図を抽出することができる
- 複数ファイルを一括して処理しやすいこと
- ツールの導入ハードルは可能な限り低くする
ツール調査
軽く調べるだけでも様々な言語で様々なパッケージ・モジュールが公開されています。VBAをそのまま使うこともできますし、Pythonであればopenpyxl
を使うこともできます。ただ今回は導入ハードルを極力下げるため、シングルバイナリを簡単にビルドできるGo言語でいきたいと思います。(VBAは大きな処理をさせるにはちょっと…)
Go言語にはexcelize
というパッケージがあります。セルの値のset、getはもちろん、画像の扱いもでき、また書式の扱いもできる高機能なお方です。(今回はそこまでやりませんが)
ドキュメントも整備されており使いやすそうです。いってみましょう。
サンプルアプリケーションの構成
サンプルアプリケーションでは、エクセルの内容をSQLデータベースに記録することと、それだけではつまらないので、記録した結果をクエリしてブラウザで表示するところまでやってみます。(もし一部の機能だけ使いたい場合は上記repoのコードを分解していただければと思います)
なお、読み込んでみるエクセルは次のようなものとします。
Gopher君の図が並んでいますね。いったい何のデータベースなんでしょうか。
Gopher was designed by Renee French (http://reneefrench.blogspot.com/).
機能(保存側)
- エクセルファイルを読み込んで、テキストをコピーする。またセルに画像が貼られていたら画像を保存してそのファイルパスをコピーする。
- テキストおよび画像ファイルパスをSQLデータベースに記録する
機能(表示側)
- SQLデータベースに保存してある結果の一覧を取得する
- 簡単なHTMLをレンダリングして、保存してあるテキストおよび画像を一覧表示する
周辺ツールの選定
- SQLデータベースを取り扱うにあたっては、今回は複雑なことはしないので何を使っても良いのですが、筆者が最近使っている
ent
というORMを使ってみます。SQLは利用者側での導入が簡単という理由でSQLite3
を使います。 - クエリした結果を表示する方法も何でも良いのですが、ブラウザに一覧表示するためのホスティングには
echo
を使うことにしました。軽量なところが好きです。
実装結果
以上のような選定を経て、サンプルアプリケーションを構築しました。エクセルの取り込みを終えてlocalhost:1323/posts
にアクセスすると、読み取った結果の一覧が表示されました。今回は簡単なクエリと表示だけですが、SQLクエリ+ホスティングをしているだけなので、いかようにも拡張がききますね。
一覧表示. Gopher's copyright is same as above
実装上のTips
難しいことはないので詳細な実装はソースコードを眺めていただければと思いますが、せっかくの記事ですのでTipsも紹介させてもらいます。
excelizeでセル上の画像を保存する
- 保存にあたっては、エクセル上に保存されている画像のフォーマットを適切に扱う必要があります。
- 私も今回のサンプルを通じて初めて知ったのですが、エクセル上のセルに貼り付けられた画像にはちゃんとファイル名と拡張子が付いているんですね。
- ここでちょっと躓くのは、クリップボードから直接貼り付けた画像やbmp画像はpngとして保存されるところです(筆者のWindows環境でのみ確認したので、もしかしたら環境によって異なるかもしれません)
- ということでexcelizeの
GetPicture
メソッドを使い、画像のファイル名(拡張子付き)と画像のbyteデータをゲットした後、その拡張子を使って画像を保存する必要があります。
以上を加味した実装は次のようになりました。
// SaveImage saves a inline image, then returns the saved file's name.
// Set the func if you want to define save-name. Default is yyyymmdd-hhmmdd_hash(md5).ext.
// Set the func as nil, if you do not define save-name.
func (r *Reader) SaveImage(row, col int, savedir string, fn func([]byte) string) (string, error) {
cell, err := excelize.CoordinatesToCellName(col, row)
if err != nil {
return "", fmt.Errorf("error at (%d, %d); %w", row, col, err)
}
t, i, err := r.file.GetPicture(r.sheet, cell)
if err != nil {
return "", fmt.Errorf("error at (%d, %d); %w", row, col, err)
}
saveBase := defaultSaveName(i)
if fn != nil {
saveBase = fn(i)
}
saveExt := filepath.Base(t)
saveName := filepath.Join(savedir, saveBase+saveExt)
f, err := os.Create(saveName)
if err != nil {
return "", fmt.Errorf("error at (%d, %d); %w", row, col, err)
}
defer f.Close()
if _, err := f.Write(i); err != nil {
return "", fmt.Errorf("error at (%d, %d); %w", row, col, err)
}
return saveName, nil
}
func defaultSaveName(b []byte) string {
n := util.LocalNow().Format("20060102-150405")
return fmt.Sprintf("%s_%x", n, md5.Sum(b))
}
おわりに
このようにして作ったシングルバイナリを配布し年末の大掃除に取り組みたいところですが、実際問題エクセル愛好者は多いと思いますので、実行に移すのはなかなか難しいですね(片手落ち
お後がよろしいようで。
Discussion