Pythonを利用して簡単にお手製RPAを作成する


この記事は、RPA (Robotic Process Automation) Advent Calendar 2020 の 24 日目の記事です。

この記事の3行まとめ

  • Python の pywinauto というライブラリを利用して、かんたんな RPA を自作します
  • サンプルシナリオとして、Windows の電卓の操作を自動化します
  • 最後に pywinauto の技術的な仕様についても言及します

はじめに

巷に流通している RPA ツールを利用すれば、当然業務の自動化を行うことができます。しかし、プログラミング言語とそのライブラリを活用すれば、RPA ツールを利用しなくても、無料でカジュアルに業務自動化を行うことができます。
今回は、Python というプログラミング言語と、 pywinauto というライブラリを利用して、Windows 上のアプリケーションの操作の自動化を行ってみます。

対象

  • Python を利用した業務自動化に興味がある方
  • 手元に RPA ツールはないが、なんとかして業務を自動化したい方
  • RPA ツールの導入前に、手軽に「自動化」を試してみたい方

用意するもの

  • Windows 10 がインストールされたPC

pywinauto を利用して、電卓の操作を自動化する

1. 各種ライブラリなどのインストール

Python

まずは Python をインストールします。Python の公式ページからインストールしても良いですが、わかりにくいので、以下のページからインストールします。

非公式Pythonダウンロードリンク

インストール方法については、こちらのサイトが詳しいです。"Add Python 3.x to PATH" にチェックを入れるのを忘れないようにしましょう。

インストールが終わったあとは、コマンドプロンプトを立ち上げ、python --version と入力します。以下のように、インストールした Python のバージョンが表示されれば、インストールは成功です。

> python --version
Python 3.8.5

pipenv

Python のパッケージ管理システムである pipenv をインストールします。コマンドプロンプトを立ち上げ、以下のコマンドでインストールを行います。

> pip install pipenv

pipenv の初期化を行います。
まずは、作業用のフォルダを作成します。場所はどこでも構いません。その後、コマンドプロンプトで当該 フォルダまで移動し、以下のコマンドを叩きます。作成したフォルダ内に、 Pipfile というファイルが作成されます。

> pipenv --python 3

pywinauto

実際にアプリケーションの操作の自動化を担うライブラリである piwinauto をインストールします。インストールは以下のコマンドで行います。

> pipenv install pywinauto

2. 電卓の操作を自動化するプログラムを書く

以下のプログラムにより、「Windowsの電卓を起動して 1+2 を計算する」ことができます。
適当なファイル名(例えば calc.py)で保存します。

from time import sleep
from pywinauto import Desktop, Application

app = Application(backend="uia")
app.start("calc.exe")

dlg = Desktop(backend="uia")["電卓"]

dlg['1'].click()
sleep(1)
dlg['プラス'].click()
sleep(1)
dlg['2'].click()
sleep(1)
dlg['等号'].click()
sleep(1)

dlg.close()

3. プログラムを実行する

プログラムの実行は、以下のコマンドで行うことができます。

> pipenv run python calc.py

うまく行けば、自動で電卓が立ち上がり、1+2 を計算してくれるはずです。

pywinauto の技術的な背景

pywinauto を利用する上で、知っておいたほうがよい技術的な背景をいくつかご紹介します。
以下の情報は、多くの場合 公式ドキュメント に記載されています。詳細な説明は公式ドキュメントを確認して下さい。

要素特定技術について

pywinauto では、「Win32 API」と「UI Automation」の、2つの要素特定技術を利用することができます。どちらを利用するかは、自動化したいアプリケーションに依存します。「Win32 API」を利用したほうが要素特定がしやすい場合と、「UI Automation」を利用したほうが要素特定がしやすい場合があります。

Win32 API を利用したい場合は、以下のようにアプリケーションを立ち上げます。

from pywinauto import Application
app = Application(backend="win32").start("notepad.exe")

一方で、UI Automation を利用したい場合は、以下のようにアプリケーションを立ち上げます。

from pywinauto import Application
app = Application(backend="uia").start("notepad.exe")

デフォルトは、Win32 API となっています。

個人的な実感として、最近作成されたモダンなデスクトップアプリケーションを自動化する場合は、多くの場合「UI Automation」を利用したほうが、要素特定がしやすい傾向にある気がします。UI Automation を利用した要素特定については、以下の Qiita 記事が詳しいです。

RPAの画面要素分解機能でいろいろなアプリの画面を分解してみた
https://qiita.com/Okura_/items/4406e3de8a6582948526

余談ですが、Win32 API と、MSAA(UI Automationの前身となる技術)が同じものなのかなと勝手に思い込んでいたのですが、どうやらそれは違うようです。

MSAA is not the same as backend="win32" in pywinauto.
https://github.com/pywinauto/pywinauto/issues/268#issuecomment-261468161

属性解決マジック(Attribute Resolution Magic)について

pywinauto では、操作対象の要素を特定するために、属性解決マジックと呼ばれる技術を利用しています。
先ほどの電卓を例に考えます。「1」のボタンを特定するために、上記のプログラムでは以下の記述を行いました。

