LoginSignup
19
14

More than 1 year has passed since last update.

Excelを扱うRubyGemまとめ 2022

Posted at

※本記事は、22/12/02のQiita Night~Ruby~のLTでお話したことを記事化したものです。

8年前、RailsでExcelを扱うGemまとめという記事を書きました(RubyではなくRailsと書いているのは若気の至り)。Qiitaに記事を書き始めてから初めてたくさんのいいねをもらった思い出深い記事です。

ですが、投稿から8年も経っているにもかかわらず、「excel gem」と検索するとまだ最初に出てきます。
Excel関連Gem業界にとってはあんまり良くないと思うので、改めてまとめてみました。
image.png

なお、本記事での検証用コードは以下に置いています
https://github.com/Kta-M/excel_gems

システムでExcelファイルを使う需要

システム的にはCSVのほうが扱いやすいですが、Excelファイルの需要もそれなりにあります。
どうしてもExcelで!と言われたときに、この記事を参照してみてください。

  • CSVの壁
    • CSVは(ExcelがデフォルトでShift-JISで読み込むので)扱えない文字がある
    • CSVは(Excelが0始まりの数字文字列や日時文字列を勝手に解釈してしまうので)
      • 出力内容通りに表示してくれない
      • 開いて保存するだけで内容が変わってしまう
    • 罫線を引いたり、セルや文字に色を付けたり、セルのサイズを変えたりしたい
  • PDFの壁
    • あとから書き込める帳票がほしい
  • 運用の壁
    • 既存の業務をExcelでやっていたので

調査対象

The Ruby Toolboxの上位にあるものから選択しました。
image.png

axlsxcaxlsxに引き継がれたため除外、axlsx_railsaxlsxをrailsに導入するためのgemなので除外しました(caxlsx_railsもあります)。
そのため、調査対象は以下になります。8年前と変わってない…。

簡単比較表

ひとまず、それぞれ何ができるのか、簡単にまとめておきます。

新規作成 書き出し 読み込み 拡張子(Excel関連のみ)
roo x x o .xls, .xlsx
spreadsheet o o o .xls
rubyXL o o o .xlsx, .xlsm
caxlsx o o x .xlsx

書き込み部門

rooは読み込み専門なので、spreadsheet, rubyXL, caxlsxで比較していきます。

ちょっと無理やりいろんな書式を使っている感がありますが、以下のような表を作ってみます。
image.png

spreadsheet

  • 😄
    • いろいろな方法でセルにデータを入れられる
  • 🥲
    • 計算式が入力できない
    • デフォルトのフォントが設定できず、行ごと・セルごとに指定する必要がある
    • 色がカラーパレットからしか選べない
      • image.png
    • ドキュメントが手薄なので、ドキュメントに載っていないものはソースコードを見て回る必要がある
    • そもそも.xlsしか対応していない。。
サンプルコード
require 'spreadsheet'

# Workbookを作成
workbook = Spreadsheet::Workbook.new

# Sheetを作成
sheet = workbook.create_worksheet(name: 'curry')

# 各セルに書き込み
# いろいろな方法でデータを入れられる
# 計算式は入力できない
sheet.row(0).concat %w{品名 単価 数量 計}

sheet[1,0] = 'にんじん'
sheet[1,1] = 80
sheet[1,2] = 1
sheet[1,3] = 80

row = sheet.row(2)
row.push 50
row.push 2
row.push 100
row.unshift 'たまねぎ'

sheet.row(3).replace [ 'じゃがいも', 40, 2, 80 ]

sheet.row(4).push '牛肉', 1, 200
sheet.row(4).insert 1, 200

sheet.update_row 5, 'カレー粉', 150, 1, 150

sheet[6,0] = '総計'
sheet[6,3] = 610

# セルのマージ
sheet.merge_cells(6, 0, 6, 2)

# 各行のフォントを変更
format = Spreadsheet::Format.new(font: Spreadsheet::Font.new('メイリオ'))
(0..6).each do |row|
  sheet.row(row).default_format = format
