プログラムでおかえしできるかな

定年を過ぎて何かの役に立てないかなと始めた元SEのブログです

pywinautoでRPA(自動化)◇ブラウザ編【Python】

このエントリーをはてなブックマークに追加

pywinauto は Python で RPA を実現するパッケージです。
ブラウザを対象に RPA を作りました。
pywinauto はブラウザで表示されたサイトの細かい操作は苦手ですが、ブラウザ自体の操作は可能です。

目次

◆アプリのサンプル画像と機能・特長

▷アプリ起動後の画面

【機能・特長】

  • ブラウザ(ChromeFirefox)を操作できます
  • 一つのタブを選択して操作します
  • 次の操作を提供
    • スクロール(上下)
    • 先頭、末尾へ移動
    • 次のリンクへカーソル移動
    • 戻るボタン
    • リンク先を表示
  • はてなブログ グループサイトの「次のリンク」ボタンに対応
  • ページ内の検索を提供

【考え方】

  • 調査の結果、ブラウザの操作はキー入力で行う
  • ブラウザは自薦に起動しておく
  • ブラウザとタブ名を指定して操作対象を探す

プログラムの作り方について知りたい方は次へ

早速、アプリを使ってみたい方はこちらへ「アプリの取得」 追加:2023-05-27

◆pywinautoの基本的な使い方

▶pywinauto の基本的な使い方は、こちらの記事を参照してください。
 📖 pywinautoでRPA(自動化)◇導入編【Python】 🔗

説明している内容は次の通りです。

【pywinautoの基礎】

【待機】

タブブラウザの特徴を理解する

pywinauto でアプリの制御をおこなう場合、アプリがどのような構成であるかを知っておく必要があります。
事前に知っている必要はなくて、pywinauto で調べていきます。

◇複数ウィンドウ

複数のウィンドウを表示している場合、アクティブタブの名称とブラウザの名称でアプリを指定

ブラウザのウィンドウを複数起動している場合、pywinauto には次のように見えます。

確認には Desktop オブジェクトを使います。
Application オブジェクトだと connect したウィンドウしか見えないためです。

▼確認コマンド

from pywinauto import Desktop
app = Desktop(backend="uia")
    
for win in app.windows():
    print(win)

▼結果例(Chrome のウィンドウを2つ、Firefox を1つ表示している場合)

uiawrapper.UIAWrapper - '', Pane
uiawrapper.UIAWrapper - 'C:\Windows\system32\cmd.exe - python', Dialog
uiawrapper.UIAWrapper - 'Python - はてなブログ グループ - Google Chrome', Pane
uiawrapper.UIAWrapper - '年代 - はてなブログ グループ - Google Chrome', Pane
uiawrapper.UIAWrapper - 'Python - はてなブログ グループ — Mozilla Firefox', Dialog
uiawrapper.UIAWrapper - 'Program Manager', Pane

▶アプリへの接続、ダイアログの指定方法

  • connect() メソッドや window() メソッドでは、title_re 引数でタイトルの一部を正規表現でマッチングさせると良い
    例:window(title_re='.*Chrome')
  • ウィンドウのタイトルにはカレントのタブ名が表示される
  • 複数ウィンドウを表示している場合、ブラウザ名だけで指定すると複数該当の例外(例外処理を参照⤵)が発生

◇複数タブ

複数タブは TabItem コントロールを指定

ブラウザの一つのウィンドウに複数タブを表示している場合、複数のダイアログがあるのではなく、複数の TabItem コントロールが存在します。

▼確認コマンド

  • print_control_identifiers() メソッドで確認します
from pywinauto.application import Application
app = Application(backend='uia')
app.connect(title_re='.*Chrome')
dlg = app.window(title_re='.*Chrome')

dlg.print_control_identifiers()

▼結果(Chrome でタブを2つ表示している場合)

  • タブ以外に何を表示しているかによって出力内容は変わります
  • 以下はタブの情報が分かる部分だけを切り抜いてあります
  • タブには「はてなブログ グループ」のサイトを表示しています
  • タブは「TabItem」という要素であることが分かります
  • ここで出力される要素の [] で挟まれた文字列は、要素の名前解決に使えます
Control Identifiers:

Pane - '年代 - はてなブログ グループ - Google Chrome'    (L960, T0, R1920, B1040)
['Pane', '年代 - はてなブログ グループ - Google Chrome', '年代 - はてなブログ グループ - Google ChromePane', 'Pane0', 'Pane1']
child_window(title="年代 - はてなブログ グループ - Google Chrome", control_type="Pane")
   | 
   | Pane - ''    (L968, T0, R1912, B1032)
   | ['Pane2']
   |    | 
   |    | Pane - ''    (L968, T0, R1912, B1032)
   |    | ['Pane2']
   |    |    | 
   |    |    | Pane - ''    (L968, T13, R1912, B1032)
   |    |    | ['Pane4']
   |    |    |    | 
   |    |    |    | Pane - ''    (L968, T13, R1912, B86)
   |    |    |    | ['Pane5']
   |    |    |    |    | 
   |    |    |    |    | TabControl - ''    (L968, T13, R1809, B48)
   |    |    |    |    | ['TabControlタブを検索', 'TabControl', 'TabControl新しいタブ']
   |    |    |    |    |    | 
   |    |    |    |    |    | Pane - ''    (L968, T13, R1463, B48)
   |    |    |    |    |    | ['Pane6']
   |    |    |    |    |    |    | 
   |    |    |    |    |    |    | Pane - ''    (L968, T13, R1463, B48)
   |    |    |    |    |    |    | ['Pane6']
   |    |    |    |    |    |    |    | 
   |    |    |    |    |    |    |    | TabItem - 'Python - はてなブログ グループ'    (L968, T13, R1224, B48)
   |    |    |    |    |    |    |    | ['Python - はてなブログ グループTabItem', 'Python - はてなブログ グループ', 'TabItem', 'TabItem0', 'TabItem1']
   |    |    |    |    |    |    |    | child_window(title="Python - はてなブログ グループ", control_type="TabItem")
   |    |    |    |    |    |    |    |    | 
   |    |    |    |    |    |    |    |    | Button - '閉じる'    (L1188, T13, R1224, B48)
   |    |    |    |    |    |    |    |    | ['閉じる2', '閉じるButton2', 'Button4']
   |    |    |    |    |    |    |    |    | child_window(title="閉じる", control_type="Button")
   |    |    |    |    |    |    |    | 
   |    |    |    |    |    |    |    | TabItem - '年代 - はてなブログ グループ'    (L1207, T13, R1463, B48)
   |    |    |    |    |    |    |    | ['年代 - はてなブログ グループ', 'TabItem2', '年代 - はてなブログ グループTabItem']
   |    |    |    |    |    |    |    | child_window(title="年代 - はてなブログ グループ", control_type="TabItem")
   |    |    |    |    |    |    |    |    | 
   |    |    |    |    |    |    |    |    | Button - '閉じる'    (L1427, T13, R1463, B48)
   |    |    |    |    |    |    |    |    | ['閉じる3', '閉じるButton3', 'Button5']
   |    |    |    |    |    |    |    |    | child_window(title="閉じる", control_type="Button")
   |    |    |    |    |    | 
   |    |    |    |    |    | Button - '新しいタブ'    (L1463, T13, R1499, B48)
   |    |    |    |    |    | ['新しいタブ', '新しいタブButton', 'Button6']
   |    |    |    |    |    | child_window(title="新しいタブ", control_type="Button")
   |    |    |    |    |    | 
   |    |    |    |    |    | Pane - ''    (L1499, T13, R1773, B48)
   |    |    |    |    |    | ['Pane9']
   |    |    |    |    |    | 
   |    |    |    |    |    | MenuItem - 'タブを検索'    (L1773, T16, R1801, B44)
   |    |    |    |    |    | ['MenuItem2', 'タブを検索', 'タブを検索MenuItem']
   |    |    |    |    |    | child_window(title="タブを検索", control_type="MenuItem")
   |    |    |    |    | 

▼descendants()メソッドを使った調査

  • 同様のことはdescendants() メソッドを使ってもわかります
    descendants() メソッドは子孫の要素を出力します
from pprint import pprint
pprint(dlg.descendants())

▼結果