dlg['1']

この記述は非常に直感的ですが、以下の方法でも要素を特定することができます。

dlg['1Button']

または、以下の記述でも可能です。24という数字は、電卓アプリケーションの内部構造的に、「1」ボタンが24番目のボタンであることを意味していると考えられれます。

dlg['Button24']

信じがたいことに、以下でも「1」ボタンを捉えることができます。

dlg['11']

このように、pywinauto では、要素を特定する方法は一意ではありません。公式ドキュメントでは、「タイプミスや表記ゆれに強いマッチングアルゴリズムを利用している」と記載されています(アルゴリズムの中身までは読めていません)。

But fortunately pywinauto uses “best match” algorithm to make a lookup resistant to typos and small variations.
https://pywinauto.readthedocs.io/en/latest/getting_started.html#attribute-resolution-magic

どのような単語を利用すれば、要素を特定する事ができるかを調べるためには、print_control_identifiers() メソッドを利用します。
以下は、電卓ダイアログに対して、メソッドを適用したときの返り値です。

>>> dlg.print_control_identifiers()
Control Identifiers:

Dialog - '電卓'    (L2780, T141, R3200, B816)
['電卓', 'Dialog', '電卓Dialog', '電卓0', '電卓1', 'Dialog0', 'Dialog1', '電卓Dialog0', '電卓Dialog1']
child_window(title="電卓", control_type="Window")
   |
   | Dialog - '電卓'    (L3004, T142, R3192, B174)
   | ['電卓2', 'Dialog2', '電卓Dialog2']
   | child_window(title="電卓", auto_id="TitleBar", control_type="Window")
   |    |
   |    | Menu - 'システム'    (L0, T0, R0, B0)
   |    | ['システムMenu', 'システム', 'Menu', 'システム0', 'システム1']
   |    | child_window(title="システム", auto_id="SystemMenuBar", control_type="MenuBar")
   |    |    |
   |    |    | MenuItem - 'システム'    (L0, T0, R0, B0)
   |    |    | ['MenuItem', 'システム2', 'システムMenuItem']
   |    |    | child_window(title="システム", control_type="MenuItem")
   |    |
   |    | Button - '電卓 の最小化'    (L3054, T142, R3100, B174)
   |    | ['電卓 の最小化', 'Button', '電卓 の最小化Button', 'Button0', 'Button1']
   |    | child_window(title="電卓 の最小化", auto_id="Minimize", control_type="Button")
   |    |
   |    | Button - '電卓 を最大化する'    (L3100, T142, R3146, B174)
   |    | ['電卓 を最大化するButton', '電卓 を最大化する', 'Button2']
   |    | child_window(title="電卓 を最大化する", auto_id="Maximize", control_type="Button")
   |    |
   |    | Button - '電卓 を閉じる'    (L3146, T142, R3192, B174)
   |    | ['電卓 を閉じる', '電卓 を閉じるButton', 'Button3']
   |    | child_window(title="電卓 を閉じる", auto_id="Close", control_type="Button")

...

   |    |    |    | Button - '1'    (L2792, T665, R2889, B733)
   |    |    |    | ['1', '1Button', 'Button24']
   |    |    |    | child_window(title="1", auto_id="num1Button", control_type="Button")

...

この出力結果から、1, 1Button, Button24 などの表記によって、「1」ボタンを特定できることが読み取れます。

pywinauto ライブラリのクラス構造について

pywinautoで操作する対象が、どのようなクラスに属しているか検証してみます。

上記で説明した電卓アプリケーションを例に考えます。電卓が起動した状態で、PythonのREPLで以下のような操作を行うことにより、pywinauto 上で、電卓の「1」ボタンに対応するオブジェクトが、どのクラスに属しているのかを確認することができます。

>>> dlg = Desktop(backend="uia")["電卓"]
>>> dlg["1"].wrapper_object() # 電卓の「1」のボタンの実態を確認する
<uia_controls.ButtonWrapper - '1', Button, -2595060702488467549>
>>> dlg["1"].wrapper_object().__class__
<class 'pywinauto.controls.uia_controls.ButtonWrapper'>

どうやら、「1」ボタンは、pywinauto 内では、uia_controls.ButtonWrapper クラスのインスタンスとして定義されている事がわかります。

このクラスの祖先をたどってみると、以下のような出力を得ます。

>>> dlg["1"].wrapper_object().__class__.mro()
[<class 'pywinauto.controls.uia_controls.ButtonWrapper'>, <class 'pywinauto.controls.uiawrapper.UIAWrapper'>, <class 'pywinauto.base_wrapper.BaseWrapper'>, <class 'object'>]

結論として、pywinautoに登場するクラスの構造ですが、ざっくり以下のようになっています。この図は、すべてのクラスを表現している訳ではないことに注意してください。

操作対象がどのクラスに属しているかを知ることで、その操作対象に対して、どのようなメソッドを利用できるのかを理解することができます。各クラスに属するインスタンスに対して、どのようなメソッドが利用できるのかについては、こちらのドキュメント から確認することができます。