RPA を自作していて、「すぐにウィンドウや画像は出てこないけど、一定時間待つのだと不安定。安定するまで待機時間を延ばすと今度は待ちすぎてしまう」という悩みはよくあることです。筆者も、割とよく遭遇します。これは Python に限らずおよそ全ての RPA ツールで共通ですね。
GUIの RPA ツールでは、専用の「ウィンドウを待つ」や「画像の表示を待つ」コマンドが用意されていることがほとんどですが、 Python はプログラミング言語ですので、このくらいは自分で書く必要があります。プログラミングの考え方の勉強としては、最適なテーマと言えます。
今回利用するのは、前回学習したif文に加え、for文によるループ・繰り返しです。この2種類の組み合わせは、プログラミングの世界では初級から上級まで常について回る基本的な構造ですので、しっかり学習しましょう。とくに、if文の応用として条件の「逆転」も学習します。
本記事は、 Python で RPA を書きつつ、 Python の基本を学習する欲張り連載の1部です。インストールから学習したい! という方は連載をご覧ください。
- Python での forループの記述の仕方
- 指定のウィンドウがアクティブになるのを待つ方法
- 指定のウィンドウが非アクティブになるのを待つ方法
- 名前を付けて保存の確認ダイアログを高速で解決する方法
- 指定の画像が表示されるのを待つ方法
今回は実例が多めになりますので、頑張りましょう。
Python で指定回数繰り返す for ループの記述の仕方
for ループとは、多くのプログラミング言語で、同じ処理を繰り返すときの基本的なループ構造の書き方です。ただ、厳密に解説してしまうと少し難しくなってしまうので、ここでは「指定回数だけ繰り返す」forループの書き方を学習します。
chapter-9-1.py という名前でPython ファイルを新規作成してください(分かれば名前はなんでもいいです)。今回のchapterの中でファイルを分けて置くと、後で「あれ、どうやって書いたっけ?」というときに、自分の書いたプログラムを見直せて便利ですよ。筆者楽介は、いろんな言語をつまみ食いするので、しょっちゅう自分の書いたプログラムを引っ張り出して書き方を思い出します……。
import time
for n in range(10):
print('回数: ', n)
time.sleep(0.5)
print('終了')
if 文の時と同様、字下げ・インデントには気をつけて入力してください。こういった記述上のルールもあるので、コピー&ペーストではなく、手で入力してください。
PowerShellから、chapter-9-1.pyを実行すると、「回数: 」が0~9の10回、最後に「終了」と出力されて終了します。
もちろん、出力はprint関数からですが、変数 n に数字を代入はしていないですよね? 実は、これもfor文の効果です。
for文の基本構造を整理するとこのようになります。range 関数はとてもシンプルな関数なのですが、そこを含めて解説すると混乱しやすいので、ここでは「お約束」として、かならず使うものとして覚えて下さい。つまり、
- for ■ in range(●): が for 文の基本形
- ■には「好きな」変数を指定すると、繰り返し回数が分かる数字が入っている。
- ●に繰り返したい回数を入力する
- 繰り返したい部分は、半角スペースやタブ文字で字下げ・インデントをする
となっています。
Python では、for文やif文のようにソースコードを「グループ化する」ために、字下げ・インデントを使用して「ブロック」にします。詳しくは、下記記事で復習できます。
ここで、今回のソースコードを見ると、確かに繰り返し回数に10が指定されていて、変数には n が指定されていることが分かります。
実行が分かりやすいように、time.sleepを入れている以外はprintだけが字下げされていて、10回繰り返されているので、期待通り動いていますね。
「あれ? でも 10 で終わってないよ?」
と思うかもしれません。よく見ると数字が 0 から始まっていますね? プログラムの世界では、数字は「0から始まる」のが基本です。そのため、0~9で10回の繰り返しとなっています。
理解を深めるために、数字を色々変えて、動作を確認してみてくださいね。
他のプログラミング言語では、for 文というと、「数字を1ずつ足していく」形式が多いです。
「あれ? Python でも1ずつ増えて行ってるよね?」と思うかもしれませんが、実は同じ変数(n)に数字を「足して」いるわけではないのです。他のプログラミング言語経験者は、ここで少し戸惑うこともあるかもしれません(筆者は、別の書き方あるんじゃないかな、と調べました)。
これを説明するには、配列やタプルといったデータ構造の話になってしまうので、ここでは range関数に繰り返したい回数を入力すればいい、と覚えていてください。他の言語と並行して勉強している方は「そういうもんなんだ」くらいで大丈夫です。
指定のウィンドウがアクティブになるのを待つ方法
指定のウィンドウがアクティブになっているか確認する方法は、前回学習しました。そして、前項で、0.5秒間隔で print文を10回実行することに成功しましたから、 for 文と組み合わせることでアクティブになるまで待つことができそうです。
では、chapter-9-2.py とpython ファイルを作成して作って行きます。この項では動作の理解を深めるために、少しずつ追記する形で進めます。
import pygetwindow as gw
import time
import sys
for n in range(30):
a_win = gw.getActiveWindow()
if a_win.title == '無題 - メモ帳':
print('メモ帳がアクティブになりました')
time.sleep(0.5)
print('終了')
実行前に、まずは字下げ・インデントの量を確認してください。
for 文のループの中に if 文があるので、if 文も半角スペース4つ分、インデントされています。
さらに、メモ帳がアクティブになったときだけ実行したい print 関数については、半角スペース 8つ分インデントされています。
このようにして、どの構造の中に入っているかということが目で見て分かりやすくなっています。普通のWordなどで書く文章でも、特にリストなどで、
- 字下げ1段目
- 更に補足要素
のような構造を作ることがあると思いますが、それをプログラミング言語でも行っているだけ、と考えると分かりやすいと思います。
では、プログラムを入力したら実行して、実行中にメモ帳を起動してアクティブにしてみてください。どうでしょう、動作したでしょうか?
この RPA・プログラムの問題
実行すると分かると思いますが、このプログラムには問題が複数あります。
まずは何より、メモ帳がアクティブになってもループが終わらないことです。延々と、「メモ帳がアクティブになりました」と表示されます。また、途中でメモ帳が非アクティブになるとメッセージが途絶えます。これでは、アクティブになったから次の RPA を実行……という訳にはいきませんね。
また、メモ帳のタイトルバーの内容が変わると反応しない点も問題です。
このように、ウィンドウのタイトルは頻繁に変わります。変わらないアプリケーションの方が少ないでしょう。
条件を満たしたら、ループを抜けたい
まずは一つ目の問題に対処していきます。(無題の)メモ帳がアクティブになっているかどうかの判断はできていますから、 for 文を終了できればいいですね。ではどうしましょう? nが29までカウントされたら終わるのですから、 n = 29 や n = 30 と代入すれば止まるでしょうか? なかなかいいアイディアですが、残念ながら、この方法では止まりません。
ちゃんと、ループを終了しますよ、という命令が用意されています。それが、 break です。() も : も必要ありません。
ということで、 chapter-9-2.py の、print(‘メモ帳がアクティブになりました’)の次の行に、break と書いて以下のようにします。
import pygetwindow as gw
import time
import sys
for n in range(30):
a_win = gw.getActiveWindow()
if a_win.title == '無題 - メモ帳':
print('メモ帳がアクティブになりました')
break
time.sleep(0.5)
print('終了')
インデントの量に気をつけてくださいね。ループから抜けたいのは、 if文の条件を満たしたときだけですので、インデントの量は8です。これを4にしてしまうと、実質的にはループなど全くないことになってしまいます(ですので、事実上break は if 文とほぼセットで登場します)。
修正したものを実行してみましょう。
期待通りに、メモ帳がアクティブになったらループを抜け、すぐに「終了」と表示され、Pythonが終了すると思います。
新たな問題にif else で対応する
しかし、実はここで新しい問題が起きています。それは、「時間切れ(30回ループ)でループを抜けたのか」「ウィンドウがアクティブになったループを抜けたのか」分からないという問題です。正確に言うと、それによって処理を切り替えられないということです。
RPA を実現するという本連載の目的を考えると、ウィンドウがアクティブにならないということはなんらかのトラブルが起きていると考えられるので、「エラーとして終了」したり、「メモ帳を起動しようとしたり」したいですね。
とりあえず、ここでは区別だけすることにします。それには、 if 文に else: 節を付け加えます。これは日本語に訳すと「そうでなければ」という意味になり、if 文の条件を満たしていたら、ifの直後のインデントの塊(ブロック)を実行し、満たしていなかったら、else: の直後のブロックを実行します。
動作させてみないと分かりづらいと思うので、Python ファイルを書き換えて実行してみましょう。
import pygetwindow as gw
import time
import sys
for n in range(30):
a_win = gw.getActiveWindow()
if a_win.title == '無題 - メモ帳':
print('メモ帳がアクティブになりました')
break
time.sleep(0.5)
a_win = gw.getActiveWindow()
if a_win.title == '無題 - メモ帳':
print('メモ帳がアクティブでループを終わりました')
else:
print('メモ帳が非アクティブでループを終わりました')
sys.exit(0)
print('終了')
print(‘終了’)の前に、if else文と、その中身を追記しています。また、30回目のtime.sleep(0.5)中にメモ帳がアクティブになることも考えて、もう一度 a_win 変数にアクティブなウィンドウを格納しています。
実行すると、期待通りに動作するでしょうか?(もし、ループを時間切れまで待つのが面倒な場合は、rangeの数字を小さくしてみてください)
概ね、予想通りに動作したと思います。ループの中でメモ帳がアクティブになっていると、その後もアクティブでループを終わった、と出ています。反対に、ループ中にprint関数が動作していない方は、メモ帳が非アクティブでループを終わりました、とだけ出ていますね。
そして、その次の行にも注目してもらいたいのですが、メモ帳がアクティブになった方は最後のprint関数の「終了」が出力され、アクティブにならなかった方は出力されていない点です。
これは、else のブロックにある、sys.exit()関数の働きで、Pythonのプログラムを強制的に終了する効果があります。そのため、 RPA でどうしてもメモ帳がアクティブにならなかったら、以後の操作は行わないで終了したい、という時に使用できます。
また、このexit関数は、chapter-9-2.py の先頭でimport している sysライブラリの関数です。このライブラリは、timeと同様、組み込みのライブラリなのでpipでのインストールは必要ありません。sysライブラリ自体は、パソコンOSの、プログラムを動かすための基本的な機能を呼び出すためのライブラリです。
プログラミングの本当の最初の最初は、分からない用語・ちゃんと理解できない部分が沢山あります。というのも、プログラムを書くということは、パソコンの中を作る、DIYで材料から家具を作るようなものだからです。
家具を作るときも、何故そうしなければいけないかは分からないけれど、やるときちんと仕上がるといったことがあります。
そういった細かな部分にはきちんと理由・理屈がありますが、最初のうちは気にしていても仕方がありません。もっと高度なものを、自分で作れるようになる段階で理解できるようになっていくので、本記事で敢えて軽く流している部分は、「今はいいんだな」と思っていてください。
メモ帳という文字が含まれていたら見つかったことにする
PyGetWindowの getWindowsWithTitle() 関数では、「メモ帳」という文字列がタイトルに含まれているウィンドウを全て探していました。
今回は、ウィンドウを探すのではなく、アクティブなウィンドウに指定した文字列が含まれているかを調べます。それには、今まで == としていた演算子(計算の記号)を in 演算子に変えます。使い方を見た方が早いですので、以下のようにプログラムを書き換えて実行してみましょう。
import pygetwindow as gw
import time
import sys
for n in range(30):
a_win = gw.getActiveWindow()
if ' - メモ帳' in a_win.title:
print('メモ帳がアクティブになりました')
break
time.sleep(0.5)
a_win = gw.getActiveWindow()
if ' - メモ帳' in a_win.title:
print('メモ帳がアクティブでループを終わりました')
else:
print('メモ帳が非アクティブでループを終わりました')
sys.exit()
print('終了')
== が in に変わっていることと、a_win.title 変数と ’ – メモ帳’の順番が入れ替わっていること。後はもちろん、条件の文字列から「無題」が取り除かれていることに注目してください。
‘ – ‘ が残っているのは、他のウィンドウを誤認してしまう可能性を減らすためです。例えば、ブラウザで「メモ帳の隠れた機能を発見!」なんていう記事を表示していたとします。そのときに、アクティブなウィンドウ判定で「メモ帳」という文字列が含まれているとします。そうすると、ブラウザをアクティブにしてもメモ帳がアクティブになった! と誤解してしまうことになってしまいます。
それを避けるために、敢えて 「- メモ帳」と、自然な感覚とはちょっと違った指定をしています。
さて、 == を in に変えるだけでなく、 a_win との位置を入れ替えているのは、 in 演算子が算数でいう引き算と同様に、順番が大事な演算子だからです。
日本語だと、「a_win.title の中に ‘ – メモ帳’という文字列が入っているか?」という語順が自然ですが、英語だと逆です。Is the string ” – メモ帳” included in a_win.title? という感じになるでしょうか。プログラミングの世界は英語が標準なので、こういった私たちの自然な感覚とずれた書き方がしばしばあります。が、そういうものだと思って覚えてください。
プログラムを実行し、メモ帳で文字を入力して * をつけたり、ファイルを保存して「無題」でなくしてみたりして実験してください。きちんと動作することが分かると思います。
指定のウィンドウが非アクティブになるのを待ち、名前を付けて保存の確認に対応する
今度はウィンドウが非アクティブになるのを待ちたいと思います。どういうときに使うか? ですが、前回の 「Python の RPA で「名前を付けて保存」の確認を攻略する」で見たような、出るか出ないか、動作させるまで分からないようなダイアログの対応が代表的です。
「名前を付けて保存」ダイアログ以外がアクティブになることは確実ですが、そのままメモ帳に戻るか、「名前を付けて保存の確認」ダイアログが出るかは分からない。
もちろん、for 文の中に2つのif 文を入れることも可能です。ですが、for 文のブロックが長くなるとプログラムが読みづらくなる面もありますので、勉強のために書いてみましょう。
さて、アクティブなウィンドウが何か? は分かりましたが、非アクティブなウィンドウはどうやって探しましょう。getNoneActiveWindows…なんていう関数はありません。
ここでは、少しプログラム的な考え方を使います。特定のウィンドウAが非アクティブということは、「アクティブなウィンドウは、特定のウィンドウAではない」ということができそうです。これをプログラムの世界では、論理否定と言います。
Python では論理否定を意味する演算子は、 not 演算子と言います。完全に、英語の否定ですね。
では、「名前を付けて保存の確認」ダイアログに対応する RPAを書き換えてみましょう。chapter-9-3.py として次の Python プログラムを作成します。
import pyautogui as ag
import pygetwindow as gw
import time
import pyperclip as cb
import sys
memo = gw.getWindowsWithTitle('メモ帳')[0]
memo.activate()
ag.hotkey('ctrl', 'shift', 's')
for n in range(10):
a_win = gw.getActiveWindow()
if a_win == '名前を付けて保存':
break
time.sleep(0.1)
#「名前を付けて保存」ダイアログを待つ
ag.press('backspace')
cb.copy('test2.txt')
time.sleep(0.1)
ag.hotkey('ctrl', 'v')
time.sleep(0.3)
ag.press('enter')
time.sleep(0.5)
#「名前を付けて保存」ダイアログはここまでのはず
for n in range(20):
a_win = gw.getActiveWindow()
if not(a_win == '名前を付けて保存'):
break
#名前を付けて保存がアクティブな状態の反対である
time.sleep(0.1)
a_win = gw.getActiveWindow()
if a_win.title == '名前を付けて保存の確認':
ag.press('y')
for n in range(20):
a_win = gw.getActiveWindow()
if not(a_win == '名前を付けて保存の確認'):
break
#名前を付けて保存の確認がアクティブな状態の反対である(メモ帳に戻っている)
time.sleep(0.1)
cb.copy('保存しました\n')
ag.hotkey('ctrl', 'v')
sleep()関数で長めに待機していた部分を、全てfor文を利用したものに書き換えています。
さらに、「名前を付けて保存」ダイアログが閉じられた後、エンターキーを入力した後の、for 文の中のif文に注目してください。
if not(a_win == '名前を付けて保存'):
となっていますね。これが、 not 演算子の使い方です。丸括弧()があるので、関数のように見えますが、これは数学でいう丸括弧と同じ意味で、丸括弧の中身を優先して計算する(処理する)という意味です。
つまり、先に a_win == ‘名前を付けて保存’をして、同じかどうか判断してから、not演算子でそれを反対にしています。「あっていたら間違っている」といい、「間違っていたらあっている」と言う、という感じですね。それにより、if 文は逆の動きをするようになっています。
そして、for文を抜けた後で、改めて「名前を付けて保存の確認」ダイアログがアクティブだったら、「yキーを押して上書き保存をし、また名前を付けて保存の確認ダイアログが閉じる」を待っています。
実はこの部分は
for n in range(20):
a_win = gw.getActiveWindow()
if a_win.title == '名前を付けて保存の確認':
ag.press('y')
#名前を付けて保存の確認がアクティブになったから、yキーで上書きする
for n in range(20):
a_win = gw.getActiveWindow()
if ' - メモ帳' in a_win:
break
#メモ帳に戻っている
#breakは、一番内側のforを終了するので、ここでは名前を付けて保存の確認ダイアログを待つ部分しか抜けない
time.sleep(0.1)
break
#ここで、「名前を付けて保存の確認」が消えるのを待つループを抜ける
if ' - メモ帳' in a_win:
break
#メモ帳がアクティブになったからループを抜ける
time.sleep(0.1)
このようにすることで、not演算子を使わずに「名前を付けて保存の確認」ダイアログが表示されてもされなくても動作する形式にすることができます。
しますが、ご覧の通りとても分かりづらいです。プログラムが実行される順番すら、コメントなしでは分からなくなってしまいます。
こういったことを避けるために、次に行く前に一度ループを終了するということも重要です。
同じようなものを何度も書くのは大変ですし、例えばメモ帳からサクラエディタに切り替えたときに、プログラムを全部書き換えるのはとても大変です。
そこで、 Python (に限らずプログラミング言語には)関数という仕組みが用意されています。
とても重要な概念のため、下記の記事(連載の次回)で解説しています。
指定の画像が表示されるのを待つ方法
では、今度は指定の「画像」が表示されるまで待つ方法を見てみましょう。
とはいえ、 Python で書く RPA の基本構造はまったく変わりません。for 文でループを作って、if 文で判断します。
「Python で画像認識 RPA の作成(PyAutoGUI使用)」で作成した、メモ帳のアイコンをクリックする RPA を、「メモ帳のアイコンが表示されるまで待って、表示されたらクリックする」 RPA にしたいと思います(Webブラウザの操作や、ゲームで使えそうですね)。
画像を検索するのは、locateOnScreen関数を使用するのでした。chapter-9-4.py として、以下の Python プログラムを作成します。
import pyautogui as ag
import pygetwindow as gw
import time
import sys
for n in range(20):
memoicon = ag.locateOnScreen(r'C:\Users\your_path\memoicon.png')
print(memoicon)
if memoicon:
icon_center = ag.center(memoicon)
ag.click(icon_center)
break
time.sleep(0.5)
#locateOnScreenの動作が遅いので、もっと待つように感じる
chapter-9-3.pyと比較して、とても短くてシンプルですね!
注目して欲しいのは、memoiconという変数に、locateOnScreen()関数の結果を代入している点です。
実行してみると、以下のようにprint文が動作するのが分かると思います。
最後のBox(left…)というのが、実際に見つかったループの回です。それ以外の時は、None となっていますね。
これは、nullオブジェクトといい、綴りの通り「何もないよ」ということを示しています。アイコンが見つからないので、None, 分かりやすいですね。
そして、この None は if 文に入れた場合は、 == で同じでなかったときと同じような動きをします。厳密には、 == で違ったときは真偽値のFalseという結果になるので、同じではないですが、if文では同じように使えるのだ、となんとなく理解していてください。
したがって、if文に単に memoicon 変数を渡した場合、Noneのときには実行されず、Box…というものになったら、「とりあえずなんかあるから実行する」ということになります。
そのため、アイコンがクリックされて、Python の RPA が終了した、ということです。
おわりに
ここまで頑張った方、お疲れ様でした。
ループは Python, RPA, プログラミング、すべてにおいて重要事項なので、いくつものパターンを作成して使用イメージを掴めるようにしてみました。
- 繰り返しには for 文
- 回数指定には、range()関数を使う
- 文字列の中に、文字列が含まれているか、というときには in 演算子を使う
- 逆のことを if 文で言いたい場合は not 演算子を使う
- 算数や数学と一緒で、丸括弧()は計算の優先順位を決める
- None という、「何もないよ」ということを示す特別な値がある
こうしてみると盛りだくさんでしたね! しかし、ループとif文を組み合わせるといよいよ「プログラミングを自分で書いている!」という感覚が味わえたと思います。これを使って日々のお仕事を楽に! 効率的にしていってください。
また、「プログラミングが楽しく、仕事にできるかも?」と思えたら、転職活動をするのも一つの手段です。
未経験から最速で転職できる転職サービスがありますので、もし転職したい! となったら相談してみるのもひとつの手段だと思います。
転職にはどうしてもリスクはつきものですが、転職活動であれば特にリスクはありません。特に、このブログでプログラミングや RPA を学習しながら、プログラマーになれそうなタイミングをうかがうと、学習にも転職にも、両方に意欲が持てていい効果が期待できます。