[<uiawrapper.UIAWrapper - '', TitleBar, -8033474080583220560>,
 <uia_controls.MenuWrapper - 'システム', Menu, -9139694015942147616>,
 <uia_controls.MenuItemWrapper - 'システム', MenuItem, -3864871990355589628>,
 <uia_controls.ButtonWrapper - '最小化', Button, 7629479337271264987>,
 <uia_controls.ButtonWrapper - '最大化', Button, -4309623980336288204>,
 <uia_controls.ButtonWrapper - '閉じる', Button, -7093414636191354273>,
 <uiawrapper.UIAWrapper - '', Pane, 5740354900026072187>,
 <uiawrapper.UIAWrapper - '', Pane, 5740354900026072187>,
 <uiawrapper.UIAWrapper - '', Pane, 5740354900026072187>,
 <uiawrapper.UIAWrapper - '', Pane, 5740354900026072187>,
 <uia_controls.TabControlWrapper - '', TabControl, 5740354900026072187>,
 <uiawrapper.UIAWrapper - '', Pane, 5740354900026072187>,
 <uiawrapper.UIAWrapper - '', Pane, 5740354900026072187>,
 <uiawrapper.UIAWrapper - 'Python - はてなブログ グループ', TabItem, 5740354900026072187>,
 <uia_controls.ButtonWrapper - '閉じる', Button, 5740354900026072187>,
 <uiawrapper.UIAWrapper - '年代 - はてなブログ グループ', TabItem, 5740354900026072187>,
 <uia_controls.ButtonWrapper - '閉じる', Button, 5740354900026072187>,
 <uiawrapper.UIAWrapper - '', Pane, 5740354900026072187>,
 <uia_controls.ButtonWrapper - '新しいタブ', Button, 5740354900026072187>,
 <uiawrapper.UIAWrapper - '', Pane, 5740354900026072187>,
 <uia_controls.MenuItemWrapper - 'タブを検索', MenuItem, 5740354900026072187>,
 <uia_controls.ToolbarWrapper - '', Toolbar, 5740354900026072187>,
 <uia_controls.ButtonWrapper - '戻る', Button, 5740354900026072187>,
 <uia_controls.ButtonWrapper - '進む', Button, 5740354900026072187>,
 <uia_controls.ButtonWrapper - '再読み込み', Button, 5740354900026072187>,
 <uiawrapper.UIAWrapper - '', Custom, 5740354900026072187>,
 <uia_controls.MenuItemWrapper - 'サイト情報を表示', MenuItem, 5740354900026072187>,
 <uia_controls.EditWrapper - 'アドレス検索バー', Edit, 5740354900026072187>,
 <uiawrapper.UIAWrapper - '', Pane, 5740354900026072187>,
 <uia_controls.ButtonWrapper - 'このページを共有', Button, 5740354900026072187>,
 <uia_controls.ButtonWrapper - 'このタブをブックマークに追加します', Button, 5740354900026072187>,
 <uiawrapper.UIAWrapper - '', Pane, 5740354900026072187>,
 <uia_controls.MenuItemWrapper - '拡張機能', MenuItem, 5740354900026072187>,
 <uia_controls.ButtonWrapper - 'サイドパネルを表示', Button, 5740354900026072187>,
 <uia_controls.ButtonWrapper - '7g', Button, 5740354900026072187>,
 <uia_controls.MenuItemWrapper - 'Chrome', MenuItem, 5740354900026072187>,
 <uiawrapper.UIAWrapper - '', Pane, 5740354900026072187>,
 <uiawrapper.UIAWrapper - '', Pane, 5740354900026072187>,
 <uiawrapper.UIAWrapper - '', Pane, 5740354900026072187>]

▼TabItem だけを出力することもできます

  • ここまでの調査で対象が「TabItem」ということが分かっているのでそれだけを出力することができます
    control_type 引数を指定します
pprint(dlg.descendants(control_type='TabItem'))

# 結果
[<uiawrapper.UIAWrapper - 'Python - はてなブログ グループ', TabItem, 5740354900026072187>,
 <uiawrapper.UIAWrapper - '年代 - はてなブログ グループ', TabItem, 5740354900026072187>]

▼タブの名前解決のサンプル

