LoginSignup
3

Ruby+PyCall.rbでLibreOffice Calcのオートメーションをやってみた(Ubuntu 18.04)

Last updated at Posted at 2020-12-20

これは LibreOffice Advent Calendar 2020 の20日目の記事です。

前の記事は nogajun さんの「XMLを表で編集したりCSVに書き出したいときはLibreoffice Calcの「XMLソース」を使えば一撃です」です。


PyCall.rb は Ruby から Python を利用するためのブリッジライブラリです。詳しくは下記を参照してください。

「Ruby PyCall LibreOffice」でググってみたところ、まだ事例がないようだったので試してみました。

準備

まっさらな環境で試したいので Docker を使います。
素の Ubuntu 18.04 でも大体同じだと思いますが、適宜 apt 〜sudo apt 〜 に読み替えるなどしてください。


雑な Dockerfile。

FROM ubuntu:18.04

RUN apt-get update
RUN apt-get -y install libreoffice-calc
RUN apt-get -y install ruby
RUN apt-get -y install build-essential

WORKDIR /root/work

イメージ作成+コンテナ起動。以降はファイルの編集を除いてコンテナ内での作業です。

docker build -t libo_pycall:trial .
docker run --rm -it -v "$(pwd):/root/work/" libo_pycall:trial bash

バージョンを確認。

root@2377c5b80dfb:~/work# python3 -V
Python 3.6.9
root@2377c5b80dfb:~/work# ruby -v
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux-gnu]

今回は pyenv, rbenv などは使わず、素の Python/Ruby でやってみます。

まずは Python で書いてみる

Ruby + PyCall を試す前に、まずは Python でサンプルを書いて動かしてみます。ちなみに私は Python 詳しくなくて、見様見真似でやっています。ひょっとしたらおかしなことをやっている部分があるかもしれません。

下記のような操作をやってみます。

  1. sample.ods ファイルを開く
  2. Sheet1 シートの A1 セルに入っている数を読んで
  3. それに 1 足した数で A1 セルを更新
  4. ファイルを保存
  5. ファイルを閉じる
# sample.py

import uno

def get_desktop():
  local_ctx = uno.getComponentContext()
  resolver = local_ctx.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", local_ctx)
  ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")
  return ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)

def open_ods_file(path):
  desktop = get_desktop()
  url = uno.systemPathToFileUrl(path)
  return desktop.loadComponentFromURL(url, "_blank", 0, ())

doc = open_ods_file("/root/work/sample.ods")
sheet = doc.Sheets.getByName("Sheet1")

cell = sheet.getCellByPosition(0, 0)
n = int(cell.getFormula())
print(n)
cell.setFormula(int(n) + 1)

doc.store()
doc.dispose()

LibreOffice インスタンスを起動(最初に一度だけ実行)

soffice --headless "--accept=socket,host=localhost,port=2002;urp;" &

sample.py を実行

python3 sample.py

sample.py を複数回実行し、実行するたびに数値が1ずつ増えていけば成功です。

PyCall のインストール

とりあえず gem install。

gem install --pre pycall

失敗しました。

Fetching: pycall-1.3.1.gem (100%)
Building native extensions. This could take a while...
ERROR:  Error installing pycall:
        ERROR: Failed to build gem native extension.

    current directory: /var/lib/gems/2.5.0/gems/pycall-1.3.1/ext/pycall
/usr/bin/ruby2.5 -r ./siteconf20201217-110-g260nd.rb extconf.rb
mkmf.rb can't find header files for ruby at /usr/lib/ruby/include/ruby.h

extconf failed, exit code 1

Gem files will remain installed in /var/lib/gems/2.5.0/gems/pycall-1.3.1 for inspection.
Results logged to /var/lib/gems/2.5.0/extensions/x86_64-linux/2.5.0/pycall-1.3.1/gem_make.out

ruby.h が見つからないと言われました。 ruby-dev をインストールしてやりなおし。

apt install ruby-dev
gem install --pre pycall

インストール成功。

root@fac43278ae75:~/work# ruby -r pycall -e 'p PyCall::VERSION'
"1.3.1"

軽く確認

pycall.rb の README に載っているコード例で軽く確認してみます。

root@2377c5b80dfb:~/work# irb --prompt simple
>> require 'pycall/import'
=> true
>> include PyCall::Import
=> Object
>> pyimport :math
Traceback (most recent call last):
  File "/var/lib/gems/2.5.0/gems/pycall-1.3.1/lib/pycall/python/investigator.py", line 1, in <module>
    from distutils.sysconfig import get_config_var
