PowerShellとSeleniumで簡単なブラウザRPAを作る

Tips

はじめに

ブラウザで同じ操作を何度も行う場面では、簡単な RPA ツールを作れるとかなり楽になります。 本格的な RPA 製品を導入するほどではないものの、検索、一覧画面の確認、社内ツールへの入力補助など、少しだけ自動化したい場面はあります。

そのようなときに、PowerShell で小さな GUI を用意し、ボタンを押したら Selenium でブラウザを操作できると便利です。 PowerShell で Windows Forms の画面を作る基本は、以前の記事 PowerShellで簡単にGUIを作成する で紹介しています。

本記事では、その続きとして PowerShell + Selenium でブラウザを自動操作するサンプルを作ります。 サンプルは、入力欄にキーワードを入れてボタンを押すと Chrome を起動し、Google 検索を実行する小さなツールです。

なぜPowerShellでブラウザRPAを作るのか

ブラウザ自動化だけであれば、Python や Node.js で Selenium を使う方法もあります。 一方で、Windows 上で手元の作業を少しだけ自動化したい場合、PowerShell はかなり相性がよいです。

PowerShell であれば、Windows 標準の操作、ファイル処理、CSV の読み書き、既存の社内スクリプトとの連携をそのまま扱えます。 さらに Windows Forms を使うと、検索キーワードや実行対象を入力するための簡単な画面も用意できます。

今回の構成では、以下の役割分担にします。

役割 使うもの 内容
入力画面 Windows Forms 検索キーワードと実行ボタンを用意する
ブラウザ操作 Selenium WebDriver Chrome を起動して検索フォームを操作する
ドライバー管理 Selenium Manager ChromeDriver の取得とキャッシュを Selenium に任せる

大きな自動化基盤を作るというより、「手作業を少し減らすための道具」をすぐ作れるところがメリットです。

Selenium Managerでドライバー管理を任せる

Selenium で Chrome を操作するには、以前は Chrome のバージョンに合った chromedriver.exe を自分でダウンロードし、PATH に置く必要がありました。 Chrome は自動更新されるため、ブラウザだけ新しくなり、手元の ChromeDriver が古いままになると起動に失敗します。

Selenium 4.6 以降では、Selenium Manager が Selenium に同梱されています。 Selenium Manager は、必要なドライバーが見つからない場合に、ブラウザのバージョンを検出し、対応するドライバーを取得してローカルにキャッシュします。

そのため、本記事のサンプルでは chromedriver.exe のパスを直接指定しません。 以下のように ChromeDriver を作成するだけにして、ドライバーの解決は Selenium 側に任せます。

$options = [OpenQA.Selenium.Chrome.ChromeOptions]::new()
$driver = [OpenQA.Selenium.Chrome.ChromeDriver]::new($options)

この書き方にしておくと、ドライバーを手動で更新する作業を減らせます。 初回実行時は Selenium Manager がドライバーを取得するため、インターネット接続が必要です。 プロキシ環境やオフライン環境では、事前にキャッシュを用意するか、Selenium Manager の設定を調整する必要があります。

使用環境

今回のサンプルは、以下の環境を想定しています。

項目 内容
OS Windows 11
PowerShell Windows PowerShell 5.1 / PowerShell 7 系
ブラウザ Google Chrome
自動化ライブラリ Selenium WebDriver 4.6 以降
UI Windows Forms

Windows に最初から入っている Windows PowerShell 5.1 でも動くようにしています。 ただし、古い Windows 環境では .NET Framework の更新が必要になる場合があります。 初回実行時は Selenium パッケージや ChromeDriver を取得するため、インターネット接続が必要です。

Selenium WebDriverを自動取得して読み込む

今回のサンプルは、スクリプト単体で動くことを優先します。 そのため、GoogleSearchRpa.ps1 の中で nuget.exe を取得し、Selenium.WebDriver パッケージも自動で取得します。

初回実行時は少し時間がかかります。 2 回目以降は toolspackages フォルダに保存されたファイルを使うため、同じ処理を毎回手で準備する必要はありません。

コードの中では、Windows PowerShell 5.1 と PowerShell 7 のどちらでも読み込める DLL を選ぶようにしています。 使う際は GoogleSearchRpa.ps1 を実行するだけです。