二つのタブをクリックしてアクティブなタブを切り替えるさんプリです。
タブの指定に「TabItem」「TabItem2」を使用しています。

from pywinauto impoort Desktop
app = Desktop(backend='uia')
app.window(title_re='.*Chrome').TabItem2.click_input()
app.window(title_re='.*Chrome').TabItem.click_input()

◇ダイアログなどは表示しないと情報が取れない

今回、ブラウザの制御として検索を使います。
検索は、ctrl+F のショートカットキーで使えます。
この時、検索ワードを入力するエリアが表示されます。
このエリアは、新しい要素であり、表示されている状態で上記の調査を行わないと情報を取得できません。

◆ブラウザを操作する

pywinauto ではブラウザで読み込んだサイト情報の中にあるボタンや入力フィールドを操作することが難しいようです。

したがって、今回、キーボード入力でできる操作でブラウザの制御をおこないます。

◇次のページへ遷移

本アプリでは、はてなブログのグループのサイトを対象に想定してブラウザの制御をおこないます。
対象のサイトにはページの一番下に「次のページ」ボタンがあります。
このボタンを押せば次のページへ遷移します。
pywinauto ではこのボタンを要素として認識しないようなので、ブラウザの検索機能で探して、カーソルをボタンに置き、Enter キーを押すことで代替えします。

◇キー操作

キー操作は基本的にブラウザから特定したダイアログ(タブ)で行います。

仕様では、キーの入力に send_keys() メソッドが用意されているのですが、私にはうまく使えませんでした。
その代わり、type_keys() メソッドを使用して実現しています。
type_keys() メソッドは内部で send_keys() メソッドを使用しているようです。
具体的には、◇キー操作で制御⤵を参照してください。

◇本アプリとブラウザのフォーカスの切替

ブラウザを制御しているとブラウザにフォーカスが移動してしまいます。
そのままでは本アプリでの連続した操作がやりにくくなります。

そこで、ブラウザに移動したフォーカスを本アプリに戻します。

本アプリにフォーカスするには、以下を実行します。

ウィジェット.focus_force()

ここで注意が必要です。
ブラウザに type_keys() メソッドでキー操作した後に本アプリにフォーカスを戻す場合、直ぐに戻すとキー操作が有効になる前にフォーカス移動が起きてキー操作が有効にならない問題が発生しました。
対策として、キー操作の動作を待つように以下のコードを実施します。

ダイアログ.wait("ready")

また、キー操作する前にブラウザにフォーカスを移動するには、以下のコードを実施します。
※実際には無くてもキー操作は機能します。

ダイアログ.set_focus()

ソースコードの説明

見出しに「モデル」や「ビュー」などとあるのは、MVC モデルを意識して付けています。

◇ブラウザに接続(モデル)

【処理】ブラウザに接続

  1. Application オブジェクトをアプリに接続
  2. ダイアログ(ウィンドウ)を取得
  3. 対象のタブをアクティブにします
  4. 例外の発生を考慮して結果を返します

【コード】ブラウザに接続

▼ブラウザに接続

    def connect_browser(self, app_name:str, tab_name:str, event=None) -> int:
        """
        ブラウザに接続
        Args:
            str:    アプリケーション名
            str:    タブ名
        Returns:
            int:    接続結果(0:OK, -1:TimeoutError, -2:NotUnique)
        """
        logger.info(f">>>Start connect, browser:{app_name}, tab:{tab_name}")

        self.app_name = app_name
        try:
            self.get_app_via_application_connect(app_name) # Applicationオブジェクトで接続
            # self.get_app_via_desktop()        # Desktopオブジェクトで接続
            self.get_dialog(app_name)           # ダイアログ(ウィンドウ)取得
            self.get_tab_ctl(tab_name)          # タブを指定
        except timings.TimeoutError:            # 接続中にタイムアウトが発生
            return -1
        except findwindows.ElementAmbiguousError as e:   # 一致する要素が複数あった
            return -2
        return 0

▼ダイアログの取得

    def get_dialog(self, app_name):
        """
        ダイアログを取得
        取得できなかった場合は例外が発生し、呼び出し元で処理
        ダイアログはself.browser_dlgに取得
        Args:
            str:    アプリケーション名
        """
        self.browser_dlg = self.app.window(title_re=f".*{app_name}")
        self.browser_dlg.wait("exists", timeout=10)
        logger.info(">>>Got target dialog")

        self.browser_dlg.set_focus()    # アプリを見えるようにしてその後の操作を見せる

        return