end

# 背景色設定
# パレットからの選択(カラーコード指定不可)
# https://github.com/zdavatz/spreadsheet/blob/master/lib/spreadsheet/datatypes.rb#L15-L77
(0..3).each do |col|
  sheet.row(0).update_format(col, pattern_fg_color: :silver, pattern: 1)
end
sheet.row(6).update_format(3, pattern_fg_color: :xls_color_16, pattern: 1)

# フォントカラー設定
sheet.row(6).update_format(3, color: :red)

# 書体変更
sheet.row(6).update_format(3, weight: :bold)

# アラインメント
(0..3).each do |col|
  sheet.row(0).update_format(col, align: :center)
end
sheet.row(6).update_format(0, align: :right)

# 罫線
(0..6).each do |row|
  (0..3).each do |col|
    sheet.row(row).update_format(col, border: :thin)
  end
end

# 書き出し
workbook.write('output/example/spreadsheet.xls')

rubyXL

  • 😄
    • 数式が使える
    • 書式設定系のメソッドに命名規則があって分かりやすい
    • .xlsxが扱えて、読み込み、書き込みができるのはこれだけ
      • テンプレートのExcelファイルを読み込んで必要箇所だけ更新して出力するような用途に向いていそう
  • 🥲
    • デフォルトのフォントが設定できず、行ごと・セルごとに指定する必要がある
    • 罫線を引くとき、セルの上下左右それぞれを個別に指定する必要がある
      • 広範囲に罫線を引くと結講重い
    • 値が入っていないセルの書式をいじろうとしたら怒られる
サンプルコード
require 'rubyXL'
require 'rubyXL/convenience_methods'

# Workbookを作成
workbook = RubyXL::Workbook.new

# Sheet取得
sheet = workbook[0]   # 最初からシートが1つある

# Sheetの名前変更
sheet.sheet_name = 'curry'

# 各セルに書き込み
sheet.add_cell(0, 0, '品名')
sheet.add_cell(0, 1, '単価')
sheet.add_cell(0, 2, '数量')
sheet.add_cell(0, 3, '計')

sheet.add_cell(1, 0, 'にんじん')
sheet.add_cell(1, 1, 80)
sheet.add_cell(1, 2, 1)
sheet.add_cell(1, 3, '', 'B2*C2')

sheet.add_cell(2, 0, 'たまねぎ')
sheet.add_cell(2, 1, 50)
sheet.add_cell(2, 2, 2)
sheet.add_cell(2, 3, '', 'B3*C3')

sheet.add_cell(3, 0, 'じゃがいも')
sheet.add_cell(3, 1, 40)
sheet.add_cell(3, 2, 2)
sheet.add_cell(3, 3, '', 'B4*C4')

sheet.add_cell(4, 0, '牛肉')
sheet.add_cell(4, 1, 200)
sheet.add_cell(4, 2, 1)
sheet.add_cell(4, 3, '', 'B5*C5')

sheet.add_cell(5, 0, 'カレー粉')
sheet.add_cell(5, 1, 150)
sheet.add_cell(5, 2, 1)
sheet.add_cell(5, 3, '', 'B6*C6')

sheet.add_cell(6, 0, '総計')
sheet.add_cell(6, 1, '') # 値を入れないと罫線が引けない
sheet.add_cell(6, 2, '') # 値を入れないと罫線が引けない
sheet.add_cell(6, 3, '', 'SUM(D2:D6)')

# セルのマージ
sheet.merge_cells(6, 0, 6, 2)

# 各行のフォントを変更
(0..6).each do |row|
  sheet.change_row_font_name(row, 'メイリオ')
end

# 背景色設定
(0..3).each do |col|
  sheet[0][col].change_fill('d0d0d0')
end
sheet[6][3].change_fill('b8cce4')

# フォントカラー設定
sheet[6][3].change_font_color('ae2f29')

# 書体変更
sheet[6][3].change_font_bold(true)

# アラインメント
sheet.change_row_horizontal_alignment(0, 'center')
sheet[6][0].change_horizontal_alignment('right')

# 罫線
(0..6).each do |row|
  (0..3).each do |col|
    sheet[row][col].change_border(:top, 'thin')
    sheet[row][col].change_border(:left, 'thin')
    sheet[row][col].change_border(:bottom, 'thin')
    sheet[row][col].change_border(:right, 'thin')
  end
end

# 書き出し
workbook.write("output/example/rubyXL.xlsx")

caxlsx

  • 😄
  • 🥲
    • 勝手に列の幅が調整されてしまう
      • 固定するなら逐一指定する必要がある
    • 基本的にExcelのセル番地?での指定(A1:C1など)になる
      • 行・列のindex値からの変換メソッドを用意すれば解決だけれども
      • (余談)いざ作ろうとしたら案外難しいw
サンプルコード
require 'caxlsx'

# Workbookを作成
package = Axlsx::Package.new
workbook = package.workbook

# Sheetを作成
sheet = workbook.add_worksheet(name: 'curry')

# 各セルに書き込み
sheet.add_row(['品名',        '単価', '数量', '計'])
sheet.add_row(['にんじん',    80,     1,      '=B2*C2'])
sheet.add_row(['たまねぎ',    50,     2,      '=B3*C3'])
sheet.add_row(['じゃがいも',  40,     2,      '=B4*C4'])
sheet.add_row(['牛肉',        200,    1,      '=B5*C5'])
sheet.add_row(['カレー粉',    150,    1,      '=B6*C6'])
sheet.add_row(['総計',        '',     '',     '=SUM(D2:D6)'])

# セルのマージ
sheet.merge_cells('A7:C7')

# 各行のフォントを変更
sheet.add_style('A1:D7', font_name: 'メイリオ')

# 背景色設定
sheet.add_style('A1:D1', bg_color: 'd0d0d0')
sheet.add_style('A7:A7', bg_color: 'b8cce4')

# # フォントカラー設定
sheet.add_style('D7:D7', fg_color: 'ae2f29')

# # 書体変更
sheet.add_style('D7:D7', b: true)

# アラインメント
sheet.add_style("A1:D1", alignment: { horizontal: :center })
sheet.add_style("A7:A7", alignment: { horizontal: :right })

# 罫線
sheet.add_style('A1:D7', border: { style: :thin, color: '000000' })

# 書き出し
package.serialize('output/example/caxlsx.xlsx')

書き込みベンチマーク

実行速度

各操作について実行速度を計測してみました。
Webシステムから出力する際、あまりに遅いと非同期処理にしないといけなくなったりして大変なので、速度は重要ですよね。

シート作成は100枚、セルへの各種操作は100x100のセルに対して行っています。
実際のコードはこちら

## Create Workbook
                 user     system      total        real
spreadsheet  0.000786   0.000017   0.000803 (  0.000793)
rubyXL       0.013436   0.000615   0.014051 (  0.021612)
caxlsx       0.000581   0.000053   0.000634 (  0.000637)

## Create Sheet
                 user     system      total        real
spreadsheet  0.001184   0.000031   0.001215 (  0.001214)
rubyXL       0.040784   0.000804   0.041588 (  0.042620)
caxlsx       0.017650   0.001672   0.019322 (  0.030375)

## Add Cells
                 user     system      total        real
spreadsheet  0.014695   0.000271   0.014966 (  0.014968)
rubyXL       0.354589   0.001620   0.356209 (  0.356193)
caxlsx       0.186781   0.000104   0.186885 (  0.186876)

## Merge Cells
                 user     system      total        real
spreadsheet  0.003145   0.000010   0.003155 (  0.003158)
rubyXL       0.037855   0.000411   0.038266 (  0.038267)
caxlsx       0.045560   0.000038   0.045598 (  0.045594)

## Change Font
                 user     system      total        real
spreadsheet  0.101532   0.000776   0.102308 (  0.102336)
rubyXL       0.824043   0.000607   0.824650 (  0.824719)
caxlsx       0.035586   0.000142   0.035728 (  0.035724)

## Change Fill
                 user     system      total        real
spreadsheet  0.158450   0.000404   0.158854 (  0.158845)
rubyXL       1.028291   0.000648   1.028939 (  1.028883)
caxlsx       0.036306   0.000307   0.036613 (  0.036611)

## Change Font Color
                 user     system      total        real
spreadsheet  0.115407   0.000327   0.115734 (  0.115728)
rubyXL       1.247051   0.001759   1.248810 (  1.248745)
caxlsx       0.038612   0.000194   0.038806 (  0.038802)

## Change Font Color
                 user     system      total        real
spreadsheet  0.105828   0.000548   0.106376 (  0.106370)
rubyXL       0.998762   0.000824   0.999586 (  0.999547)
caxlsx       0.036599   0.000151   0.036750 (  0.036750)

## Change Alignment
                 user     system      total        real
spreadsheet  0.114057   0.000414   0.114471 (  0.114463)
rubyXL       0.712438   0.000425   0.712863 (  0.712828)
caxlsx       0.035214   0.000090   0.035304 (  0.035299)

## Change Border
                 user     system      total        real
spreadsheet  0.218832   0.000549   0.219381 (  0.219381)
rubyXL       4.297522   0.006336   4.303858 (  4.303643)
caxlsx       0.040979   0.000129   0.041108 (  0.041106)

spreadsheetは.xls, rubyXL, caxlsxは.xlsxという違いはありますが、
全体的にrubyXLはちょっと重めかなという感じです。

メモリ使用量

一応、メモリの使用量も見てみました。
なかなか正確な値を出しづらいので、あくまで参考値としてとらえてください。

実行速度の計測で行なっている処理をすべて繋げたもので計測してみました。
実際のコードはこちら

計測方法は2通り。いずれも処理前後の値の比較をしています。

  • すべての生存しているオブジェクトが消費しているメモリ使用量を計測するmemsize_of_allを利用したもの。
  • psコマンドのrss(プロセスが確保している物理使用量)を利用したもの
## spreadsheet
memsize_of_all: 12.07 MB, rss: 19.86 MB

## rubyXL
Bmemsize_of_all: 11.4 MB, rss: 32.3 MB

## caxlsx
memsize_of_all: 5.55 MB, rss: 21.82 MB

劇的な差が出ているわけではないですが、どれも10~数十MBぐらいは使うようですね。

読み込み部門

caxlsxは新規作成専門なので、roo, spreadsheet, rubyXLで比較していきます。

書き込みのときに目標としていたこれを読みこんで、各セルの値を出力してみます。
image.png

roo

  • これだけだとわかりにくいですが、データテーブルを読むのが得意な印象です。
    • streamで少しずつ読み込むことができる
    • csv, matrix, xml, yamlへの変換が可能
    • ヘッダを与えて各行をhashとして取得することも可能
サンプルコード
require 'roo'

# Workbookを読み込み
workbook = Roo::Excelx.new('input/example.xlsx')

# Sheet取得
sheet = workbook.sheet('curry')

# 値の取得
sheet.first_row.upto(sheet.last_row) do |row|
  puts "#{sheet.cell(row, 1)} #{sheet.cell(row, 2)} #{sheet.cell(row, 3)} #{sheet.cell(row, 4)}"
end

spreadsheet

  • 計算式の入力はできませんが、計算式かどうかの判定はできます。
サンプルコード
require 'spreadsheet'

# Workbookを読み込み
workbook = Spreadsheet.open('input/example.xls')

# Sheet取得
sheet = workbook.worksheet('curry')

# 値を取得
sheet.each do |row|
  # 数式が入っているセルは .value で値を取得
  puts "#{row[0]} #{row[1]} #{row[2]} #{row[3].instance_of?(Spreadsheet::Formula) ? row[3].value : row[3]}"
end

