見出し画像

[SeleniumVBA]業務システムでよく利用する要素検索


インストールが不要でありながら、WebDriver自動更新機能が標準でついていたりと、機能豊富なSeleniumVBAを利用して、Excelに入力したデータを業務システムに自動登録できるよう開発しています。

業務で使っていくうちに、高頻度で使用する要素検索が分かってきましたので、備忘録も兼ねて以下にExcelVBAコードを記載しています。

コード事例では、「driver.」の記述を何度も繰り返し使用するため、Withによる「.」表記で記述を省略しています。

また、By.Xpathをフル活用しています。Xpathについては以下の記事に詳しい説明がありますので御参照いただければ、と思います。

1 セレクトボックスの選択

業務システムを操作するとき、セレクトボックスの選択を必要とするケースはかなりありました。以下のコードでセレクトボックスの選択に利用しています。

'HTMLの例:
'<select name="〇〇">
'  <option>"××"</option>
'</select>

Dim elmfound as WebElement
With driver
  If .IsPresent(By.Xpath, "//select[@name='〇〇']/option[text()='××']",2000,,elmfound) = False Then Stop
  elmfound.Click
End With

業務においては、要素が見つかるまで待機処理を必ず行う必要がありますが、「driver.wait 1000」といったような固定秒数による待機は、やむを得ない場合を除き推奨されません。

ここでは、Ispresentの機能により、待機処理(最大2000ミリ秒=2秒)を行いながら、見つかったらelmfoundという要素を返し、見つからなかったとStopするという、複数の処理を一度に記述できることからIspresentをよく利用しています

関数 IsPresent の構造

またBy.Xpathを利用している理由は、Xpathであればselectタグのname属性の指定(〇〇)とoptionタグのテキストの値指定(××)が1行で実行できるところが良く、他の方法(By.name等)では難しかったことからです。

実際には、値を代入する箇所(〇〇や××)は変数を利用しています。〇〇を変数「str1」、××を変数「str2」とすると、以下のとおり「&」で繋げることになります。

'【変数利用の場合 str1とstr2が変数】
Dim elmfound as WebElement
With driver
  If .IsPresent(By.Xpath, "//select[@name='" & str1 & "']/option[text()='" & str2 & "']",2000,,elmfound) = False Then Stop
End With

要素が見つからなかったらStopで止めている理由は、エラーの起きた場所で止まるようにするためです。

【補足】セレクトボックスで選択されている値の取得

まず該当するselect要素を取得してから、GetSelectedOption(要素).GetTextをすることにより、選択状態の値が取得できます。

Dim elmfound as WebElement
Dim str as String
With driver  
'セレクトボックスで選択されている値を取得
    If .IsPresent(By.Xpath, "//select[@name='〇〇']", 2000, , elmfound) = False Then Stop
    str = .GetSelectedOption(elmfound).GetText
   '【別の書き方】
  str = elmfound.GetSelectedOption.GetText
End With

2 テキストボックスの入力

まず該当のinputタグの要素を特定します。そして以下のとおりJavaScript利用によりテキストボックス入力しています。

Dim elmfound as WebElement
With driver
  If .IsPresent(By.Xpath, "//input[@name='〇〇']", 2000,,elmfound) = False Then Stop
  .ExecuteScript "arguments[0].value= '××';", elmfound
  '【別の書き方】コードが長くなっても引用符を減らしたい人向け
  .ExecuteScript "arguments[0].value= arguments[1]", elmfound, "××"

  ’××を変数strとする場合、&で繋げる
  .ExecuteScript "arguments[0].value='" & str & "';", elmfound
 '【注意】ExecuteScriptはdriverで始める必要があるため以下の記述はダメ
 'elmfound.ExecuteScript "arguments[0].value='" & str & "';"
 
  '直接、JavascriptのquerySelectorを使ってみる(待機処理なし)
  '//引用符の中に引用符があるので、ダブルコーテーションで代用した
  .ExecuteScript "document.querySelector(""input[name='〇〇']"").value=""××"";"

 ’【注】上記コードはイベントが発生しないため、イベントを発生させるにはSendKeysが安定
 elmfound.SendKeys "××", True 'Trueの場合は入力済データを消去してから入力する

End With

テキストの文字入力は、ExecuteScriptによりJavaScriptを実行することにより、値(××)を代入しています。arguments[0]は、コンマ1つめ(第1引数)に記述しているelmfoundを指しますので、そのvalueの値を××に更新することを示しています。※arguments[1]は、コンマ2つめ(第2引数)を指します

ExcecuteScriptにおけるargumentsの意味

(補足説明)
標準仕様のSendkeysを使わず、あえてJavaScriptを使っている理由は、モタツキなく、より高速で動作させることが業務上必要だったからです。


