その他

【VBAブラウザ操作】seleniumの導入前後でコード比較(Before After)[No64]

スポンサーリンク

Internet Explorer対応のスクレイピングプログラムも、最近はEdge化の影響で正常に動作しなくなってきました。

selenium basicをインストールしたけど、プログラムをどのように旧型式のソースコードからselenium化すれば良いか、迷った方も多いのではないでしょうか。

今回はそんな方々向けに、seleniumをインストールする前と後でコードがどのように変わるかについてまとめてみました。

行いたいこと

今回のプログラムは、yahooの「検索バー」上の「ウェブ」「画像」「動画」などの情報を取得し、メッセージボックスを出力するプログラムとします。

大まかな調べ方は、該当サイトをデベロッパーツールで確認します。操作する要素を確認するのは、どの手法を使っても同じです。

要素をコピーしてテキストファイルに貼ると、よりプログラムを作成しやすいと思います。下図は、classで指定するクラス名を検索して色付けした様子です。

className01 = "_33uErKrZG6jQv2aeVeGWGc"

className02 = "fQMqQTGJTbIMxjQwZA2zk _3tGRl6x9iIWRiFTkKl3kcR"

「selenium:利用なし」のプログラム

プログラム01:従来のInternetExplorerオブジェクトを利用した場合


Sub RunTest_InternetExplorer()
    Dim objIE As InternetExplorer
    Set objIE = CreateObject("Internetexplorer.Application")
    
    objIE.Visible = True
    objIE.navigate "https://www.yahoo.co.jp/"
    
    Application.Wait Now() + TimeValue("00:00:03")
    
    Dim htmlDoc As HTMLDocument
    Set htmlDoc = objIE.document

    Dim className01 As String
    className01 = "_33uErKrZG6jQv2aeVeGWGc"
    
    Dim var As String
    var = htmlDoc.getElementsByClassName(className01)(0).innerHTML
'    var = htmlDoc.getElementsByClassName(className01)(0).innerText
'    var = htmlDoc.getElementsByClassName(className01)(0).outerHTML
'    var = htmlDoc.getElementsByClassName(className01)(0).outerText
    MsgBox var

    Dim listData As IHTMLElementCollection
    Set listData = htmlDoc.getElementsByClassName(className01)
    
    Dim varList01 As String
    Dim msgText01 As String
    For i = 0 To listData.Length - 1
        varList01 = listData(i).innerHTML
'        varList01 = listData(i).innerText
'        varList01 = listData(i).outerHTML
'        varList01 = listData(i).outerText
        msgText01 = msgText01 & "▼(i=" & i & ")" & varList01 & vbCrLf
    Next i
    MsgBox prompt:=msgText01, Title:="昇順(01)"

    Dim className02 As String
    className02 = "fQMqQTGJTbIMxjQwZA2zk _3tGRl6x9iIWRiFTkKl3kcR"
    
    Dim varList02 As String
    Dim msgText02 As String
    For i = 0 To listData.Length - 1
        varList02 = listData(i).getElementsByClassName(className02)(0).innerHTML
'        varList02 = listData(i).getElementsByClassName(className02)(0).innerText
'        varList02 = listData(i).getElementsByClassName(className02)(0).outerHTML
'        varList02 = listData(i).getElementsByClassName(className02)(0).outerText
        msgText02 = msgText02 & "▼(i=" & i & ")" & varList02 & vbCrLf
    Next i
    MsgBox prompt:=msgText02, Title:="昇順(02)"
    
    msgText02 = htmlDoc.getElementsByClassName(className01)(0).getElementsByClassName(className02)(0).innerHTML