▼タブコントロールの取得

    def get_tab_ctl(self, tab_name):
        """
        タブコントロールを取得
        取得できなかった場合はタイムアウトの例外が発生し呼び出し元で処理
        タブコントロールはself.tab_ctlに取得
        Args:
            str:    タブ名
        """
        self.tab_ctl = self.browser_dlg.child_window(title_re=f".*{tab_name}.*", control_type="TabItem")
        self.tab_ctl.wait("exists", timeout=60)
        logger.info(">>>Got TabItem control")

        self.tab_ctl.click_input()      # タブをクリックして該当のタブを表示する
        self.tab_ctl.wait("visible", timeout=180)
        logger.info(">>>Got visible")

◇接続ボタンの動作(ビュー)

【処理】接続ボタン

  1. ブラウザに接続して該当のタブを表示
  2. 結果に基づいてボタンの背景色を設定
    成功:緑、タイムアウト:赤、対象が複数存在:黄色
  3. フォーカスを接続ボタンに戻す
  4. マウスカーソルを接続ボタンに移動

【コード】接続ボタン

    def connect_browser(self):
        """
        ブラウザに接続
        成功:背景色を緑に タイムアウト:背景色を赤に 対象が複数:背景色を黄色に
        マウスカーソルを接続ボタンに戻す
        """
        self.btn_connect.config(bg="white")
        # 画面で指定されたブラウザとタブ名でブラウザに接続
        connect_result = self.rpa.connect_browser(self.var_browser.get(), self.var_tab_name.get())
        # 接続結果に基づいてボタンの背景色を変える
        if connect_result == 0:
            self.btn_connect.config(bg="lightgreen")
        elif connect_result == -1:
            self.btn_connect.config(bg="red")
        else:
            self.btn_connect.config(bg="yellow")
        self.btn_connect.focus_force()    # ブラウザにあるフォーカスをアプリに戻す
        self.event_generate("<Motion>", warp=True
            , x=self.btn_connect.winfo_x()
            , y=self.btn_connect.winfo_y())  # マウスカーソルの移動

◇画面の作成(ビュー)

画面の作成については Tkinter の使い方になるので詳細は省きます。
どのような画面にしたか簡単に説明します。
詳細は、ソースコードを見てください。

【ポイント】

  • ブラウザを選択するコンボボックスを持つ
  • タブ名を入力するエントリーを持つ
  • ブラウザに接続する「接続」ボタンを持つ
  • ブラウザ操作用ボタンを持つ
    • ボタンは名称(値)とキー操作(キー)を辞書で持つ
    • 用意した操作:下、次のページ、上、トップ、ボトム、次のリンク、戻る、見る、検索
  • 検索文字を入力するエントリーを持つ
  • ボタンが押されたら pywinauto の処理を実行します
    マウスクリック、Enter キー、space キーに対応

【コード】

    def __init__(self, master) -> None:
        """
        コンストラクタ:画面作成
        """
        super().__init__(master)

        self.config(background="lightblue")     # ボタンの境目に色を出すために背景色を指定

        # ブラウザ選択コンボボックス
        lbl_browser = tk.Label(self, text="ブラウザ")
        lbl_browser.pack(fill=tk.X)
        lst_browser = ("Chrome", "Firefox")
        self.var_browser = tk.StringVar()
        cbb_browser = ttk.Combobox(self, textvariable=self.var_browser, values=lst_browser)
        cbb_browser.pack(fill=tk.X)
        cbb_browser.set(lst_browser[0])

        # タブ名指定エントリー
        lbl_tab_name = tk.Label(self, text="タブ名")
        lbl_tab_name.pack(fill=tk.X)
        self.var_tab_name = tk.StringVar(value="グループ")
        ent_tab_name = tk.Entry(self, textvariable=self.var_tab_name)
        ent_tab_name.pack(fill=tk.X)

        self.btn_connect = tk.Button(self, text="接続")
        self.btn_connect.pack(fill=tk.X, pady=(0,5))

        # 制御用ボタンの作成
        commands = {"down":"下", "next_page":"次のページ", "up":"上", "top":"トップ", "bottom":"ボトム", "jump":"次のリンク", "back":"戻る", "see":"見る", "search":"検索"}
        buttons = {}
        for key, value in commands.items():
            buttons[key] = tk.Button(self, text=value)
            buttons[key].bind("<1>", lambda event, key=key: self.rpa.key_type(event, key))
            buttons[key].bind("<space>", lambda event, key=key: self.rpa.key_type(event, key), add=True)
            buttons[key].bind("<Return>", lambda event, key=key: self.rpa.key_type(event, key), add=True)
            buttons[key].pack(fill=tk.X)
        
        # 検索ボタンとエントリーの作成
        buttons["search"].pack(pady=(5,0))
        self.var_word = tk.StringVar(value="")
        ent_word = tk.Entry(self, textvariable=self.var_word)
        ent_word.pack(fill=tk.X)