ただしinputタグ内にonchangeやonkeydownなどの属性が存在し、イベントが発生する仕組みの場合、上記コードでは発生しません。手動操作での再現性を重視する場合には不向きです。

一方、SendKeysであればイベントが確実に発生します。SendKeysを使用する場合、すでに入力フィールドに文字が存在した場合、追記されますので、クリアして入力する場合はオプション(第2引数)でTrueを指定します。

JavaScriptとSendKeysの文字入力は奥が深いです。メリットデメリットを検証した記事を以下に掲載しておりますので御参照ください。

3 値の取得処理

以下のコードで、値の取得処理で使用しています。
具体的には「GetAttribute」「GetText」「GetProprety」が該当します。

以下のコードのように「GetAttribute」はタグ内の属性の値を取得したい場合に、「GetText」は開始タグと閉じタグに囲まれたテキストを取得したい場合に使用することになります。

Dim elmfound as WebElement
Dim str as Stirng
With driver

  'GetAttribute(指定された属性の値を取得する)
  'HTMLの例:  <input name="〇〇" value="△△"></input>
  ’inputタグのうちvalue属性の値を取得
  If .IsPresent(By.Xpath "//input[@name='〇〇']", 2000, , elmfound) = False Then Stop
  str = elmfound.GetAttribute("value")

  'GetText(テキストの値を取得する)
  'HTMLの例:  <table><tbody><tr><td>"〇〇"</td></tr><//tbody></table>
  '/テーブルのセルであるtdタグのテキストの値を取得
   If .IsPresent(By.Xpath, "//table//tr[1]/td[1]", 2000, , elmfound) = False Then Stop
   str = elmfound.GetText

  'GetPropertyJavaScriptによる書き換え後の値を取得する)
  'HTMLの例:  <input name="〇〇" value="△△"></input> ※JavaScriptによりvalue値が変更
  'Chromeでは「F12」「Propreties」で確認可能。今回はそのPropertiesのうちvalueの値を取得
  If .IsPresent(By.Xpath, "//input[@name='〇〇']", 2000, , elmfound) = False Then Stop
  str = elmfound.GetProperty("value")

End With

このうち最後の「GetProprety」は、考え方は「GetAttribute」に近く、属性の値を取得するのですが、HTMLに反映されない<textarea>タグ内のvalue属性値の取得や、書き換えられた後の現在の属性値を取得したい場合に利用します。

属性値は、Chromeを御利用であれば「F12」を押したあとに現れる右側の画面(以下の画像)で確認できます。

ChromeデベロッパーツールのProperties画面

4 チェックボックス/ラジオボタンの選択

以下のコードにより該当のlabelタグの要素を特定します。選択の際は、IsSelectedにより、選択の有無の確認をしてから行うことになります。

'HTMLの例:
'<label>
'  <input type="radio"></input>
'  <span>〇〇</span>
'</label>
'<label>
' (以下続く)

Dim elmfound as WebElement
With driver 
 'テキストが〇〇であるラジオボタンが含まれるラベルを検索
  If .IsPresent(By.Xpath, "//label/span[text()='〇〇']/..", 2000, , elmfound) = False Then Stop
 
 'もし選択されていなければクリック(選択を解除する場合は「IsSelected = True」)
  If elmfound.IsSelected = False Then elmfound.click
End With

(注)labelタグをクリックしている理由は、inputタグをクリックした場合、i以下のエラーが出現することがあるからです。なおIsSelectedはlabel要素に対しても有効です。

要素が重なってクリックできないエラーの例

【補足】チェックボックス/ラジオボタンで選択状態の値を取得

checked属性の有無により判別したいこところですが、業務システムによってはあてになりません。その場合は、FindElementsによりinputタグを複数取得し、ループ処理により個別に判定します。

'HTMLの例:
'<label>
'  <input type="radio" name="××" checked="checked"></input>
'  <span>〇〇</span>
'</label>

Dim elmfound as WebElement
Dim str as String
With driver

'【checked属性により判別できる場合】
If .IsPresent(By.Xpath, "//label/input[@name='××'][@checked='checked']/../span", 2000, , elmfound) = False Then Stop
str =  elmfound.GetText

'【少し古い業務システムの場合】※checked属性では判別できない場合
 'name属性が××である複数のinputタグをループしてIsSelectedにより選択要素を拾う
 Dim elms As WebElements
 Set elms = .FindElements(By.XPath, "//label/input[@name='××']")
 For Each elmfound In elms
     If elmfound.IsSelected = True Then Exit For
 Next
 '基点から一階層上位に上がって直下のspanタグのテキストを取得
 If .IsPresent(By.Xpath, "./../span", 2000, elmfound, elmfound) = False Then Stop
 str =  elmfound.GetText