Google検索するGUIツールを作る

ここから、実際に GUI 付きのサンプルを紹介します。 ファイル名は GoogleSearchRpa.ps1 などにして保存します。

処理の流れは以下のとおりです。

  1. Windows Forms の画面を表示する
  2. テキストボックスに検索キーワードを入力する
  3. 実行ボタンを押す
  4. Selenium で Chrome を起動する
  5. Google の検索ボックスに入力して Enter を送る

全体のコードは以下です。

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

function Import-SeleniumWebDriver {
    $packageRoot = Join-Path $PSScriptRoot "packages"
    $toolRoot = Join-Path $PSScriptRoot "tools"

    foreach ($path in @($packageRoot, $toolRoot)) {
        if (-not (Test-Path $path)) {
            New-Item -ItemType Directory -Path $path | Out-Null
        }
    }

    [Net.ServicePointManager]::SecurityProtocol =
        [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12

    $nugetExe = Join-Path $toolRoot "nuget.exe"

    if (-not (Test-Path $nugetExe)) {
        $downloadParams = @{
            Uri = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
            OutFile = $nugetExe
        }

        if ($PSVersionTable.PSVersion.Major -lt 6) {
            $downloadParams.UseBasicParsing = $true
        }

        Invoke-WebRequest @downloadParams
    }

    function Install-SeleniumPackage {
        & $nugetExe install Selenium.WebDriver `
            -OutputDirectory $packageRoot `
            -DependencyVersion Highest `
            -DirectDownload `
            -NonInteractive | Out-Null

        if ($LASTEXITCODE -ne 0) {
            throw "nuget.exe による Selenium.WebDriver の取得に失敗しました。"
        }
    }

    function Get-LatestSeleniumPackage {
        Get-ChildItem -Path $packageRoot -Directory -Filter "Selenium.WebDriver.*" -ErrorAction SilentlyContinue |
            Where-Object { $_.Name -match "^Selenium\.WebDriver\.\d+(\.\d+){1,3}$" } |
            Sort-Object @{ Expression = { [version]($_.Name -replace "^Selenium\.WebDriver\.", "") } } -Descending |
            Select-Object -First 1
    }

    Install-SeleniumPackage

    $seleniumPackage = Get-LatestSeleniumPackage

    if (-not $seleniumPackage) {
        throw "Selenium.WebDriver パッケージが見つかりませんでした。"
    }

    $seleniumPackageDir = $seleniumPackage.FullName

    if ($PSVersionTable.PSEdition -eq "Desktop" -or $PSVersionTable.PSVersion.Major -lt 6) {
        $frameworkPreference = @("net48", "net472", "net471", "net47", "net462", "net461", "netstandard2.0")
    }
    else {
        $runtimeMajor = [System.Environment]::Version.Major
        $frameworkPreference = @()

        for ($major = $runtimeMajor; $major -ge 6; $major--) {
            $frameworkPreference += "net$major.0"
        }

        $frameworkPreference += @("netstandard2.1", "netstandard2.0", "net472", "net462")
    }

    $packageDirs = Get-ChildItem -Path $packageRoot -Directory |
        Where-Object { $_.Name -match "^(Selenium\.WebDriver|System\.|Microsoft\.)" }

    $assemblyFiles = foreach ($packageDir in $packageDirs) {
        $libRoot = Join-Path $packageDir.FullName "lib"

        if (-not (Test-Path $libRoot)) {
            continue
        }

        foreach ($framework in $frameworkPreference) {
            $frameworkDir = Join-Path $libRoot $framework

            if (Test-Path $frameworkDir) {
                Get-ChildItem -Path $frameworkDir -Filter "*.dll"
                break
            }
        }
    }

    $seleniumDll = $assemblyFiles |
        Where-Object { $_.Name -eq "Selenium.WebDriver.dll" } |
        Select-Object -First 1

    if (-not $seleniumDll) {
        Get-ChildItem -Path $packageRoot -Directory -Filter "Selenium.WebDriver.*" -ErrorAction SilentlyContinue |
            Remove-Item -Recurse -Force

        Install-SeleniumPackage

        $seleniumPackage = Get-LatestSeleniumPackage

        if (-not $seleniumPackage) {
            throw "Selenium.WebDriver パッケージが見つかりませんでした。"
        }

        $seleniumPackageDir = $seleniumPackage.FullName

        $packageDirs = Get-ChildItem -Path $packageRoot -Directory |
            Where-Object { $_.Name -match "^(Selenium\.WebDriver|System\.|Microsoft\.)" }

        $assemblyFiles = foreach ($packageDir in $packageDirs) {
            $libRoot = Join-Path $packageDir.FullName "lib"

            if (-not (Test-Path $libRoot)) {
                continue
            }

            foreach ($framework in $frameworkPreference) {
                $frameworkDir = Join-Path $libRoot $framework

                if (Test-Path $frameworkDir) {
                    Get-ChildItem -Path $frameworkDir -Filter "*.dll"
                    break
                }
            }
        }

        $seleniumDll = $assemblyFiles |
            Where-Object { $_.Name -eq "Selenium.WebDriver.dll" } |
            Select-Object -First 1

        if (-not $seleniumDll) {
            throw "Selenium.WebDriver.dll が見つかりませんでした。"
        }
    }

    $seleniumManager = Get-ChildItem -Path $seleniumPackageDir -Recurse -Filter "selenium-manager.exe" -ErrorAction SilentlyContinue |
        Select-Object -First 1

    if (-not $seleniumManager) {
        throw "selenium-manager.exe が見つかりませんでした。"
    }

    $seleniumManagerTarget = Join-Path $seleniumDll.DirectoryName "selenium-manager.exe"
    $seleniumManagerSource = [System.IO.Path]::GetFullPath($seleniumManager.FullName)
    $seleniumManagerTargetFullPath = [System.IO.Path]::GetFullPath($seleniumManagerTarget)

    if ($seleniumManagerSource -ne $seleniumManagerTargetFullPath) {
        Copy-Item -Path $seleniumManager.FullName -Destination $seleniumManagerTarget -Force
    }

    if (-not (Test-Path $seleniumManagerTarget)) {
        throw "selenium-manager.exe を $seleniumManagerTarget にコピーできませんでした。"
    }

    $env:SE_MANAGER_PATH = $seleniumManagerTarget
    [System.Environment]::SetEnvironmentVariable("SE_MANAGER_PATH", $seleniumManagerTarget, "Process")

    $loadedAssemblies = @{}
    [System.AppDomain]::CurrentDomain.GetAssemblies() | ForEach-Object {
        $loadedAssemblies[$_.GetName().Name] = $true
    }

    $assemblyFiles |
        Where-Object { $_.Name -ne "Selenium.WebDriver.dll" } |
        ForEach-Object {
            $assemblyName = [System.IO.Path]::GetFileNameWithoutExtension($_.Name)

            if (-not $loadedAssemblies.ContainsKey($assemblyName)) {
                Add-Type -Path $_.FullName
                $loadedAssemblies[$assemblyName] = $true
            }
        }

    Add-Type -Path $seleniumDll.FullName
}

$form = [System.Windows.Forms.Form]::new()
$form.Text = "Google検索RPA"
$form.Size = [System.Drawing.Size]::new(520, 180)
$form.StartPosition = "CenterScreen"

$label = [System.Windows.Forms.Label]::new()
$label.Text = "検索キーワード"
$label.Location = [System.Drawing.Point]::new(20, 22)
$label.AutoSize = $true
$form.Controls.Add($label)

$textBox = [System.Windows.Forms.TextBox]::new()
$textBox.Location = [System.Drawing.Point]::new(120, 18)
$textBox.Size = [System.Drawing.Size]::new(360, 28)
$textBox.Text = "PowerShell Selenium RPA"
$form.Controls.Add($textBox)

$button = [System.Windows.Forms.Button]::new()
$button.Text = "検索"
$button.Location = [System.Drawing.Point]::new(120, 60)
$button.Size = [System.Drawing.Size]::new(100, 32)
$form.Controls.Add($button)

$statusLabel = [System.Windows.Forms.Label]::new()
$statusLabel.Text = "待機中"
$statusLabel.Location = [System.Drawing.Point]::new(20, 108)
$statusLabel.Size = [System.Drawing.Size]::new(460, 24)
$form.Controls.Add($statusLabel)

$form.AcceptButton = $button

$button.Add_Click({
    $query = $textBox.Text.Trim()

    if ([string]::IsNullOrWhiteSpace($query)) {
        [System.Windows.Forms.MessageBox]::Show("検索キーワードを入力してください。")
        return
    }

    $button.Enabled = $false
    $statusLabel.Text = "Selenium を準備しています..."
    [System.Windows.Forms.Application]::DoEvents()

    try {
        Import-SeleniumWebDriver

        $statusLabel.Text = "Chrome を起動しています..."
        [System.Windows.Forms.Application]::DoEvents()

        $options = [OpenQA.Selenium.Chrome.ChromeOptions]::new()
        $options.AddArgument("--start-maximized")

        $driver = [OpenQA.Selenium.Chrome.ChromeDriver]::new($options)

        $statusLabel.Text = "Google を開いています..."
        [System.Windows.Forms.Application]::DoEvents()

        $driver.Navigate().GoToUrl("https://www.google.com/")
        Start-Sleep -Milliseconds 800

        $searchBox = $driver.FindElement([OpenQA.Selenium.By]::Name("q"))
        $searchBox.SendKeys($query)
        $searchBox.SendKeys([OpenQA.Selenium.Keys]::Enter)

        $statusLabel.Text = "検索を実行しました。"
    }
    catch {
        $message = $_.Exception.Message
        $statusLabel.Text = "エラーが発生しました。"
        [System.Windows.Forms.MessageBox]::Show($message, "実行エラー")
    }
    finally {
        $button.Enabled = $true
    }
})

[void]$form.ShowDialog()

このサンプルでは、Chrome を閉じる処理をあえて入れていません。 検索結果を人間がそのまま確認する用途を想定しているためです。 完全に無人で処理する場合は、処理の最後に $driver.Quit() を呼び出してブラウザを終了します。

$driver.Quit()

ただし、finally で必ず閉じるようにすると、エラー時にブラウザ上の状態を確認しにくくなります。 最初の検証中はブラウザを残し、動作が安定してから自動終了を入れるほうが調査しやすいです。

実行方法

スクリプトを保存したフォルダで PowerShell を開き、以下のように実行します。 Windows に最初から入っている「Windows PowerShell」でも実行できます。

Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\GoogleSearchRpa.ps1

Set-ExecutionPolicy -Scope Process は、現在の PowerShell セッションだけ実行ポリシーを緩める指定です。 永続的に設定を変えないため、動作確認用として扱いやすいです。

毎回 PowerShell を開いてコマンドを入力するのが面倒な場合は、同じフォルダに run.bat を置くと楽です。 ダブルクリックで起動できるため、自分以外の人に渡す場合も扱いやすくなります。

@echo off
cd /d "%~dp0"
powershell.exe -NoProfile -ExecutionPolicy Bypass -Sta -File "%~dp0GoogleSearchRpa.ps1"
pause

cd /d "%~dp0" は、run.bat が置かれているフォルダへ移動する指定です。 これにより、packagestools フォルダがスクリプトと同じ場所に作成されます。 -ExecutionPolicy Bypass はこの実行だけポリシーを緩める指定で、-Sta は Windows Forms を安定して動かすために付けています。

GoogleSearchRpa.ps1 の実行時は、以下の処理が行われます。

  • tools フォルダに nuget.exe を取得する
  • nuget.exeSelenium.WebDriver パッケージを取得する
  • packages フォルダ内の DLL を読み込む
  • Selenium Manager が ChromeDriver を解決する

初回は NuGet パッケージや ChromeDriver を取得するため時間がかかります。 2 回目以降は、保存済みのパッケージやドライバーのキャッシュが使われるため起動が速くなります。 キャッシュが壊れた場合や、ブラウザ更新後に挙動がおかしい場合は、Selenium のキャッシュを削除して再取得させるのも切り分けになります。

うまく動かないときの確認ポイント

PowerShell + Selenium の組み合わせでは、エラーの原因が PowerShell 側、Selenium 側、ブラウザ側、ネットワーク側のどこにあるか分かりにくいことがあります。 まずは以下を確認すると切り分けしやすいです。

症状 確認すること
nuget.exe の取得や install が失敗する NuGet へ接続できるか、プロキシ設定が必要かを確認する
Selenium.WebDriver.dll が見つからない packages フォルダを削除してスクリプトを再実行する
selenium-manager.exe が見つからない packages フォルダを削除してスクリプトを再実行する
Add-Type が失敗する Windows PowerShell 5.1 なら .NET Framework 4.6.2 以降かを確認する
Chrome が起動しない Chrome がインストールされているか、Selenium WebDriver が 4.6 以降かを確認する
ドライバー取得で止まる 初回実行時にインターネット接続できるか確認する
検索ボックスが見つからない Google 側の表示や同意画面が出ていないか確認する

特に社内ネットワークでは、プロキシや SSL インスペクションの影響で Selenium Manager がドライバーを取得できないことがあります。 その場合は、プロキシ設定を通すか、事前にドライバーをキャッシュしておく運用を考える必要があります。

また、Google の画面は地域やアカウント状態によって表示が変わることがあります。 同意画面が出る環境では、検索ボックスを探す前に同意ボタンの処理を入れるか、検索 URL を直接開く方法に切り替えると安定します。

$encodedQuery = [System.Uri]::EscapeDataString($query)
$driver.Navigate().GoToUrl("https://www.google.com/search?q=$encodedQuery")

この方法は検索フォームへの入力操作ではありませんが、検索結果を開くという目的だけならシンプルです。 RPA の練習としては検索ボックスを操作し、実用寄りにするなら URL を直接組み立てる、という使い分けがよさそうです。

RPAとして使うときの注意点

Selenium は便利ですが、人間の操作をそのまま無制限に置き換える道具として使うと危険です。 対象サイトの利用規約、アクセス頻度、ログインや多要素認証の扱いには注意が必要です。

とくに以下のような処理は、安易に自動化しないほうがよいです。

  • 多要素認証を無理に自動化する処理
  • 利用規約で禁止されているスクレイピング
  • 短時間に大量アクセスする処理
  • 個人情報や認証情報を平文で保存する処理

PowerShell で作る小さな RPA は、まず自分の管理下にある作業などの入力補助から始めるのが安全です。 パスワードが必要な処理を扱う場合は、スクリプトに直接書かず、資格情報管理や環境変数などを使う設計にします。

応用しやすい処理

今回のサンプルは Google 検索だけですが、同じ形でいろいろな用途に応用できます。

用途
社内ポータル検索 社員番号や案件番号を入力して検索する
管理画面の確認 URL と検索条件を入力して一覧を開く
定型フォーム入力 CSV の内容をフォームに転記する
レポート取得 日付を選択してダウンロードボタンを押す
動作確認 Web アプリの基本導線を手元で確認する

最初から全部を自動化しようとすると、例外処理が一気に複雑になります。 まずは「画面を開く」「検索する」「結果画面まで進む」のように、人間が面倒に感じる 1 区間だけを置き換えるのが扱いやすいです。

PowerShell の GUI を付けておくと、スクリプトに慣れていない人にも渡しやすくなります。 検索語、対象日付、ファイルパスなどを画面から入力できるため、引数の指定ミスも減らせます。

まとめ

PowerShell の Windows Forms と Selenium を組み合わせることで、簡単な GUI 付きブラウザ RPA を作れます。 今回のサンプルでは、検索キーワードを入力してボタンを押すだけで Chrome を起動し、Google 検索を実行するところまで作りました。

ポイントは、chromedriver.exe を手動で管理せず、Selenium 4.6 以降の Selenium Manager に任せることです。 これにより、Chrome の自動更新に合わせてドライバーを入れ替える手間を減らせます。

実際に使う場合は、対象サイトの利用規約、ログイン情報の扱い、アクセス頻度に注意が必要です。 小さく始めるなら、まずは自身の管理下にある作業や、手元の確認作業を 1 つだけ自動化するのがよいと思います。

コメント

タイトルとURLをコピーしました