Excel VBAでこれまで使えていた InternetExplorerがWindows 11で使えなくなりました。(と思ったら、また使えるようになっていた。よくわからないが、いずれは使えなくなる前提でコーディングしておいた方がよいでしょう。)
Excelはサポート切れの2010と、ちと古いです。
ちょっと調べると、代替え策としてフォームのWebBrowserを使う方法が出てきます。Navigateでurlのページを表示するだけならこれが良さそうです。ですが、DOMのメソッドを使って操作する場合、使えないメソッドがいくつかあります。たとえば
getElementsByName
getElementsByClassName
でエラーが発生します。他にも使えないメソッドがあるかもしれません。
これへの解決策として、次のコードのようにDocument.Allを使って全エレメントを取得し、個々にnameやclassNameをチェックする方法が出てきます。
Dim elm As HtmlElement
For Each elm In WebBrowser1.Document.All
I if elm.GetAttribute("className") = "someName" Then
Debug.Printh elm.InnerText
End If
Next
これは単発で使うには十分かと思いますが、ループで使用するにはいささか重い処理になりそうで、検討の余地ありです。加えてclassには複数の値が設定されることが多く、"="での比較はほとんど場合に不適切でしょう。
なお、「このページのスクリプトでエラーが発生しました」が発生する場合は、navigateを呼ぶ前に次の行を追加します。
WebBrowser1.Silent = True
次に見つけたのはMSXML2.XMLHTTPを使う方法です。次のようなコードでXmlDocumentを作ります。
Dim MyRequest As Object
Set MyRequest = CreateObject("MSXML2.XMLHTTP")
MyRequest.Open "GET", url
MyRequest.send
Do Until MyRequest.readyState = 4: DoEvents: Loop
Set xmldoc = MyRequest.responseXML
これで成功する場合は良さそうですが、これは TLS 1.2 以上には対応していないという問題があるようです。次のようにするとよいという指摘があります。
Set MyRequest = CreateObject("MSXML2.ServerXMLHTTP")
私は MSXML2.XMLHTTP.6.0 を使いましたが、今のところ問題ありません。
Set MyRequest = CreateObject("MSXML2.XMLHTTP.6.0")
なお、MSXML2.XMLHTTP.6.0 でエラーが出る場合は参照を追加します。
ツール⇒参照設定⇒Microsoft XML v6.0追加
その他、次のライブラリ参照も必要です。
Microsoft HTML Object Library
後述のサンプルにあるRegExpを使うには、このライブラリも必要です。
Microsoft VBScript Regular Expressions 5.5
さらに調べた結果、次の方法が見つかりました。これはHTMLをDOMDocumentとして取得し、それからHTMLDocumentを作ります。
これはよい結果でしたので、次のようなFunctionを作り、実際に使用しています。
Public Function GetHTMLDoc(url As String)
Dim httpReq As Object
Set httpReq = CreateObject("MSXML2.XMLHTTP.6.0")
httpReq.Open "GET", url, False
httpReq.send (Null)
Do Until httpReq.readyState = 4: DoEvents: Loop
Dim htmlDoc As IHTMLDocument
Set htmlDoc = New HTMLDocument
htmlDoc.Write httpReq.responseText
Set GetHTMLDoc = htmlDoc
End Function
この方法はWebページを表示する必要がなく、DOMメソッドで操作するのが目的な場合に適しています。表示を行わないため、処理は高速です。表示も必要な場合は、WebBrowerにHTMLをセットすればよいでしょう(試していませんが)。
さて、これで作った HTMLDocument ですが、ちょっと奇妙な動きをします。
htmlDoc.GetElementsByClassName(className)
は動作します。(先頭の"G”は大文字です。)
ですが、個々の HtlmElement には getElementsByClassName を適用できません。
この範囲で十分であれば、従来のIEと同様の方法で利用することができます。
ですが、ある Element の子要素から getElementsByClassName で要素抽出をしたい場合には処理が複雑になります。
そこで、次のようなFunctionを作りました。あるエレメント内の指定のtagNameのものからclassNameを含むものを抽出します。
Public Function GetElementsByClassName(htmlElm As IHTMLElement, tagName As String, className As String)
Dim elm As Object, attr As String, list As Collection
Dim re As New RegExp: re.pattern = "(^| )" & className & "( |$)"
Set list = New Collection
For Each elm In htmlElm.getElementsByTagName(tagName)
If re.test(elm.getAttribute("className")) Then
list.Add elm
End If
Next
Set GetElementsByClassName = list
End Function
正規表現はclass属性の最初、または最後か、前後がスペースで区切られているものとマッチします。
re.pattern = "(^| )" & className & "( |$)"
戻り値の型はCollectionになります。インデックスを使う場合、配列と異なり先頭の要素のインデックスは 1 になることに注意してください。
加えて、次のようなFunctionも作りました。私の場合、getElementsByClassNameで見つかった最初の要素を使うことが多く、IEを使った処理では次のようなコードでした。
name = someElm.getElementsByClassName("name")(0).innerText
そこで、Collectionで返さず最初に見つかった要素を返すようにしました。
Public Function GetFirstElementByClassName(htmlElm As IHTMLElement, tagName As String, className As String)
Dim elm As Object, val As String
Dim re As New RegExp: re.pattern = "(^| )" & className & "( |$)"
For Each elm In htmlElm.getElementsByTagName(tagName)
If re.test(elm.getAttribute("className")) Then
Set GetFirstElementByClassName = elm
Exit Function
End If
Next
Set GetFirstElementByClassName = Nothing
End Function
呼び出し方は次のようになります。
name = GetFirstElementByClassName(someElm, "div", "name").innerText
戻り値がNothingとなる場合があるなら、次のようなチェックを入れます。
Set elm = GetFirstElementByClassName(someElm, "div", "name")
If Not elm Is Nothing Then name = elm.innerText
HtmlDocumentの先頭からclassNameのものを探したい場合は htmlDoc.body または htmlDoc.DocumentElement を htmlElm として渡します。
divs = GetElementsByClassName(htmlDoc.body, "div", "someClass")
私の場合はclassNameでエレメントを抽出する場合はtagNameも決まっているのでこの方が都合がよく、かつ処理速度も多少なりとも速くなっているだろうと思います。
もし異なるtagNameのものも含めて抽出したい場合は、たとえばtagNameにvbNullStringを渡し、次のような使い分けをすればよいでしょう。
Dim elms as IHTMLElementCollection
If tagName = vbNullString Then
Set elms = htmlElm.all
Else
Set elms = htmlElm.getElementsByTagName(tagName)
End If
結果的にはWebBrowserによる表示が不要なため、この変更で処理速度は格段に速くなりました。
補足
この例で生成する HtmlDocument にはいくつか制約があります。
・sectionなど非対応のtagがあり、その場合はHTMLUnknownElementとなり、単独のElementとして作られるがDOM構造には含まれず、getElementsByTagName、allなどのメソッド/プロパティーが使用できない。
・原因ははっきりしませんが、フリーズすることがありました。速度が速くなりすぎ、Excelの再描画追いつかないためかもしれません。そんなときは再描画の一時中止/再開を試してみてください。
Application.ScreenUpdating = False
再描画が発生する処理
Application.ScreenUpdating = True