End With

5 テーブルの値の取得処理

 まずテーブル要素を検索してから、その要素に対して、TableToArrayを行うと、テーブルのデータが二次元配列として取得できます。

TableToArrayは「driver.TableToArray(element)」のように記述しますが、「element.TableToArray()」でも動作します。

 以下のコードでは二次元配列として取得したデータを、ExcelのアクティブシートのA1セルを左上として貼り付けています。

Dim elmTable as WebElement
Dim elmTbody as WebElement
Dim arrTable as Variant
Dim arrTbody as Variant
Dim strXpath as String
Dim Ws    as Worksheet

With driver
  'Xpathを指定(見出しに〇〇が存在するテーブル)
  strXpath="//th[text()='〇〇']/ancestor::table[1]"
    
  ’検索(elmTableを返す)
  If .IsPresent(By.Xpath, strXpath, 2000, , elmTable) = False Then Stop

  '見つかったelmTableを配列に格納(見出しはスキップ)
  arrTable =.TableToArray(elmTable, skipHeader:=True)

  '配列をアクティブのシートA1を基点として貼り付け
  Set Ws = ActiveSheet
  WS.Range("A1").Resize(UBound(arrTable, 1), UBound(arrTable, 2)).Value = arrTable
End With

(備考)「skipHeader:=True」は、見出しの取り込みを行わないものです。

6 FindElementsでの検索

FindElementsをよく使う場面は、要素の数をカウントしたい場合や、複数の要素として取得後にループ処理したい場合になります。

'【事例】テーブル内にリンクがあるセルのテキストをすべて配列に格納する
Dim linkText() as String '動的配列で宣言
Dim cnt        as Long
Dim elm        as WebElement
Dim elms       as WebElements

With driver
    '××という見出し名が存在するテーブルのうち、a要素のすべてを拾う
  Set elms = .FindElementsByXPath("//th[text()='××']/ancestor::tbody[1]//a")
  cnt = elms.Count

  '配列の要素数を特定(要素は0から始まるため1マイナス)
  ReDim LinkText(cnt - 1)
  
  '取得したa要素すべてを対象にそのセルのテキストを配列に格納する
   cnt = 0
   For Each elm In elms
       LinkText(cnt) = elm.GetText
       cnt = cnt + 1
   Next

   '取得した配列の要素すべてをイミディエイトウィンドウに表示する
    Dim value As Variant
    For Each value In LinkText
        Debug.Print value
    Next

End With

上記の事例では、取得したすべてのa要素を「For Each~」によりループして、そのテキストを配列に格納してから、イミディエイトウインドウに出力する事例となります。

7 日付と時間の入力フォーム

日付入力フォームの例

この事例は、HTMLが<input type="date">または<input type="time">の場合です。テキストボックスに日付や時間を入力したい場合は、必ずExecuteScript(SendKeys不可)で実行する必要があり、日付はyyyy-mm-dd形式、時間はhh:mm形式で入力してください。

Dim elmfound as WebElement
Dim strDate  as String
With driver

'日付として入力されているA1セルを代入する場合【Format関数で変換】
’【HTML】
’<input type="date" name="〇〇">

strDate = Format(Range("A1"), "yyyy-mm-dd")
If .IsPresent(By.Xpath, "//input[@type='date'][@name='〇〇']", 2000, , elmfound) = False Then Stop
.ExecuteScript "arguments[0].value= '" & strDate & "';", elmfound

    
'時刻として入力されているA1セルを代入する場合【Format関数で変換】
’【HTML】
’<input type="time" name="〇〇">

strDate = Format(Range("A1"), "hh:mm")
If .IsPresent(By.Xpath, "//input[@type='time'][@name='〇〇']", 2000, , elmfound) = False Then Stop
.ExecuteScript "arguments[0].value= '" & strDate & "';", elmfound

End With

(注)上記の例ではカレンダーを表示してから日付を選択することはできません(Seleniumの操作対象外)のでテキストボックスから行ってください。

(JavaScriptによるカレンダー呼び出しの場合)
JavaScriptによるクリックイベントでカレンダーを呼び出す場合は、テキストボックスは存在しませんので、ExecuteScriptでカレンダーを表示させます。
カレンダー表示させても上記の例とは異なり、SeniumVBAで操作できますのでHTMLコードで日付を指定するJavaScriptを解読してExecuteScriptで日付を指定しました。

JavaScriptによるカレンダー呼び出しHTMLの例
'(上記の例の場合)
'onclickの値をそのままExecuteScriptで実行する