''''    msgText02 = htmlDoc.getElementsByClassName(className01)(0).getElementsByClassName(className02)(0).innerText
''''    msgText02 = htmlDoc.getElementsByClassName(className01)(0).getElementsByClassName(className02)(0).outerHTML
''''    msgText02 = htmlDoc.getElementsByClassName(className01)(0).getElementsByClassName(className02)(0).outerText
    MsgBox prompt:=msgText02, Title:="昇順(02)-test"

End Sub

「selenium:利用あり」のプログラム

プログラム02:従来のプログラムを崩さないように努めた場合


Sub RunTest_seleniumWebDriver01()
    Dim driver As New EdgeDriver
    driver.Get "https://www.yahoo.co.jp/"
    driver.Wait 1000
    
    Dim htmlDoc As HTMLDocument
    Set htmlDoc = New HTMLDocument
    htmlDoc.body.innerHTML = driver.PageSource
    'htmlDoc -> all -> Item(*)内のinnerHTMLに値が入る。

    Dim className01 As String
    className01 = "_33uErKrZG6jQv2aeVeGWGc"

    Dim var As String
    var = htmlDoc.getElementsByClassName(className01)(0).innerHTML
'    var = htmlDoc.getElementsByClassName(className01)(0).innerText
'    var = htmlDoc.getElementsByClassName(className01)(0).outerHTML
'    var = htmlDoc.getElementsByClassName(className01)(0).outerText
    MsgBox var

    Dim listData As IHTMLElementCollection
    Set listData = htmlDoc.getElementsByClassName(className01)

    Dim varList01 As String
    Dim msgText01 As String
    
    'For Each iList In listData
    'Next
    For i = 0 To listData.Length - 1
        varList01 = listData(i).innerHTML
'        varList01 = listData(i).innerText
'        varList01 = listData(i).outerHTML
'        varList01 = listData(i).outerText
        msgText01 = msgText01 & "▼(i=" & i & ")" & varList01 & vbCrLf
    Next i
    MsgBox prompt:=msgText01, Title:="昇順(01)"

    Dim className02 As String
    className02 = "fQMqQTGJTbIMxjQwZA2zk _3tGRl6x9iIWRiFTkKl3kcR"

'''    '---[▼▼▼A]
'''    '期待していたコードでエラーになる。
'''    Dim varList02 As String
'''    Dim msgText02 As String
'''    For i = 0 To listData.Length - 1
'''        varList02 = listData(i).getElementsByClassName(className02)(0).innerHTML
''''        varList02 = listData(i).getElementsByClassName(className02)(0).innerText
''''        varList02 = listData(i).getElementsByClassName(className02)(0).outerHTML
''''        varList02 = listData(i).getElementsByClassName(className02)(0).outerText
'''
'''        msgText02 = msgText02 & "▼(i=" & i & ")" & varList02 & vbCrLf
'''    Next i
'''    msgText02 = listData(0).getElementsByClassName(className02)(0).innerHTML
'''    msgText02 = htmlDoc.getElementsByClassName(className01)(0).getElementsByClassName(className02)(0).innerHTML
'''    MsgBox prompt:=msgText02, Title:="昇順(02)"

'''    '---[▼▼▼B]
'''    '意図した結果は得られない。
    Set listData = Nothing
    Set listData = htmlDoc.getElementsByClassName(className02)

    Dim varList02 As String
    Dim msgText02 As String
    For i = 0 To listData.Length - 1
        varList02 = listData(i).innerHTML
'        varList02 = listData(i).innerText
'        varList02 = listData(i).outerHTML
'        varList02 = listData(i).outerText
        msgText02 = msgText02 & "▼(i=" & i & ")" & varList02 & vbCrLf
    Next i
'        msgText02 = htmlDoc.getElementsByClassName(className02)(0).innerHTML
    MsgBox prompt:=msgText02, Title:="昇順(02)"
    
End Sub

スポンサーリンク

プログラム03:selenium標準の書き方で書いた場合


Sub RunTest_seleniumWebDriver02()
    Dim driver As New EdgeDriver
    driver.Get "https://www.yahoo.co.jp/"
    driver.Wait 1000

    Dim className01 As String
    className01 = "_33uErKrZG6jQv2aeVeGWGc"

    Dim var As String
    var = driver.FindElementByClass(className01).Attribute("innerHTML")
'    var = driver.FindElementByClass(className01).Attribute("innerText")
'    var = driver.FindElementByClass(className01).Attribute("outerHTML")
'    var = driver.FindElementByClass(className01).Attribute("outerText")
'    var = driver.FindElementByClass(className01).Text
    MsgBox var
    
    Dim elm01 As Selenium.WebElements
    Set elm01 = driver.FindElementsByClass(className01)
 
    Dim varElm01 As String
    Dim msgText01 As String
    For i = 1 To elm01.Count
        varElm01 = elm01(i).Attribute("innerHTML")
