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

# TL;DR

```rb
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ファイルを読み取る時ハマったこと](https://qiita.com/jerrywdlee/items/a086b8443401eaede6fe)** では、お馴染み?のGem [RubyXL](https://github.com/weshatheleopard/rubyXL)が巨大(数MB~数百MB)Excelファイルを読み込む速度が遅いとわかりました。

[別の記事](https://www.cfxlog.com/python_win32com_excel/#rtoc-5)では、PythonはWin32APIを通して、Excelアプリケーションを使ってExcelファイルを操作できると知り、Excelアプリケーションを使っているであれば加速できると思い、Rubyでも同等なライブラリーがあるかを調べました。

Pythonと違い、RubyでWin32APIをアクセス機能は標準ライブラリー[WIN32OLE](https://docs.ruby-lang.org/ja/latest/class/WIN32OLE.html)として提供されています。

またExcel操作に特化したラッピングライブラリー、[WrapExcel](https://github.com/tomiacannondale/wrap_excel/)も[日本の有志](https://tomiacannondale.hatenablog.com/entry/20111126/1322329654)より開発されました。

# 方法＆結果
## 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
```

```powershell
> 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87438/55f35432-edd2-fbcf-3304-ecbe9f4d1699.png)
![スクリーンショット 2024-12-15 225730.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87438/eb23b974-540e-6fbf-ecba-7789d118f915.png)

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

## Excelアプリで開く
![画面録画 2024-12-15 233603.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87438/e665af7a-7732-01da-6f9e-cdac7b3930e3.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
```

```ps
> 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
```

```ps
> 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
```

```ps
> 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を書かないで済むという点では有用かもしれませんが、ニッチな使い道しか適していないようです。


# 参考

- [Pythonライブラリ(Win32 API)：pywin32(Window/Office操作)](https://note.com/kiyo_ai_note/n/n1947ba674482)
- [Excelファイル(xls, xlsx)をRubyで2次元配列として読み書きする](https://qiita.com/giiko_/items/1ba5f929ba9dd9b0356c)
- [Ruby で Win32 API - 金子邦彦研究室](https://www.kkaneko.jp/pro/ruby/rubywin32api.html)