With driver
.ExecuteScript "openCalendar('From', 'forma');"
.ExecuteScript "【JavaScriptを解読して日付指定】"
End With

8 ファイルのダウンロード

SetDownloadPrefs により、ブラウザ起動時にダウンロード用の一時フォルダの設定をしています。

Dim driver As SeleniumVBA.WebDriver
Set driver = SeleniumVBA.New_WebDriver
'一時保存フォルダの設定
Dim TMPATH As String
TMPATH = ThisWorkbook.Path & "\〇〇〇〇"

With driver
  'Chrome起動'
  .StartChrome
  Set CAPS = .CreateCapabilities
  'ダウンロードフォルダの設定(ダイアログ非表示、ブラウザ内蔵PDFViewer不使用)
  CAPS.SetDownloadPrefs TMPATH, promptForDownload:=False, disablePDFViewer:=True
  '開く
  .OpenBrowser CAPS
End With

ファイルのダウンロード時では、ファイル名は可変となると思われますので、一時フォルダにファイルの拡張子(xlsx等)が存在するまで繰り返す処理をしています。

一時フォルダ保存後は、目的のフォルダにファイルを移動して、一時フォルダ内のファイルを削除しています。

'保存先フォルダをTMPATHとしている
Do While Dir(TMPATH & "\*.xlsx", vbDirectory) = ""
   .Wait 300300ミリ秒待機
Loop

'TMPATHからファイルを移動する
Dim fname As String: fname = Dir(TMPATH & "\*.xlsx")
FileCopy TMPATH & "\" & fname, (移動先のフルパス)
’(同様に待機処理)

'TMPATH内のファイルを削除する'
 On Error GoTo Err_Wait
     Application.Wait (Now + TimeValue("0:00:01"))
     Kill TMPATH & "\*.xlsx"
 On Error GoTo 0
 Exit Sub
Err_Wait:
    Application.Wait (Now + TimeValue("0:00:01"))
    Resume
End Sub

9 既存要素を基点としてIsPresentで検索

IsPresentでXpathを利用すると、すでに検索した既存の要素を基点として、要素を検索することができ、業務でもよく利用しています。

例えば、私が愛用しているIsPresentを例に挙げますが、以下のとおり2通りの書き方があります。

Dim elmfound1 as WebElement
Dim elmfound2 as WEbElement
With driver

  ’【準備】
  ’Selectタグを調べた。見つかったらelmfound1が返ってくる
  .IsPresent(By.Xpath, "//select[@name='〇〇']",2000,,elmfound1) = False Then Stop

  '上記で見つけた要素(elmfound1)を基点にOptonタグのテキストを調べてelmfound2を取得してクリックする
  ’【記載例1】
  If .IsPresent(By.Xpath, "./option[text()='××']",2000,elmfound1,elmfound2) = False Then Stop
  ’【記載例2】
  If elmfound1.IsPresent(By.Xpath, "./option[text()='××']",2000,elmfound2) = False Then Stop
  'クリックする
  elmfound2.Click

End With

「記載例1」と「記載例2」は、どちらも同じ結果になります。違いは、基点(elmfound1)をどこに書くかです。私は「記載例1」の書き方をよく利用しています。

また両方の記載例にもありますが、Xpathの記述で基点を指定する場合は、「.」から始まるように書かないと、思うように動作しない可能性がありますので御注意ください。

10 【付録】ダブルクリック/マウス移動

要素にイベントが仕込まれている場合、ActionChainを経由してダブルクリックやマウス移動することができます。

'【HTMLの例】
<input type="text" name="〇〇" ondblclick="alert('ダブルクリックしたよ')" />


'(ActionChainを経由してダブルクリックする)
With driver

'アクションチェーンの設定
Dim actions As SeleniumVBA.WebActionChain
Set actions = .ActionChain
    
'ダブルクリックする要素の特定
Dim elmfound As WebElement
If .IsPresent(By.XPath, "//input[@name='〇〇']", 2000, , elmfound) = False Then Stop
 
'ダブルクリックする
actions.DoubleClick(elmfound).Perform
'マウス移動したい場合
actions.MoveToElement(elmfound).Perform

End With

終わりに(関連記事の紹介)

この記事における動作確認は2025年1月5日時点です。
指定は正しいはずなのに、要素が見つからずに困っているという方も多いと思います。私のこれまでの経験では、要素を選択できない事態となったことはありません。下記記事を御参考にしていただければ幸いです。

また、業務ではテーブル内のセルにボタンやリンクがあり、その位置を特定してクリックしたいというケースが良くあります。その際は、下記記事を御参考にしていただければ幸いです。

いいなと思ったら応援しよう!