'        varElm01 = elm01(i).Attribute("innerText")
'        varElm01 = elm01(i).Attribute("outerHTML")
'        varElm01 = elm01(i).Attribute("outerText")
'        varElm01 = elm01(i).Text
        msgText01 = msgText01 & "▼(i=" & i & ")" & varElm01 & vbCrLf
    Next i
    MsgBox prompt:=msgText01, Title:="昇順(01)"

    Dim className02 As String
    className02 = ".fQMqQTGJTbIMxjQwZA2zk._3tGRl6x9iIWRiFTkKl3kcR"
    
    Dim varElm02 As String
    Dim msgText02 As String
    For i = 1 To elm01.Count
        varElm02 = elm01(i).FindElementByCss(className02).Attribute("innerHTML")
'        varElm02 = elm01(i).FindElementByCss(className02).Attribute("innerText")
'        varElm02 = elm01(i).FindElementByCss(className02).Attribute("outerHTML")
'        varElm02 = elm01(i).FindElementByCss(className02).Attribute("innerText")
'        varElm02 = elm01(i).FindElementByCss(className02).Text
        msgText02 = msgText02 & "▼(i=" & i & ")" & varElm02 & vbCrLf
    Next i
    MsgBox prompt:=msgText02, Title:="昇順(02)"
End Sub

わかったこと

従来形式のプログラム(プログラム01)をselenium化するのは、簡単なプログラムの置き換え作業だけでできるのではないかと期待していたが、それはできないことがわかりました。

従来の「Dim htmlDoc As HTMLDocument」、「Set htmlDoc = objIE.document」の形式は利用しない方が良い

従来のコードでは、下記のようなhtmlDocが大活躍するコードを書くことが可能でした。


    Dim htmlDoc As HTMLDocument
    Set htmlDoc = objIE.document

しかし、seleniumでは「webdriver.document」は用意されていません。

下図のようにすれば再現できるのではないかと考えましたが、完全には再現することができていないと調査後に判明しました。


	Dim htmlDoc As HTMLDocument
    Set htmlDoc = New HTMLDocument
    htmlDoc.body.innerHTML = driver.PageSource

このコードは以下2つの意味を表しています。

  1. 「HTMLDocument」のオブジェクトで「htmlDoc」を宣言し(この時点ではhtmlDocはただの箱)、Newする』ことで、「HTMLDocument」の構造を「htmlDoc」にセットしインスタント化する。
  2. 「htmlDoc」にある「body」内の「innerHTML」に「driver.PageSource」を貼り付ける。

VBA開発画面のウォッチを使うことで、流れをより理解できると思います。ちなみに、このソースを実行する際、「htmlDoc -> all -> Item(*)内のinnerHTML」に値が入ることもウォッチで確認済みです。

ここまで聞くと、上手く行くように思えますよね。

「htmlDoc」に「getElementbyID」、「getElementsbyTagName」、「getElementsbyName」、「getElementsbyClassName」を1回する分にはエラーにならず、きちんと結果が取得できます。

しかし、それぞれのgetを2回以上行うと、「Microsoft Visual Basic」「実行時エラー '438':オブジェクトは、このプロパティまたはメソッドをサポートしていません。」と表示されてしまいます。

これはプログラム02に限った話で、同様にプログラム01で「htmlDoc」に2回以上Getを行ってもエラーにはなりませんでした。

「Set htmlDoc = objIE.document」の「htmlDoc」を扱う時は「objIE.document」を参照してみていましたが、「htmlDoc.body.innerHTML = driver.PageSource」では用意したhtmlDocという箱に「driver.PageSource」を代入したため、恐らく全ての情報を参照できなかったのではないかと思われます。そのため、複数回のGETはできないのではと考えました。

IHTMLElementCollectionを使った従来のコードをそのまま移行し、実行するとエラーになることが多いのもそのためだと考えられます。コレクションにはGETした値が入ることが多く、それに対してもう一度GETしようとすると、トータルで複数回GETしたことになってエラーになるのでしょう。

どうしても複数回GETしたい場合、いきなり1回目のGETで2回目のGETを使う方法もあります。ただし、1回目→2回目のように絞り込んでいくことができないため、目的の要素以外をGETする可能性があるので気をつけなければなりません。

半角スペースの入った複合クラス(複数クラス)が使えなくなることの対策

例えば、「className01 className02」のクラスをGETする場合は、「.className01.className02」とドットで繋げれば抽出することが可能です。

最後までお付き合いいただきありがとうございます!

この情報が誰かの役にたてれば幸いです。

スポンサーリンク

-その他