VBAからGDI+を使う資料集
inet上の達人の方々から御教示いただいたコードを少しアレンジさせてもらっています(切り貼りとも言います)。
  1. ホーム
  2. Other
  3. mshtmldom


MSHTML DOM を使ってWebページのTabelをExcel Worksheetに取り込む

ASPやPHP等で動的に生成されるWebページの情報を取得するのに、ActiveX.exeであるIEのお世話にならざるを得ないケースがあります。 最初はgetElementsByTagNameで取得できるOuterHtmlを元にテンポラリのHtmlDocumentを段階的に生成して取得するのにトライ しましたが、<td>の下位のhtmlがうまく取得できません。仕方がないので、MSXML DOMを扱った経験がある事から、MSHTML DOMでの取得にトライしてみました。MSHTML DOMのVBA(VB6)の情報はWeb検索してもなかなか見当たらず、たまにみつかるC++等のコードは深くて良く分からんという状況で苦労させられました。
(追伸)JavaScriptのDOMを勉強すると良さそうです。IEもVersion9以降は標準に近づいている様ですね。

ちょっとバージョンアップ
気象庁のホームページから過去の気温等のデータを取り込もうとしたら、動きませんでした。<TABLE>タグ内に、<CAPTION>というタグがあるのが原因でしたので、 それも取り込んで活用できる様に改造しました。
また、セルの結合に対応させようと、属性を取り込もうと思い、MSXMLに倣って、attributes属性から取り出そうとしましたが、全くうまくいきません。 attributes.lengthを表示させると、108とかが取得されます。煩悩の数じゃあるまいし...。結局目的のTagのNodeのところで実行をStopし、 ローカルウィンドウを表示させる事で見つけました。Node.colSpan, Node.rowSpan で取得できるのでした。これらはインテリセンスでは表示されませんが、 属性としてcolSpanを持たない<TH>, <TD>タグでも1が取得されるので、動的に生成される訳ではなくて、タグの種類に応じて持っている属性とみるべきでしょうか。

開いているIEの最前面TABのWebページのTable(複数)の情報を、ワークシート毎に取得します。


'Microsoft Internet Control
'Microsoft HTML Object Library
'に参照設定
Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)



Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
                    (ByVal lpClassName As String, _
                    ByVal lpWindowName As String) As Long
                    
'TD内をスキャンして取得したテキストを収納する
'TDタグ内に、二つの文字列があるといった複雑な表に対応するため、ModuleレベルのCollectionを使用
Dim textCollection As Collection

Dim ie As WebBrowser

Sub getTableFromOpenPage()
  Dim ie As WebBrowser
  'AとBが相違しているのがなんか変だがこうしないとエラーになる。
  '従って、インテリセンスも効かないで大変だがやむを得ず。
  Dim doc As IHTMLDocument '-A
  
  Dim tbElements As IHTMLElementCollection
  Dim trNodes As IHTMLDOMChildrenCollection
  Dim tdNodes As IHTMLDOMChildrenCollection
  Dim tbElement As IHTMLElement
  Dim trNode As IHTMLDOMNode
  Dim tdNode As IHTMLDOMNode
  Dim tempNode As IHTMLDOMNode
  
  Dim tblCnt As Long
  Dim rowCnt As Long
  Dim columnCnt As Long
  Dim collectCnt As Long
  Dim wbk As Workbook
  Dim sh As Worksheet
  Dim buf As String
  
  Dim i As Long, j As Long, k As Long
  Dim tableCaption As String
  Dim destCell As Range
  
  Set ie = getTopIeTab
  If ie Is Nothing Then Exit Sub
  Set doc = New HTMLDocument '-B
  
  doc.write ie.document.body.outerHTML
  Set tbElements = doc.getElementsByTagName("table")
  If tbElements.Length = 0 Then Exit Sub
  Set wbk = ThisWorkbook
  tblCnt = 1 'Tableのカウンター
  Do While tbElements.Length > wbk.Worksheets.Count
    wbk.Worksheets.Add
  Loop
  For Each sh In wbk.Worksheets
    sh.Cells.Clear
  Next sh
  For Each tbElement In tbElements
    'Table毎にワークシートを変える
    Set sh = wbk.Sheets(tblCnt)
    '<table>の下に、ソース上は存在しない<TBODY>が存在している
    '<tr>タグはその下にあるらしい
    
    '気象庁のページ <TBODY>と並列に、その前に<CAPTION>というタグがある
    'このため、ChildNodesのところで、「実装されていません」という訳の分からないエラーが出た
    'nodeNameがTBODYのところから引っ張ってくる様にした。
    For j = 0 To tbElement.ChildNodes.Length
      Select Case tbElement.ChildNodes(j).nodeName
        Case "CAPTION"
          tableCaption = tbElement.ChildNodes(j).FirstChild.NodeValue
        Case "TBODY"
          Set trNodes = tbElement.ChildNodes(j).ChildNodes
          Exit For
      End Select
    Next j
    sh.Cells(1).Value = tableCaption
  
    rowCnt = 2 '行のカウンター
    For Each trNode In trNodes
      '名前はtdNodeであるが、tdとthを含む
      columnCnt = 1 '列のカウンター
      
      For Each tdNode In trNode.ChildNodes
        Set textCollection = New Collection
        buf = ""
        getNodeText tdNode
        If textCollection.Count > 0 Then
          For collectCnt = 1 To textCollection.Count
            If collectCnt = 1 Then
              buf = textCollection.Item(collectCnt)
            Else
              buf = buf & "," & textCollection.Item(collectCnt)
            End If
          Next collectCnt
        End If
        '行、列方向の結合に対応
        Set destCell = sh.Cells(rowCnt, columnCnt)
        Do While destCell.MergeCells = True
          Set destCell = destCell.Offset(0, 1)
          columnCnt = columnCnt + 1
        Loop
        '
        destCell.Value = buf
        ' <TH rowSan= xx>の、rowSpanといった属性は、Node.Attributesからは取得できない
        '(lengthが100個以上にのぼる別ものらしい。メンバもnodeと類似したものである)
        'Nodeの種類に応じて動的にメンバを有しており(インテリセンスで表示されない)決め打ちで取得可能
        'node.colSpan, node.rowSpanという様に。確認するには条件を満足したらStopするコードを組んでおいて
        'Local Windowで確認すれば可能。
        
        If tdNode.colSpan > 1 Or tdNode.rowSpan > 1 Then
          destCell.Resize(tdNode.rowSpan, tdNode.colSpan).Merge
        End If
        Set textCollection = Nothing
        columnCnt = columnCnt + 1
      Next tdNode
      rowCnt = rowCnt + 1
    Next trNode
    tblCnt = tblCnt + 1
  Next tbElement
  Set doc = Nothing
  Exit Sub