◇キー操作で制御

【処理】キー操作で制御

▼キー操作

  1. キーと操作に使うキーを持つ辞書を用意
  2. キーが next_page の場合(次のページへ遷移)、そのキーの値で検索して、see キーの処理(見る)を行う
  3. キーが search の場合(検索)、検索ワードを取得して検索
  4. それ以外のキーの場合、キーの値をキー出力
  5. キーの動作が終わるのを待ち、ボタンにマウスカーソルを移動
    ※参照:◇本アプリとブラウザのフォーカスの切替

▼検索

  1. Chromeの場合
    1. ctrl+F 検索ワード enter ESC をキー出力
      ※本来であれば、Firefox と同じようにダイアログを見つけて処理した方が確実ですが、ダイアログをうまく見つけられないのでこのようなやり方にしています。そのため、多少、動きが不安定です。
  2. Firefoxの場合
    1. ctrl+F をキー出力して検索ダイアログを表示
    2. タイトルが「ページ内検索」でコントロールタイプが「Edit」のコントロール(検索ダイアログ)を取得
    3. 取得したコントロールに検索ワードを設定
    4. 取得したコントロールESC を出力して閉じる

【コード】キー操作で制御

▼キー操作

    def key_type(self, event=None, key:str=None):
        """
        ブラウザにキー操作を施す
        Args:
            str:    キーストローク名(key_stroke辞書のキーのどれか)
        """
        self.browser_dlg.set_focus()
        self.browser_dlg.wait("ready")

        key_stroke = {"bottom":"{END}", "top":"{HOME}", "down":"{PGDN}", "up":"{PGUP}", "next_page":"次のページ"
                        , "search":"検索", "jump":"{TAB}", "back":"%{LEFT}", "see":"~"}
        logger.info(f">>>start typing keys:{key}")
        if key == "next_page":
            self.search_word(key_stroke.get(key))
            self.key_type(key="see")
        elif key == "search":
            self.search_word(self.view.var_word.get())
        else:
            self.browser_dlg.type_keys(key_stroke.get(key))
        
        self.browser_dlg.wait("ready")      # type_keysの動作が終わるのを待つ これをしないとfocus_force()が先に動いてしまう
        self.view.focus_force()             # 自分自身にフォーカスを戻す
        
        # ボタンが押されてこの処理に来た時は、マウスカーソルをボタンに戻す
        if event:
            event.widget.focus_set()
            self.view.event_generate("<Motion>", warp=True, x=event.widget.winfo_x()+10, y=event.widget.winfo_y()+10)

