これは LibreOffice Advent Calendar 2020 の20日目の記事です。
前の記事は nogajun さんの「XMLを表で編集したりCSVに書き出したいときはLibreoffice Calcの「XMLソース」を使えば一撃です」です。
PyCall.rb は Ruby から Python を利用するためのブリッジライブラリです。詳しくは下記を参照してください。
- mrkn/pycall.rb: Calling Python functions from the Ruby language
-
(2017) Ruby-Pythonブリッジライブラリ「PyCall」を使ってRubyでデータ分析をしよう! (1/3):CodeZine(コードジン)
- PyCall.rb 作者の mrkn さんによる解説記事
「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 詳しくなくて、見様見真似でやっています。ひょっとしたらおかしなことをやっている部分があるかもしれません。
下記のような操作をやってみます。
- sample.ods ファイルを開く
-
Sheet1
シートのA1
セルに入っている数を読んで - それに 1 足した数で
A1
セルを更新 - ファイルを保存
- ファイルを閉じる
# 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
の内容
を標準出力にダンプするスクリプトを書いてみました。
# 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 関連で書いたもの