End Sub

'ノード内の全Textを取得してCollection(モジュールレベル変数)に納める関数
'再帰使用している
Sub getNodeText(myNode As IHTMLDOMNode)
  Dim myChildNode As IHTMLDOMNode
  Dim buf As String
  
  '半角スペース1個という#textが存在するページあり
  If myNode.nodeName = "#text" And Trim(myNode.NodeValue) <> "" Then
    textCollection.Add myNode.NodeValue
    Exit Sub
  End If
  '下位のnodeまで調べる
  If myNode.HasChildNodes Then
    For Each myChildNode In myNode.ChildNodes
      getNodeText myChildNode
    Next myChildNode
  End If
End Sub

'IEの最前面Tabを取得
Function getTopIeTab(Optional matchWord As String) As WebBrowser
  Dim hWnd As Long
  Dim ie As WebBrowser
  Dim targetIe As WebBrowser
  Const IEClassName As String = "IEFrame"     'IEのClass名
  
  hWnd = FindWindow(IEClassName, vbNullString)
  If hWnd = 0 Then Exit Function
  For Each ie In CreateObject("Shell.Application").Windows()
    If hWnd = ie.hWnd Then
      ie.StatusBar = True
      ie.statusText = CStr(hWnd)
      If ie.statusText = CStr(hWnd) Then
        If matchWord = "" Then
            Set getTopIeTab = ie
            Exit Function
        Else
          If InStr(ie.LocationURL, matchWord) > 0 Then
            Set getTopIeTab = ie
            Exit Function
          End If
        End If
      End If
    End If
  Next ie
  Set getTopIeTab = Nothing
End Function

☆おまけ Webページの構造を、IEでXMLを開いた時の様に階層構造でWorksheetに出力する-Attribute表示改良中
'結構処理時間がかかるので留意のこと。
’IE8と、IE9以降の分岐が必要かもしれない。
'http://blogs.msdn.com/b/ie_jp/archive/2011/10/03/10219055.aspx

'Microsoft Internet Control
'Microsoft HTML Object Library
'に参照設定

Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
                    (ByVal lpClassName As String, _
                    ByVal lpWindowName As String) As Long
                    
'ワークシートへの出力行を示す変数(再帰使用するのでモジュールレベルで宣言)
Dim myRow As Long

Sub analyzeWebPage()
  Dim ie As InternetExplorer
  Dim doc As IHTMLDocument '-A
  Dim chNode As IHTMLDOMNode
  Dim i As Long, j As Long, k As Long
  Dim wbk As Workbook
  Dim sh As Worksheet
    
  Set ie = getTopIeTab
  If ie Is Nothing Then Exit Sub
  Set doc = New HTMLDocument '-B
  Set doc = ie.document
  Set wbk = ThisWorkbook
  Set sh = wbk.Sheets(1)
  sh.Cells.ClearContents
  sh.Activate
  
  myRow = 1

  'Docのメンバはインテリセンスされないので、Stopしてローカルウィンドウで調査
  For Each chNode In doc.ChildNodes
    scanNode chNode, 1
  Next chNode
  Set doc = Nothing
  sh.Cells.WrapText = False
End Sub