ModuleNotFoundError: No module named 'distutils.sysconfig'
Traceback (most recent call last):
        8: from /usr/bin/irb:11:in `<main>'
        7: from (irb):3
        6: from /var/lib/gems/2.5.0/gems/pycall-1.3.1/lib/pycall/import.rb:18:in `pyimport'
        5: from /var/lib/gems/2.5.0/gems/pycall-1.3.1/lib/pycall.rb:62:in `import_module'
        4: from /var/lib/gems/2.5.0/gems/pycall-1.3.1/lib/pycall/init.rb:16:in `const_missing'
        3: from /var/lib/gems/2.5.0/gems/pycall-1.3.1/lib/pycall/init.rb:35:in `init'
        2: from /var/lib/gems/2.5.0/gems/pycall-1.3.1/lib/pycall/libpython/finder.rb:41:in `find_libpython'
        1: from /var/lib/gems/2.5.0/gems/pycall-1.3.1/lib/pycall/libpython/finder.rb:36:in `find_python_config'
PyCall::PythonNotFound (PyCall::PythonNotFound)

pyimport でエラーになりました。
python3-distutils をインストールしてやりなおし。

apt install python3-distutils

今度は成功しました。

root@2377c5b80dfb:~/work# irb --prompt simple
>> require 'pycall/import'
=> true
>> include PyCall::Import
=> Object
>> pyimport :math
=> :math
>> math.sin(math.pi / 4) - Math.sin(Math::PI / 4)           
=> 0.0
>> 

サンプルスクリプトを Ruby に移植

では Python で書いたスクリプトを Ruby で書き直しましょう。

# sample.rb

require "pycall/import"
include PyCall::Import

pyimport "uno"

def get_desktop
  local_ctx = uno.getComponentContext()
  resolver = local_ctx.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", local_ctx)
  ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")
  ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
end

def open_ods_file(path)
  desktop = get_desktop()
  url = uno.systemPathToFileUrl(path)
  desktop.loadComponentFromURL(url, "_blank", 0, [])
end

doc = open_ods_file("/root/work/sample.ods")
sheet = doc.Sheets.getByName("Sheet1")

cell = sheet.getCellByPosition(0, 0)
n = cell.getFormula().to_i
puts n
cell.setFormula(n + 1)

doc.store()
doc.dispose()

Python 版では desktop.loadComponentFromURL(url, "_blank", 0, ()) のように3つ目の引数として空のタプルを渡していましたが、ここは空配列にしました。それ以外は Python 版とほとんど同じです。

参考: LibreOffice: XComponentLoader Interface Reference

実行。

ruby sample.rb

Python 版と同じように、実行するたびに 1 増える動きになりました。大丈夫そうです。

サンプルその2

もう一つ別の例ということで、シート Sheet2 の内容

image.png

を標準出力にダンプするスクリプトを書いてみました。

# sample2.rb

require "json"

require "pycall/import"
include PyCall::Import

pyimport "uno"

def get_desktop
  local_ctx = uno.getComponentContext()
  resolver = local_ctx.ServiceManager.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", local_ctx)
  ctx = resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")
  ctx.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
end

def open_ods_file(path)
  desktop = get_desktop()
  url = uno.systemPathToFileUrl(path)
  desktop.loadComponentFromURL(url, "_blank", 0, [])
end

def all_select_cursor(sheet)
  range = sheet.getCellRangeByName("A1")
  cursor = sheet.createCursorByRange(range)
  cursor.gotoEndOfUsedArea(true)
  cursor
end

def get_col_index_max(sheet)
  all_select_cursor(sheet).Columns.Count - 1
end

def get_row_index_max(sheet)
  all_select_cursor(sheet).Rows.Count - 1
end

doc = open_ods_file("/root/work/sample.ods")
sheet = doc.Sheets.getByName("Sheet2")

(0..get_row_index_max(sheet)).each do |ri|
  cols = 
    (0..get_col_index_max(sheet)).to_a.map do |ci|
      cell = sheet.getCellByPosition(ci, ri)
      cell.getFormula()
    end
  puts JSON.generate(cols)
end

doc.dispose()

実行。

root@fac43278ae75:~/work# ruby sample2.rb 
func=xmlSecCheckVersionExt:file=xmlsec.c:line=188:obj=unknown:subj=unknown:error=19:invalid version:mode=abi compatible;expected minor version=2;real minor version=2;expected subminor version=25;real subminor version=26
["id","name","score","note"]
["1","foo","12.3","hoge"]
["2","bar","-12.3",""]

ヨシ!

おわりに

というわけで、インストールまわりでちょっとだけひっかかりましたが、簡単なものであればすんなり動かせると分かりました。
PyCall.rb すばらしいですね……。


LibreOffice Advent Calendar 2020 向けに下記の記事も書きました。あわせてどうぞ。

関連

JRuby + Java版SDK の組み合わせも良い選択肢だと思います。

他に LibreOffice 関連で書いたもの

他に Ruby 関連で書いたもの

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
What you can do with signing up
3