UWSC を Python で置換しよう（5） Robotを作ってみよう

# はじめに
「UWSC を Python で置換しよう」第五回です。今回は実際にロボットを作っていきます。いろいろあって投稿がしばらくできませんでした。

<font color=red>前回同様なにぶん、調べながら書いているため、間違っている点もあるかと思います。
その場合は、ビシバシ編集リクエストをください(汗
</font>

何気に、UWSCで検索したら、以下のサイトに補足されていたようなので、フォーラムに書いてあった欲しい機能を実装してみようと思う

CSWU～どうする　UWSCなしの互換システム
https://wiki3.jp/CSWU

前回は
[UWSC を Python で置換しよう（4） チートシート[2]](https://qiita.com/hirohiro77/items/0b7fa5bea1024b39af6b)
次回は
<font color=red>未定</font>

# 足りない機能を作ろう

UWSCにあって、今回の環境にない機能のいくつかを作りこみます。作る機能は、

- 1.画像検索(フォーカス)ハイライト
- 2.要素検索(フォーカス)ハイライト
- 3.GETID/CLKITEMの互換機能

なお、機能作成にあたり、以下のサイトには大変お世話になりました。作りたい機能があったら、参考にしてみるといいと思います。

Python Example
https://www.programcreek.com/python/

## 1.画像検索(フォーカス)ハイライト

画像検索をした際に、画面のどこでマッチしたのかを、画面上に矩形を描くことでわかりやすくします
win32guiをインポートして、(left,top,right,bottom)を与えると、検出した場所をハイライトしてくれるようにします。矩形の線種や色を変える場合は`win32ui.CreatePen`,`win32api.RGB`,`pyhandle`,`win32con`で設定が必要になります

```func_dhlight.py
import win32gui

##デスクトップのハイライト
def dhlight(x1, y1, x2, y2):
    hwnd=win32gui.WindowFromPoint((x1,y1))
    hdc=win32gui.GetDC(hwnd)
    rc=(x1, y1, x2, y2)
    win32gui.DrawFocusRect(hdc, rc)
    win32gui.ReleaseDC(hwnd,hdc)

if __name__ == '__main__':
    ##pyautoguiのlocateOnScreenと組み合わせて使う感じですね
    ##ただし、locateOnScreenはBox(left, top, width, height)を返すので
    pos_x,pos_y,size_w,size_h  = pyautogui.locateOnScreen('target.png')
    x1 = pos_x
    y1 = pos_y
    x2 = pos_x + size_w
    y2 = pos_y + size_h
    dhlight(x1, y1, x2, y2)
    ##と書けば、target.pngが見つかったところに点線で矩形が表示されます。
```



## 2.要素検索(フォーカス)ハイライト

ブラウザ上の要素を検索した際、どの要素を選択したのか、ハイライト表示します
こちらは、seleniumのcss挿入を利用しますので、cssの書き換えを禁止していたり、動的なUI/UXの場合は反応しないかもしれません(いっそ要素の座標とってwin32guiで描画したほうがいいかも)
css("background: white; border: 1px solid blue;")を差し込んでいるだけなので、好みで変更できます。

```func_hhlight.py
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException

##Html Elementのハイライト
def hhlight(element):
    driver = element._parent
    def apply_style(s):
        driver.execute_script("arguments[0].setAttribute('style', arguments[1]);",element, s)
    original_style = element.get_attribute('style')
    apply_style("background: white; border: 1px solid blue;")
    time.sleep(.3)
    apply_style(original_style)


if __name__ == '__main__':
    driver = webdriver.Chrome()
    driver.implicitly_wait(10) 
    driver.set_page_load_timeout(5)
    driver.set_script_timeout(5)
    driver.get('https://www.google.com/')
    
    ## こんな感じに、要素を検索して、作った関数に要素を渡すと指定したCSSが適用されて、
    ## どの要素が選択されているのかわかるようになります
    search_box = driver.find_element_by_name("q")
    hhlight(search_box)
```

## 3.GETID/CLKITEMの互換機能
UWSCではGETIDでウインドウIDを取得して操作しましたが、これをPythonでそのまま行うのは難しいので、ウインドウ名からプロセスIDを取得(get_pid)とプロセスIDからウインドウハンドルを取得(get_hwnds)する関数を作って、対応することにした。

```func_getid.py
import os,sys,re,time,subprocess
import win32api, win32gui, win32con, win32process

def get_pid(title):
    hwnd = win32gui.FindWindow(None, title)
    threadid,pid = win32process.GetWindowThreadProcessId(hwnd)
    return pid

def get_hwnds(pid):
    def callback(hwnd, hwnds):
        if win32gui.IsWindowVisible(hwnd) and win32gui.IsWindowEnabled(hwnd):
            _, found_pid = win32process.GetWindowThreadProcessId(hwnd)
            if found_pid == pid:
                hwnds.append(hwnd)
        return True
    hwnds = []
    win32gui.EnumWindows(callback, hwnds)
    return hwnds 

def clkitem(win_title,itemid):
    #ウインドウタイトルで探す
    hwnd = win32gui.FindWindow(0, win_title)

    #ウインドウのアイテムをリスト化
    inplay_children = []
    def is_win_ok(hwnd, *args):
        s = win32gui.GetWindowText(hwnd)
        inplay_children.append(hwnd)
    win32gui.EnumChildWindows(hwnd, is_win_ok, None)

    #アイテムIDの番号を指定し、アイテム/ボタンのハンドルを取得
    button_hwnd = inplay_children[itemid]

    #ウインドウをアクティブにする
    win32gui.SetForegroundWindow(hwnd)
    #指定したボタンをクリック（押下->解除を送信)
    win32api.PostMessage(button_hwnd, win32con.WM_LBUTTONDOWN, 0, 0)
    win32api.PostMessage(button_hwnd, win32con.WM_LBUTTONUP, 0, 0)

#ウインドウIDからプロセスIDを取得
pid = get_pid(u"電卓")

#電卓の1つめのボタンをクリック
clkitem(u"電卓",1)

#プロセスIDからハンドルを取得して操作[GETCTLHND]と同等
for hwnd in get_hwnds(pid):
    if hwnd == 0:
        print(u"ウインドウが見つからない")
    else:
        #サイズ指定(リサイズ)
        win32gui.MoveWindow(hwnd, 100, 100, 500, 500, True)
        #最大化
        win32gui.ShowWindow(hwnd, win32con.SW_MAXIMIZE)
        time.sleep(1)
        #閉じる
        win32gui.SendMessage(hwnd,win32con.WM_CLOSE,0,0)
```

これで、画面内のネイティブアプリのリサイズや移動、フォーカスが可能になりました
ブラウザに限ればseleniumで行った方が簡単ですね。

# ロボットを作ってみよう

## <<動作シナリオ>>

[]で括られた部分は使うモジュール名

- 1.ログイン情報の入ったini(setting.ini)を読み込む : [ConfigParser]
- 2.ブラウザを開いて、最大化した後、iniのlogin_urlに移動 : [Selenium]
- 3.ログイン情報を利用してログイン : [Selenium]
- 4.指定の画面(投稿)に移動 : [Selenium / win32gui]
- 5.記入情報一覧の入ったExcelファイルを開く [xlrd]
- 6.1行ずつ、入力フォームに転記 [Selenium]
- 7.Excelファイルの最終行を検知したら、画面を閉じて終了 [Selenium]

## 設定ファイル(setting.ini)

```setting.ini
[Server]
login_id = user01
login_pw = password
login_url = https://127.0.0.1/wp/wp-login.php

[SitePage]
write_fourm = 投稿

[WriteFile]
excel_file = input_list.xlsx
```

```input_list.xlsx
[Sheet1]
title,text,tag,format
テスト1,初めての投稿1,未分類,標準
テスト2,初めての投稿2,未分類,標準
テスト3,初めての投稿3,未分類,標準
テスト4,初めての投稿4,未分類,標準
テスト5,初めての投稿5,未分類,標準
テスト6,初めての投稿6,未分類,標準
```

## ロボットソース

実際に、シナリオに合わせて、ソースを書いていきます。本当はもっと、関数とか作って読みやすく作ればいいのですが、シナリオが追いやすいように、フラットに書いています。なお、UTF-8で書いていますので注意してください

```wordpress_auto_post.py
# -*- coding: utf-8 -*-

#モジュール読み込み
import os,sys,re,time,subprocess
import pyautogui
import win32gui
import configparser
import xlrd
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.alert import Alert
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.select import Select

##デスクトップのハイライト
def dhlight(x1, y1, x2, y2):
    hwnd=win32gui.WindowFromPoint((x1,y1))
    hdc=win32gui.GetDC(hwnd)
    rc=(x1, y1, x2, y2)
    win32gui.DrawFocusRect(hdc, rc)
    win32gui.ReleaseDC(hwnd,hdc)

##Html Elementのハイライト
def hhlight(element):
    driver = element._parent
    def apply_style(s):
        driver.execute_script("arguments[0].setAttribute('style', arguments[1]);",element, s)
    original_style = element.get_attribute('style')
    apply_style("background: white; border: 1px solid blue;")
    time.sleep(.3)
    apply_style(original_style)

# 設定ファイル読み込み
CONF_FILEPATH = 'setting.ini'
config = configparser.ConfigParser()
config.read( CONF_FILEPATH, 'UTF-8')

#confファイルで[]で囲った場所を指定
config_server = config['Server']
config_page   = config['SitePage']
config_excel  = config['WriteFile']

#confで[]の下に変数とデータを入れてる内容を取得
uid = config_server['login_id']
upw = config_server['login_pw']
url = config_server['login_url']
search_button = config_page['write_fourm']
xlsxfile = config_excel['excel_file']

##Chromeを初期化
options = Options()

##Chromeのパス（通常は指定不要、ポータブル版などを使う場合やstable/beta切り替え時）
options.binary_location = 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
options.add_argument('--start-maximized')
options.add_argument('--disable-gpu')

## ChromeのWebDriverオブジェクトを作成する。
driver = webdriver.Chrome(options=options,executable_path="C:\Python37\chromedriver.exe")

#要素が見つかるまでの待ち時間、ドライバ生成直後にのみ指定可能
driver.implicitly_wait(10) 

#ページが完全にロードされるまで最大で5秒間待つよう指定、ドライバ生成直後にのみ指定可能
driver.set_page_load_timeout(5)

#Javascript実行が終了するまで最大5秒間待つように指定
driver.set_script_timeout(5)

#URL移動
driver.get(url)

#ログイン処理
user_id_elm  = driver.find_element_by_id("user_login")
#エレメントハイライト
hhlight(user_id_elm)
user_id_elm.send_keys(uid)

user_pw_elm  = driver.find_element_by_id("user_pass")
hhlight(post_fourm)
user_pw_elm.send_keys(upw)

login_submit = driver.find_element_by_id("wp-submit")
hhlight(post_fourm)
login_submit.submit()

time.sleep(5)

#投稿
menu_post_elm = driver.find_element_by_xpath("/html/body/div[1]/div[1]/div[2]/ul/li[3]/a/div[3]")
hhlight(menu_post_elm)
menu_post_elm.click()

time.sleep(2)

#新規追加
new_post_elm = driver.find_element_by_xpath("/html/body/div[1]/div[2]/div[2]/div[1]/div[3]/a")
hhlight(new_post_elm)
new_post_elm.click()

time.sleep(2)

#Excelファイルを開く
wb = xlrd.open_workbook(xlsxfile)

#シート指定
sheet = wb.sheet_by_name('sheet1')

#最終行検出
last_row = sheet.nrows

#全行2次元配列に読み込み(メモリに余裕がなければ、1行ずつ読んだ方がいい)
readcells = [sheet.row_values(row) for row in range(sheet.nrows)]

time.sleep(5)

#記事タイトル
card_title_elm = driver.find_element_by_xpath("//*[@id='post-title-0']")

#記事本文
card_body_elm = driver.find_element_by_xpath("/html/body/div[1]/div[2]/div[2]/div[1]/div[3]/div[1]/div/div/div/div[2]/div[3]/div/div[1]/div/div/div[2]/div[1]/div[3]/div/div/div/div/div/div/p")

# driverを終了
driver.close()
```

## ロボットを動かす

```
python ./wordpress_auto_post.py
```

問題なけれな動いて投稿されるはず。

# 最後に

UWSCがなくなったのは本当に痛い、もっと簡単な自動化ツールが出てくるといいですね。
次回は何か思いついたら続編を書くと思います。