'再帰的にスキャンして下位NodeのTagと値をセルに出力する
Sub scanNode(myNode As IHTMLDOMNode, myLevel As Long)
  Dim myChildNode As IHTMLDOMNode
  Dim newLevel As Long
  Dim i As Long
  Dim myAttribute As IHTMLDOMAttribute
  Dim buf As String
  
  newLevel = myLevel + 1
  Select Case myNode.NodeType
    Case 3
      Cells(myRow, myLevel).Value = myNode.NodeValue
    Case 8
      Cells(myRow, myLevel).Value = myNode.NodeValue
    Case Else
      If Not myNode.Attributes Is Nothing Then
        For i = 0 To myNode.Attributes.Length - 1
          Set myAttribute = myNode.Attributes(i)
          If myAttribute.specified And myAttribute.Value <> "" Then
            buf = buf & " " & myAttribute.nodeName & " = '" & myAttribute.NodeValue & "'"
          End If
        Next i
          Cells(myRow, myLevel).Value = "<" & myNode.nodeName & buf & ">"
      Else
        Cells(myRow, myLevel).Value = "<" & myNode.nodeName & ">"
      End If
  End Select
      myRow = myRow + 1
  If myNode.HasChildNodes Then
    For Each myChildNode In myNode.ChildNodes
      scanNode myChildNode, newLevel
    Next myChildNode
  End If
End Sub

'nodeType
'1 要素ノード
'2 属性ノード
'3 テキストノード
'4 CDATA ノード
'5 実体参照ノード
'6 実体ノード
'7 処理命令ノード
'8 注釈宣言ノード
'9 文書ノード
'10  文書型宣言ノード
'11  文書切片ノード
'12  表記法ノード


'IEの最前面Tabを取得
Function getTopIeTab(Optional matchWord As Variant) As InternetExplorer
  Dim hWnd As Long
  Dim ie As InternetExplorer
  Dim targetIe As InternetExplorer
  Const IEClassName As String = "IEFrame"     'IEのClass名
  
  hWnd = FindWindow(IEClassName, vbNullString)
  For Each ie In CreateObject("Shell.Application").Windows()
    If hWnd = ie.hWnd Then
      ie.StatusBar = True
      ie.statustext = CStr(hWnd)
      If ie.statustext = CStr(hWnd) Then
        If IsMissing(matchWord) Then
            Set getTopIeTab = ie
      ie.statustext = ""
            Exit Function
        Else
          If InStr(ie.LocationURL, matchWord) > 0 Then
            Set getTopIeTab = ie
            Exit Function
          End If
        End If
      End If
    End If
  Next ie
  Set getTopIeTab = Nothing
End Function

'今後使用予定
Function getIEversion() As String
    Dim fso As Object
    Dim ieVersion As String
    
    Set fso = CreateObject("Scripting.FileSystemObject")
    ieVersion = fso.GetFileVersion("C:\Program Files\Internet Explorer\IEXPLORE.EXE ")
    '11.0.9600.16521
    getIEversion = Split(ieVersion, ".")(0)
    Set fso = Nothing
End Function

'☆ おまけ2 TextNodeのテキスト取得法のIEバージョンによる動作の違いを試すためのコード
'VirtualBox(Windows2000)で試したが結果は同じだった。IE6をインストールしてある筈だが、IEのファイルバージョンは5であった?
'Microsoft Internet Control
'Microsoft HTML Object Library
'に参照設定


Sub test()
  Dim doc As IHTMLDocument '-A
  Dim chNode As IHTMLDOMNode
  Dim myNodes As IHTMLElementCollection
  Dim buf As String
    
  Set doc = New HTMLDocument '-B
  buf = "<html><head></head><body><p>test</p></body></html>"
  doc.write buf
  Set myNodes = doc.getElementsByTagName("p")
  
  Debug.Print myNodes.Length  '1(IE11)
  With myNodes(0)
    Debug.Print .nodeName   'P(IE11)
    Debug.Print .NodeValue  'Null(IE11)
    Debug.Print .NodeType   '1(IE11)
    Debug.Print .innerText  'test(IE11)
    Debug.Print .FirstChild.NodeValue 'test(IE11)
    Debug.Print .innerHTML    'test(IE11)
    Debug.Print .outerHTML    '<P>test</P>(IE11)
    Debug.Print .HasChildNodes 'true(IE11)
  End With
  With myNodes(0).FirstChild
    Debug.Print .nodeName   '#text(IE11)
    Debug.Print .NodeValue  'test(IE11)
    Debug.Print .NodeType   '3(IE11)
    'Debug.Print .innerText '非サポート(IE11)
    'Debug.Print .innerHTML  '非サポート(IE11)
    'Debug.Print .outerHTML  '非サポート(IE11)
    Debug.Print .HasChildNodes 'False(IE11)
    
  End With
  'Stop 'local windowでメンバを探る
  Set doc = Nothing
End Sub

'☆ おまけ3 WebPageを動的に作成。javascriptのコードも与えてやる。
'docment.script.setTimeoutの制限がきつくなった事もあり、発展型を下記にのせました。
発展的移転先:Excel/VBAからJavaScriptのコードを実行してみる