🐥note.

小鳥とMicrosoft <3 なエンジニアの技術Blog📚

MicroBatchFrameworkとSeleniumでCLIツールを作った話(その2)

前回の記事の続きで、Selenium に関する話です。

SeleniumWebサービスを自動操作する部分を実装します。

Webサービスは以下の流れで操作します。

  1. WebサービスのIndexページにアクセスする
  2. リンクをクリックし、画面遷移する
  3. アップロードするExcelファイルを選択する
  4. Excelファイルをアップロードする
  5. Excelファイルのアップロードの完了を待つ
  6. アップロード完了後、登録ボタンを押下する
  7. 登録ボタン押下後、バックグラウンドの処理の完了を待つ

前回の記事をご参照頂くと分かると思いますが、このツールはChromeDriverを含める形でExeを作成しています。ChromeDriverがアップデートされた際、新しいChromeDriverをExeに含めるためにツールをビルド・発行したくありません。

よってExeと同じ階層にChromeDriverが存在する場合は、そちらのChromeDriverを使用するという仕様でツールを実装します。
こうすることでツールのビルド・発行の手間を省きます。
※不要なChromeDriverがExeに含まれるのは怒られそうですが…。

ソースコード

コードは以下の通りです。
コメントに操作と対応する番号を記載しています。 ※URIとFilePathを関数の引数で受け取るのは微妙ですが、今回は目を瞑ります。。
※ヘッドレスモードにするかどうかはCommandLineの引数で受け取った方が良いですね。

ChromeBrowserAutoOperator.cs

using System;
using HogeBot.Interfaces;
using Microsoft.Extensions.Logging;

namespace HogeBot.Services
{
    public class ChromeBrowserAutoOperator : IAutoOperationable
    {
        private readonly ILogger<ChromeBrowserAutoOperator> _logger;

        public ChromeBrowserAutoOperator(ILogger<ChromeBrowserAutoOperator> logger)
        {
            this._logger = logger;
        }

        public void AutoOperate(string uri, string filePath)
        {
            try
            {
                // ヘッドレスモード(ブラウザ非表示状態で起動する。)
                var options = new ChromeOptions();
                options.AddArgument("--headless");

                // Exeと同じ階層にChromeDriverが存在する場合は、そちらのChromeDriverを使用する
                var exeDir = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
                var dir = File.Exists(Path.Combine(exeDir, "chromeDriver.exe")) ? exeDir : Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
                using (var driver = new ChromeDriver(dir, options))
                {
                    // 1. WebサービスのIndexページにアクセスする
                    driver.Url = param.URI;

                    // 2. リンクをクリックし、画面遷移する
                    driver.FindElementById("link-id1").Click();

                    // 3. アップロードするExcelファイルを選択する
                    // ※Excelファイルのパスを<input type="file">にSendKeysで入力すると
                    //   ファイル選択ダイアログでファイルを選択したのと同じ状態となります。
                    var fileInfo = new FileInfo(param.ExcelFilePath);
                    driver.FindElementById("input-excel-file").SendKeys(fileInfo.FullName);

                    // 4. Excelファイルをアップロードする
                    driver.FindElementById("button-import-start").Click();

                    // 5. Excelファイルのアップロードの完了を待つ
                    // ※1分間Uploadの完了を待つ(Timeoutすると例外が吐かれる)
                    //  Uploadの完了は指定したLabel内にメッセージが表示されることで判断する。
                    WebDriverWait wait = new WebDriverWait(driver, new TimeSpan(0, 1, 0));
                    var msgLabel = By.Id("label-upload-msg");
                    wait.Until(f => f.FindElement(msgLabel).Text == "アップロード完了しました。登録ボタンを押してください。");

                    // 6. アップロード完了後、登録ボタンを押下する
                    driver.FindElementById("button-registers").Click();

                    // 7. 登録ボタン押下後、バックグラウンドの処理の完了を待つ
                    wait.Until(f =>
                        f.FindElement(msgLabel).Text == "登録完了" || 
                        f.FindElement(msgLabel).Text == "登録失敗");
                }
                return true;
            }
            catch (Exception e)
            {
                logger.LogError(e, "RunAutomation Exception.");
                return false;
            }
        }
    }
}

備考 - アップロードするファイルの指定方法について

通常は人間がファイルを選択ボタンを押下して、ファイルを選択して…と操作します。

f:id:piyo_esq:20191010211801p:plain
自動操作前の状態

Seleniumでは<input type="file">要素にSendKeysUploadするファイルのパスを送信することで上記操作を実現できます。

f:id:piyo_esq:20191010211844p:plain
自動操作後の状態

上記コード中の3番のあたりですね。

備考 - 指定した画面要素が表示されるまで待機する方法について

Seleniumによる自動操作中FindElement関数で対象の画面要素が見つからない場合、例外が吐かれます。なんらかのイベント発火後に画面要素がAjaxで更新されるWebサービスの場合、画面の更新(描画)が完了するまで待機する必要があります。

指定したが画面要素が更新(描画)されるまで待機する必要がある場合
Selenium.Support Packageに含まれるWebDriverWait Classを使用することで 特定の画面要素が描画されるまで、指定した時間待機という動きが実現できます。

上記コード中の5番のあたりですね。

備考 - FindElement系関数で特定できない画面要素について

Id, Class, XPathなどによる操作が困難な場合は

画面のスクリーンショット撮影 -> テンプレートマッチング -> Blobから座標特定 -> 座標をクリックみたいな流れで自動操作する手もあります。

こちらについては後日記載したいと思います。

Cygamesさんが昔やってた自動化みたいな感じですね。

おわり

いい感じに社内Webサービスの自動操作ができるようになりました。

MicroBatchFramework + Seleniumの組み合わせ、非常に良いですね。