▼検索

    def search_word(self, word:str):
        """
        検索
        ブラウザにctrl+Fを出して、検索ワードを入力して検索する
        検索ダイアログは閉じる
        Args:
            str:    検索ワード
        """
        logger.info(f">>>\tsearch:{word}")
        if self.app_name == "Chrome":
            self.browser_dlg.type_keys("^f" f"{word}" "~" "{ESC}", pause=0.1) # ^:ctrl, ~:Enter chromeにはこれが合うみたい。でも十分ではない
            # Chromeで以下はうまくいかない。コントロールが見つからない。デバッグで止めると見つかるのはなぜ?
            # self.browser_dlg.type_keys("^f")      # 検索
            # search_ctrl = self.browser_dlg.Edit2
            # search_ctrl.wait("ready")
            # search_ctrl.set_edit_text(f"{word}")  # 検索ワード
            # search_ctrl.type_keys("{ESC}")        # 検索ダイアログを閉じる
        else:
            # self.browser_dlg.type_keys("'") # リンク検索 こちらは数秒で消える
            self.browser_dlg.type_keys("^f") # 検索
            search_ctrl = self.browser_dlg.child_window(title="ページ内検索", control_type="Edit")
            search_ctrl.wait("ready")
            search_ctrl.set_edit_text(f"{word}")    # 検索ワード
            self.browser_dlg.type_keys("{ESC}")     # 検索ダイアログを閉じる

◆使用したメソッドの説明

「識別子」を出力

  • 【構文】print_control_identifiers(depth=None, filename=None)
  • 引数
    • depth=None:深さ
    • filename=None:出力ファイル名

dump_tree(depth=None, filename=None) も同じ

descendants()

この要素の子孫をリストとして返します。

  • 【構文】descendants(**kwargs)
  • 主な引数
    • control_type:出力する要素のコントロールの型を指定します
  • 戻り値
    BaseWrapper (またはサブクラス) インスタンスのリスト

child_window()

コントロールの名前解決の条件を追加します。複数の条件を指定する場合に使用します。

  • 【構文】child_window(**kwargs)
  • 主な引数 1
    • title:タイトル名
    • title_re:タイトル名を正規表現で指定
    • control_type:出力する要素のコントロールの型を指定します

type_keys()

