PythonとC++が出来る人はRPAのOSSに貢献チャンスあるよ

# プロローグ

@umireonさんの[貢献チャンスの探し方](https://qiita.com/umireon/items/cbf27deea6ff96d79c1d)を見てふとヒラメキ。

「そういえば、PythonとC++が出来る人にもチャンスあったな。」

と思ったので、紹介します。

# チャンスって？

「Pythonの自動化ライブラリって、C++系のポインタとかの知識が無いと書けなくて、実装が遅れてるんだよね」というチャンスです。

RPAが次第に当たり前のものとなりつつある現代、「[退屈なことはPythonにやらせよう](https://www.amazon.co.jp/dp/4873119278/)」なんてタイトルの本を読んで、業務を自動化しようとしたエンジニアは多いことでしょう。しかし、OSSではまだまだ発展途上の領域であり、「全部入り」みたいな覇権を握るOSSは無い感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/eb2e97b6-b130-d5e6-4455-87af347b18d1.png)


「自動化して、その後のメンテナンスも考えて」となると、Pythonに軍配が上がることは多い印象です。スクリプト言語であることと、自動化用ライブラリの多さが決め手です。また、Pythonが使えるエンジニアの総数が多いこともあります。

::: note
Pythonなどの言語を使わずに、ローコードの <a href="https://powerautomate.microsoft.com/ja-jp/desktop/">PowerAutomate Desktop</a> などでやった方が早いことも多いです。ただし、バージョン管理などの機能が無いことを覚えておきましょう。
:::

# PythonでRPAしてみよう

Pythonに限らず、ソースコードを自分で書いてRPAをする場合、主に環境固有の問題が障壁になりやすいです。
モバイル・デスクトップのアプリを操作する場合のことです。

## 画像認識AIっていいよね

それを解決するのが画像認識技術であり、ここから人工知能ブームが RPA のブームを後押しする形だと分かります。これを最大の売りにした [SikuliX](http://sikulix.com/) がかなり便利です。画像認識機能には Hewlett-Packard と Google の下で強化された [Tesseract](https://github.com/tesseract-ocr/tesseract) が利用されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/3bd524fe-606f-3c4c-c8fc-7fe4543c612b.png)

しかし、[Jython](https://www.jython.org/)なので外部ライブラリの利用方法が複雑であり、カスタマイズ利用の敷居が高かったりします。おかげで適用できる範囲が狭くなっています。Java + Python + C++ ってかなり高難度ですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/cecc8095-6a93-6b5b-3f4f-a6da9bf5834a.png)

## Xpathもあるよね

- 「いやいや、画像認識なんて要らないんだ。」
- 「機械学習するからGPU積めだと？高いんだよ！」
- 「htmlみたいに構造を辿ればいいじゃないか。」

という流派が、SeleniumとかAppiumです（誤解多め）。もともと、Webのランタイムテスト用のテスト環境統合ツールとしてSeleniumがあります。そこに拡張を施して、iOSアプリやAndroidアプリ、Windowsアプリに対しても使えるようにしたものが、Appiumです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134703/a551415c-c4c8-9470-c5e0-c9cf5687b23c.png)


しかし、このAppium、激重いんですね。特にERPとか大きなアプリだと顕著です。Xpathとかカッコいいもの使って、あたかも「UI内を効率よく検索して、目的のUIをハンドリングできる」ように見えます。ところが、ハイスペックPCを所持していないと使い物になりません。RPAプログラムの開発では、実際の業務の流れに沿って操作対象アプリを実際に起動しながら開発するため、遅いと開発効率に直結したりします。モジュール分割により開発速度が改善することは稀でしょう。

# それならPythonだけで解決しよう

だからといって、Python単体でやろうとすると、PythonとWindowsの架け橋がC++なので、Windows自動化系を書くにはC++の知識が必要だったりします。主にポインタ関連。またまた敷居。

# Pythonって自動化ライブラリ無いの？

それなら、自動化用のライブラリを探したらいいのでは？

あります。

- [PyAutoGui](https://pyautogui.readthedocs.io/en/latest/index.html) 画像認識に特化
- [PyWinAuto](https://pywinauto.readthedocs.io/en/latest/index.html) UI操作に特化

てことで、Appiumでやれたこと、SikuliXでできたこと、それぞれのライブラリがあります。
そして、この２つのライブラリ、依存関係が複雑で、一緒に使わないほうがいいです。
「統合しちゃえばいいのに」ですね。

# 統合できないの？

統合というか、PyAutoGUIには近しい予定があります。実現すればPyWinAutoが要らなくなるものです。

## PyAutoGUI のロードマップ

[PyAutoGUI のロードマップ](https://pyautogui.readthedocs.io/en/latest/roadmap.html)を見てみましょう。

## Windowsを制御する機能の実装予定

これらが実装予定の Windows API 関連機能です。

- `pyautogui.getWindows() # returns a dict of window titles mapped to window IDs`

- `pyautogui.getWindow(str_title_or_int_id) # returns a dict of window titles mapped to window IDs`

- `win.move(x, y)`
- `win.resize(width, height)`
- `win.maximize()`
- `win.minimize()`
- `win.restore()`
- `win.close()`
- `win.position() # returns (x, y) of top-left corner`

- `win.moveRel(x=0, y=0) # returns (x, y) of top-left corner`

- `win.clickRel(x=0, y=0, clicks=1, interval=0.0, button=’left’) # returns (x, y) of top-left corner`

- `Additions to screenshot functionality so that it can capture specific windows instead of full screen.`


そこで、「Pythonの自動化ライブラリって、C++系のポインタとかの知識が無いと書けなくて、実装が遅れてるんだよね」という話が出てきます。そうなると、「両方できる人って少ないし、しょうがないよね」とかなってきます。

**これって実は大チャンスなわけですね。**

ドキュメントには、これから実装したい機能のロードマップが掲載されていて、もう何年も達成されていません。主に、Windows API 関連の機能です。ところが、このライブラリの利用者は多く、プルリクやイシューもたくさん来ています。これはチャンス！

# 分かった貢献しよう！でもどうやって？

簡単な流れはこうです。

1. 追加する機能を選ぶ
1. Forkする
1. 機能の実装に使えそうな Windows API 関数を見つける
1. 対応するPythonのラッパー関数を見つける
1. 実装する
1. DocStringを書く
1. ドキュメントを書く
1. PullRequestする

この流れについて、要所の話をしていきましょう。もちろん、Gitの説明は省きます。

# Windows API って？

[Win32 API インデックスポータル](https://docs.microsoft.com/ja-jp/windows/win32/apiindex/api-index-portal)から引用すると、

> Win32 API (Windows API とも呼ばれる) は、Windowsアプリのネイティブプラットフォームです。 この API は、システム機能とハードウェアに直接アクセスする必要があるデスクトップアプリに最適です。 Windows API は、すべてのデスクトップ アプリで使用できます。同じ関数は、通常、32 ビットと 64 ビットのWindowsアプリケーションでサポートされます。

と書いてあります。

Windowsをネイティブレベルで操れるっぽいことが書いてあります。多くのアプリは、こういったネイティブAPIを介して動作しているわけです。もちろん、開発者でも直接触ることは稀で、ReactNative自体やXamarin自体、FlutterForWindows自体の開発者が扱うような領域です。RPAでも、自作アプリ開発でも、ラッパーを介さずにWindows APIが直接使えるなら、できることの幅が広がるのですね。ただし、ラッパーを使うことで得られる利点とのトレードオフは考慮する必要がありそうです。

ということで今回、PythonとC++のなのでラッパーライブラリを使うことになります。

ラップ前の関数のうち、よく使うものが[Windows API インデックス](https://docs.microsoft.com/ja-jp/windows/win32/apiindex/windows-api-list)にまとめられています。

具体的には、[winuser.h header](https://docs.microsoft.com/en-us/windows/win32/api/winuser/)とかにある関数が当てはまります。

- FindWindowA
- FindWindowExA
- FindWindowExW
- FindWindowW

とかです。

# Win API をPythonの中でどうやって使う？

まず、Windows API はC系なので[ctypes](https://docs.python.org/ja/3/library/ctypes.html?highlight=ctypes)が必要になります。次に、Windowなので[WinDll](https://docs.python.org/3/library/ctypes.html?highlight=windll#ctypes.WinDLL)が必要です。

また、ウィンドウを操作する関数が入っているuser32も必要です。

WindowsでPythonをインストールすると、上の３つは標準装備されています。

これにより、例えばC++の[FindWindowW](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-findwindoww)関数を、Pythonでは`ctypes.windll.user32.FindWindowW()`という感じで使えます。また、C++で言う`&num`のようなポインターをPythonで使いたい場合は`ctypes.pointer(num)`を使います。

# Python ライブラリの作成方法

Windows API の使い方はこれで分かりました。次にPythonのライブラリの作り方です。（場所だけ）

[Documentation » Python チュートリアル » 6. モジュール](https://docs.python.org/ja/3/tutorial/modules.html#packages)

そして、Pythonでライブラリの機能を追加したら、ドキュメントも書かなくてはなりません。そこで、

- [sphinx](https://www.sphinx-doc.org/en/master/)
- [mkdocs](https://mkdocs.readthedocs.io/en/stable/)

などの静的サイトジェネレータでドキュメントを管理している部分を書き換えましょう。

# 例えば？

例えば、どんな機能を追加していくのか？
サンプルがあったほうが、分かりやすいですよね。

```py
def GetWindowRectFromName(name: str) -> tuple:
    """
    this can return window rect from window title
    """
    hwnd = ctypes.windll.user32.FindWindowW(0, name)
    rect = ctypes.wintypes.RECT()
    ctypes.windll.user32.GetWindowRect(hwnd, ctypes.pointer(rect))
    # print(hwnd)

    # print(hwnd)

    return (rect.left, rect.top, rect.right, rect.bottom)


if __name__ == "__main__":
    print(GetWindowRectFromName('CALC'))
    pass
```

この`GetWindowRectFromName`関数は、ウィンドウタイトルから、ウィンドウの位置を取ってくれる機能があります。
このように、Forkしたら関数を作ってみて、出来あがったらPullRequestするわけです。

# 2021/11/09 追記

LinuxとMacでも機能する必要のあるものについては、さらなる知識が必要ですね。ただし、これについては、OSによって処理を分岐するように書けば、分業できそうです。

# まとめ

C++とPythonが大好きな皆さん。
または、この記事を読んで、「複数の言語の組み合わさる領域ってチャンスかも？！」と思った、そこのあなた！
そのスキルでOSSに貢献してみてはいかがでしょうか。

**Excelsior!**