1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RubyAdvent Calendar 2024

Day 24

RubyでWin32APIを使ってExcelファイルを読み取る時ハマったこと

Last updated at Posted at 2024-12-23

TL;DR

require 'wrap_excel'

file_path = 'test.xlsx'
workbook = WrapExcel::Book.open(file_path)
sheet = workbook[0]

p sheet[0, 1].value
p sheet[20, 10].value

巨大Excelの読み取りは確実に早くなりますが、以下のことを注意すべきです。

  • ファイルの新規作成には適していない
  • 大量データの書込みに適していない
  • ExcelインストールしたWindowsでしか動かない

はじめに

先日投稿した RubyXLでExcelファイルを読み取る時ハマったこと では、お馴染み?のGem RubyXLが巨大(数MB~数百MB)Excelファイルを読み込む速度が遅いとわかりました。

別の記事では、PythonはWin32APIを通して、Excelアプリケーションを使ってExcelファイルを操作できると知り、Excelアプリケーションを使っているであれば加速できると思い、Rubyでも同等なライブラリーがあるかを調べました。

Pythonと違い、RubyでWin32APIをアクセス機能は標準ライブラリーWIN32OLEとして提供されています。

またExcel操作に特化したラッピングライブラリー、WrapExcel日本の有志より開発されました。

方法&結果

Excel生成

まずは先日と同じように、巨大?Excelファイルの生成です。

create_xlsx_rubyXL.rb
require 'benchmark'
require 'rubyXL'

file_path = 'test.xlsx'

# Create a test file
COL_SIZE = 50
ROW_SIZE = 10000

def init_xlsx(col_num, row_num)
  workbook = RubyXL::Workbook.new
  sheet = workbook[0]
  add_cells(sheet, col_num, row_num)

  workbook
end

def add_cells(sheet, col_num, row_num)
  (0...row_num).each_with_index do |row, i|
    sheet.add_cell(row, 0, i)
    (1...col_num).each_with_index do |col, j|
      sheet.add_cell(row, col, "#{i}:#{j}")
    end
  end
end

Benchmark.bm do |x|
  x.report("Create Xlsx by RubyXL: ") do
    workbook = init_xlsx(COL_SIZE, ROW_SIZE)
    workbook.write(file_path)

    puts "#{COL_SIZE}*#{ROW_SIZE}"
  end
end
> ruby .\create_xlsx_rubyXL.rb
       user     system      total        real
Create Xlsx by RubyXL: 50*10000
  5.766000   0.422000   6.188000 ( 22.548951)

前回の倍の50列x10000行のExcelファイルを生成しましたが、ファイルサイズは1.6MB程度でExcelの世界ではぜんぜん「巨大」とは言えません。

スクリーンショット 2024-12-15 225739.png
スクリーンショット 2024-12-15 225730.png

PS: WrapExcelを使った生成の方法は文末で紹介しますが、訳あっておすすめしません。

Excelアプリで開く

画面録画 2024-12-15 233603.gif

体感的には4,5秒程度で開けました。

WrapExcel(Win32API)で開く

read_xlsx_WarpExcel.rb
require 'benchmark'
require 'wrap_excel'

file_path = 'test.xlsx'

Benchmark.bm do |x|
  x.report("Read by WrapExcel: \n") do
    workbook = WrapExcel::Book.open(file_path)
    sheet = workbook[0]

    p sheet[0, 1].value
    p sheet[20, 10].value
  end
end
> ruby .\read_xlsx_WarpExcel.rb
       user     system      total        real
Read by WrapExcel:
"0:0"
"20:9"
  0.031000   0.031000   0.062000 (  7.501389)

読み取り時間はExcelアプリよりは時間かかったものの、僅か7.5秒で前回の50*5000のファイルよりも早く開けました。

RubyXLで開く

対照実験として前回と同じように読み込んでみます。

read_xlsx_rubyXL.rb
require 'benchmark'
require 'rubyXL'

file_path = 'test.xlsx'

Benchmark.bm do |x|
  x.report("Read by RubyXL: \n") do
    workbook = RubyXL::Parser.parse(file_path)
    p workbook[0][0][1].value
    p workbook[0][20][10].value
  end
end
> ruby .\read_xlsx_rubyXL.rb
       user     system      total        real
Read by RubyXL:
"0:0"
"20:9"
 24.156000   0.297000  24.453000 ( 45.481205)

ご覧の通り、45秒もかかりました。

余談: WrapExcelでExcelファイルの生成

  • 爆遅なので、100行生成するデモのみします
  • WrapExcelはゼロからExcelファイルを生成することはできないので、生のWIN32OLEで生成することとなります
create_xlsx_WrapExcel.rb
require 'benchmark'
require 'wrap_excel'
require 'win32ole'

FILE_PATH = 'test2.xlsx'

# Create a test file
COL_SIZE = 50
ROW_SIZE = 100

# wrap_excel does not support creating a new file, Create a new file by WIN32OLE
def init_new_xlsx(file_path)
  excel = WIN32OLE.new('Excel.Application')
  wbook = excel.Workbooks.Add

  file_path = File.expand_path(file_path)
  file_path = WrapExcel::Cygwin.cygpath('-w', file_path) if RUBY_PLATFORM =~ /cygwin/
  fp = WIN32OLE.new('Scripting.FileSystemObject').GetAbsolutePathName(file_path)

  excel.DisplayAlerts = false
  wbook.SaveAs(fp)
  wbook.Close
  excel.Quit
end

def open_xlsx(file_path, col_num, row_num)
  unless File.exist?(file_path)
    init_new_xlsx(file_path)
  end

  workbook = WrapExcel::Book.open(file_path, read_only: false)
  sheet = workbook[0]
  add_cells(sheet, row_num, col_num)

  workbook.save
  workbook.close
end

def add_cells(sheet, col_num, row_num)
  (0...row_num).each_with_index do |row, i|
    sheet[row, 0] = i
    (1...col_num).each_with_index do |col, j|
      sheet[row, col] = "#{i}:#{j}"
    end
  end
end

Benchmark.bm do |x|
  x.report("Create Xlsx By WrapExcel: ") do
    open_xlsx(FILE_PATH, COL_SIZE, ROW_SIZE)

    puts "#{COL_SIZE}*#{ROW_SIZE}"
  end
end
> ruby .\create_xlsx_WrapExcel.rb
       user     system      total        real
Create Xlsx By WrapExcel: 50*100
  1.171000   0.515000   1.686000 (111.216136)

100行だけのファイルを生成するにも111秒使いました。理由としてはセルの書き込みごとにWin32APIをCallしているせいかと推測しています。

考察

Excelアプリの助力で、Win32APIを使ったExcelファイルの読み取りは予想通り早かったです。しかしシステムコールを使った故、頻繫にデータを書き込むことに適していないようです。

また、当然なことに、Win32APIはWindowsでしか動かない上、Excel系のAPIを使うにはExcelアプリ(Microsoft Office)を購入する必要があります。

VBAを書かないで済むという点では有用かもしれませんが、ニッチな使い道しか適していないようです。

参考

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?