rubyXL

  • 計算式の部分は計算後の値を取得してくれます。
サンプルコード
require 'rubyXL'
require 'rubyXL/convenience_methods'

# Workbookを読み込み
workbook = RubyXL::Parser.parse('input/example.xlsx')

# Sheet取得
sheet = workbook['curry']

# 値を取得
sheet.each do |row|
  puts "#{row[0].value} #{row[1].value} #{row[2].value} #{row[3].value}"
end

読み込みベンチマーク

実行速度

10000x100のデータを読み込んで速度を計測しました。
実際のコードはこちら

                 user     system      total        real
roo         25.934566   0.193718  26.128284 ( 26.482589)
spreadsheet  8.964426   0.043074   9.007500 (  9.111481)
rubyxl      75.116438   9.415123  84.531561 ( 90.197534)

spreadsheetが異様に早いですが、これは .xls だからなのか…🤔
.xlsx だけに関していえば、rooのほうが速いですね。

メモリ使用量

同じ処理でメモリ使用量も計測してみました。
実際のコードはこちら

## roo
memsize_of_all: 2.25 MB, rss: 63.52 MB

## spreadsheet
memsize_of_all: 40.79 MB, rss: 45.62 MB

## rubyXL
memsize_of_all: 724.05 MB, rss: 92.21 MB

rooはstreamで読み込み、それ以外は一括での読み込みです。
さすがにrooはメモリの節約ができていますね。
rubyXLはちょっと苦しい…。

Gemの使い分け

それぞれ一長一短があって、何も考えずにこれを選んでおけば問題ない、というものはありません。
選ぶにあたっては、以下の特徴を抑えておくといいかなと思います。

roo

  • 読み込み専用
  • データテーブルを読み込むのが得意
    • streamでの読み込みができる
    • csv, matrix, xml, yamlへの変換も可能

=> Excelからデータを読み込みだけなら、rooを選択しておくと吉

spreadsheet

  • 読み書きができる
  • .xlsしか扱えない

=> 令和の時代はあまり出番がないかもしれませんが、.xlsを扱うならこちら

rubyXL

  • 読み書きができる
  • 計算式の入力や大抵の書式設定が可能
  • 比較的動作は重め

=> 帳票のテンプレートを読み込んで改変して出力するようなタスクにはよさそう

cxlsx

  • 新規作成のみ
  • 処理が速い
  • 書式の設定がやりやすい
  • グラフも作れる

=> リッチな帳票をゼロから作って出力する場合はこれ!

それ以外の方法

Rubyに囚われずとも、場合によっては他のやり方を検討してもいいかもしれません。

Lambdaを利用して別言語で!

S3経由でLambdaにcsvやjsonでデータを渡して、その内容に従ってLambdaでExcelを作成してS3に保存するというやり方もいいかもしれません。

.Net Core

Lambdaであれば、.Net Coreが使えます(使ったことはないですが
Open XML SDK 2.5 for Officeという公式SDKがあるとのことで、これを使えば純正のやり方でExcelファイルが作れてしまいます。
Open XML SDKはちょっとしたことをやるにもかなりのコードを書かないといけないので、これをラップしたClosedXMLというOSSライブラリもあるようです。

Python

Rubyが使えればなんとなく使えるPythonにもライブラリがあります。
openpyxlというライブラリがデファクトらしいです。
読み書きができて、グラフも出せるそうです。

込み入ったことはVBAで!

VBAならExcelでできることは何でもできるんですよね。ちょっと書きづらいですが。
なので、VBAを書いたExcelファイルを作って作業者に配布しておき、システムから簡単なExcelやCSVを出力して、VBAからそれを読み込んでもらって目的のファイルを作成する、という最終手段もあります。

まとめ

8年越しに情報を更新してみましたが、Gemの顔ぶれがほとんど変わっておらず驚きました。
久しぶりに実家に帰ってきたけど何も変わってない!みたいなw

ずっと開発を続けてくれているコミッターの皆様には本当に感謝です🙏

19
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
14