要素にキーを入力します。

  • 【構文】type_keys(keys, pause=None, with_spaces=False, with_tabs=False, with_newlines=False, turn_off_numlock=True, set_foreground=True, vk_packet=True)
  • 主な引数
    • keys:文字列を列挙(空白区切りで複数の文字列を指定可能)
    • pause:待ち時間(秒)(デフォルト:0.05

▶特別な文字の割り当て
shift, ctrl, alt, Enter には特別な文字が割あたっています

  • shift+
  • ctrl^
  • alt%
  • Enter~

有効なキーはこちらで確認できます:pywinauto.keyboard — pywinauto 0.6.8 documentation

click_input()

要素をクリックします。
要素が表示されている必要があります。

  • 【構文】click_input(button='left', coords=(None, None), button_down=True, button_up=True, double=False, wheel_dist=0, use_log=True, pressed='', absolute=False, key_down=True, key_up=True)
  • 主な引数
    • buttom:マウスのボタンを指定(left, right, middle など)
    • double:ダブルクリックするかどうか

set_focus()

要素にフォースを設定します。

  • 【構文】set_focus()

texts()

コントロールのテキストを返します。

  • 【構文】texts()

※コントロールが正しく取れたかどうかの確認などで便利です。

◆例外

いくつかの例外について説明します。

◇WindowAmbiguousError

【意味】:一致するウィンドウが複数存在

同じアプリを複数起動していた場合、ブラウザの場合は複数ウィンドウを開いている場合も該当、アプリ名だけで接続しようとするとこのエラーが発生します。
ウィンドウを区別できるような条件で接続する必要があります。

エラーの例:
There are 2 elements that match the criteria {'title_re': '.*Firefox.*', 'backend': 'uia', 'visible_only': False}
訳:条件 {'title_re': '.*Firefox.*', 'backend': 'uia', 'visible_only': False} に一致する 2 つの要素があります。

ウィンドウを見つけるのに条件に合うウィンドウが複数あるというエラーです。
 この例外が出るメソッド:connect, window

【意味】:cp932に変換できないコードが含まれている

print_control_identifiers()メソッドを使用していて次のエラーが出ることがあります。

エラーの例:
UnicodeEncodeError: 'cp932' codec can't encode character '\u2014' in position 58: illegal multibyte sequence 訳:UnicodeEncodeError: 'cp932' コーデックは位置 58 の文字 '\u2014' をエンコードできません: 不正なマルチバイト シーケンス

これは出力するデータに UTF-8 にしか存在しない文字が含まれていると発生します。
私が試したサイトでは、ブラウザが Chrome の場合は発生しませんが、Firefox の場合は発生しました。

【回避策】
次のコードを追加すると回避できます。2

import locale
def getpreferredencoding(do_setlocale = True):
    return "utf-8"
locale.getpreferredencoding = getpreferredencoding

◆ソースの取得

全体のソースはこちらから取得できます。

※ソースにはデバッグ用のコードが含まれていますのでご容赦ください。
デバッグには logging モジュールを使用しています。
 logging モジュールの使い方については、別の記事で解説する予定です。

◆アプリの取得

アプリは、Chrome または Firefox で表示されているサイトのブラウジングを制御するものです。
はてなブログのグループサイト用に「次のページ」ボタンを押すためのボタンを用意しました。
ページ内のスクロールや検索ができます。

利用するには、アプリを含んだ zip ファイルを下記からダウンロードして取得します。
ダウンロードした zip ファイルを解凍すると次のファイルができます。
任意のフォルダにファイルを保存してください。

  • プログラム: RPA_browser.exe

◇アプリの使い方

  • インストール

    • ダウンロードした zip ファイルを任意のフォルダで解凍します
  • 実行

    • RPA_browser.exe を実行します
  • 操作

    • ブラウザの接続
      • コンボボックスから制御するブラウザを選択
      • 制御するタブの名称の一部を入力フィールドに指定
        他のタブと区別がつく名称を入れます
      • 接続ボタンを押す 接続ボタンの背景が緑になれば操作開始です
    • ブラウザの制御
      • ボタンを押すとボタンに表示されている動作を行います
      • 検索ボタンを押す場合、ボタンの下の入力フィールドに検索ワードを入力します
  • 画面の説明

    • ボタン
      • 下:下に1ページ分スクロール
      • 次のページ:ページの下の「次のページ」ボタンを押して次のページを表示
      • 上:上に1ページ分スクロール
      • トップ:ページの一番上にスクロール
      • ボトム:ページの一番下にスクロール
      • 次のリンク:次のリンク位置にカーソルを移動(Tab キーの動き)
      • 戻る:表示を戻す
      • 見る:カーソルのあるリンク先を表示
      • 検索:入力フィールドの文字を検索
  • アンインストール

    • 解凍したファイルをすべて削除します

◆さいごに

作る前はもう少し簡単にできると思っていました。
自分のイメージとライブラリの実際の動きが違っていて、なかなか難しかったです。

記事では、Chrome での動作で書いていますが、当初は Firefox で動作させていました。
複数タブを表示している時のタブの指定方法が分かるまでに苦労しました。
ブラウザのタブの状態が pywinauto でどう見えているのかがよく分からなかったからです。
また、初めは表示されているサイトのボタンなどを制御しようと試みたのですが、こちらは pywinauto ではできないようでした。
できるかもしれませんが、簡単ではないと判断しました。

このような経緯があり、ブラウザの制御はキー操作でできることにしました。

そのため、作成したアプリはブラウザを直接操作した方が良いくらいのアプリになりました。

リモートで操作するような環境なら用途もあるかもしれません。

私としては、この後、音声認識と結合できないかなと思っています。

はてなグループの記事を音声でブラウジングできればいいなと思っています。

この記事を書くにあたって、動けばいいと思って作ったアプリをいろいろと修正しました。
人に説明しようとすると新たに気付くことがいろいろ出てきます。
パッケージの使い方一つにしてもです。

「プログラムでお返しできたら」なんて思って記事を書いていますが、自分の理解を深めるということで、自分に返ってきているとつくづく感じました。

本アプリは、Firefox での動きは十分使用できる範囲だと思いますが、Chrome での動作は不安定です。
対策が見つかればお知らせしますが、もし、見つけられた方がいらっしゃれば、ご一報いただけると幸いです。

あわせて読みたい 📖 音声入力で文章作成するアプリの作り方【Python】 🔗
📖 音声認識でブラウザを操作【Python】 🔗

◇ご注意

本記事は次のバージョンの下で動作した内容を基に記述しています。

◇免責事項

ご利用に際しては、『免責事項』をご確認ください。
お気づきの点がございましたら『お問い合わせ』からお問い合わせください。

◆参考

投稿: 、更新: