【问题标题】:Facing issue with extracting data from a website using VBA面临使用 VBA 从网站中提取数据的问题
【发布时间】:2019-11-29 18:07:44
【问题描述】:

我需要从以下网站https://www.mrci.com/ohlc/ohlc-all.php 提取 COCOA London Dec20 和 Mar21 收盘价 Screenshot for the website

我为此编写了以下代码,但它抛出错误,请帮助:

Sub extract()

Dim appIE As Object

Set appIE = CreateObject("internetexplorer.application")

With appIE

    .Navigate "https://www.mrci.com/ohlc/ohlc-all.php"

    .Visible = False

End With

Do While appIE.Busy

    DoEvents

Loop

Set allRowOfData = appIE.document.getElementsByClassName("strat").getElementsByTagName("tbody")(183)

Dim myValue As String: myValue = allRowOfData.Cells(5).innerHTML

appIE.Quit

Set appIE = Nothing

lastrow = Sheets("Sheet2").Cells(Rows.Count, "A").End(xlUp).Row + 1

Range("A" & lastrow).Value = myValue

End Sub
  • 代码出现以下错误:

运行时错误 - 438 对象不支持该属性或方法。

【问题讨论】:

  • 错误在哪一行?
  • 非常感谢您的支持。但是,我的系统在使用 HTMLDocument 时抛出“未定义用户定义的类型”错误。我需要更改一些设置吗?
  • 您需要在我的答案底部添加项目参考

标签: html excel vba web-scraping


【解决方案1】:

您的错误是因为您正在对集合调用文档/节点方法。

Set allRowOfData = appIE.document.getElementsByClassName("strat").getElementsByTagName("tbody")(183)

getElementsByClassName("strat") 返回一个你需要索引的集合,然后使用方法getElementsByTagName

例如

Set allRowOfData = appIE.document.getElementsByClassName("strat")(0).getElementsByTagName("tbody")(183)

总结方法:

由于不同futures的数据排列在一长串表行(tr)节点中,需要通过前导future标题(th)确定trs的正确块,检查随后的兄弟trs 以获得正确的mmmyy,然后从行中提取右列(td)值。需要在下一个future 块的开头或兄弟trs 的结尾处停止;以先到者为准。


tl;dr;

HTML 不适合快速识别正确的trs;你使用的索引越多,程序就越脆弱。以下确实有假设,但更稳健。

我使用XMLHTTP request,因为不需要使用浏览器。您有一个未来感兴趣的变量,所有标头ths 都被className 使用css 类选择器收集到nodeList 中,该nodeList 循环直到找到目标future。这会将您置于感兴趣的第一行的开头。各种mmmyy 然后在后续行中。我通过使用辅助函数检查表标题并与定义的目标标题名称进行比较来确定适当的列索引。该函数返回找到标头的适当索引,如果未找到则返回-1。

现在,我有一本字典,其中包含 mmmyy 感兴趣的时期。我循环所有tds,直到我点击下一部分(即下一个tr,它的th有一个标题(th)作为它的FirstChild)。我连续检查每个FirstChild,如果找到的mmmyy 值在字典中,我会使用适当的列值更新字典。

在循环中,我的工作级别低于HTMLDocument,因此,为了利用querySelectorAll,我将当前的nextNode.NextSibling.OuterHTML 转储到代理 HTMLDocument 变量中;然后我可以再次访问querySelectorAll,并且可以按索引选择适当的td。我需要将传输的 html 包装在 <TABLE><TD></TABLE> 标签中,以便 HTML 解析器不会抱怨并获得正确的 #td 元素。可以肯定的是,如果我有时间重新审视这一点,我可能会加强这一点。

最后,我将字典值写到工作表中。


VBA:

Option Explicit

Public Sub GetCocoaClosePrices()
    Dim html As MSHTML.HTMLDocument, targetPeriods As Object, targetFuture As String, targetColumnName As String

    targetFuture = "London Cocoa(LCE)"
    targetColumnName = "Close"
    Set targetPeriods = CreateObject("Scripting.Dictionary")
    Set html = New MSHTML.HTMLDocument

    targetPeriods.Add "Dec20", "Not found"
    targetPeriods.Add "Mar21", "Not found"

    With CreateObject("MSXML2.XMLHTTP")
        .Open "GET", "https://www.mrci.com/ohlc/ohlc-all.php", False
        .Send
        html.body.innerHTML = .responseText
    End With

    Dim tableHeaders As Object, targetColumnNumber As Long

    Set tableHeaders = html.querySelectorAll("tr ~ tr ~ tr .colhead")
    targetColumnNumber = GetTargetColumnNumber(tableHeaders, targetColumnName)

    If targetColumnNumber = -1 Then Exit Sub

    Set targetPeriods = GetUpdatedDictionary(targetPeriods, html, targetFuture, targetColumnNumber)

    With ThisWorkbook.Worksheets(1)
        .Cells(1, 1).Resize(1, targetPeriods.Count) = targetPeriods.keys
        .Cells(2, 1).Resize(1, targetPeriods.Count) = targetPeriods.items
    End With
End Sub

Public Function GetUpdatedDictionary(ByRef targetPeriods As Object, ByVal html As HTMLDocument, ByVal targetFuture As String, ByVal targetColumnNumber As Long) As Object
    Dim html2 As MSHTML.HTMLDocument, firstChild As Object, i As Long
    Dim nextNode As Object, headerNodes As Object

    Set headerNodes = html.querySelectorAll(".note1")
    Set html2 = New MSHTML.HTMLDocument

    For i = 0 To headerNodes.Length - 1
        If headerNodes.Item(i).innerText = targetFuture Then 'find the right target future header
            Set nextNode = headerNodes.Item(i).ParentNode 'move up to the parent tr node

            Do 'walk the adjacent tr nodes
                Set nextNode = nextNode.NextSibling
                Set firstChild = nextNode.firstChild

                If nextNode Is Nothing Then
                    Set GetUpdatedDictionary = targetPeriods
                    Exit Function 'exit if no next section
                End If

                html2.body.innerHTML = "<TABLE><TD>" & nextNode.outerHTML & "</TABLE>"

                If targetPeriods.Exists(firstChild.innerText) Then
                    targetPeriods(firstChild.innerText) = html2.querySelectorAll("td").Item(targetColumnNumber).innerText
                End If
            Loop While firstChild.tagName <> "TH" 'stop at next section i present

        End If
    Next
    Set GetUpdatedDictionary = targetPeriods
End Function

Public Function GetTargetColumnNumber(ByVal nodeList As Object, ByVal targetColumnName As String) As Long
    Dim i As Long

    For i = 0 To nodeList.Length - 1
        If nodeList.Item(i).innerText = targetColumnName Then
            GetTargetColumnNumber = i + 1 'to account for th
            Exit Function
        End If
    Next
    GetTargetColumnNumber = -1
End Function

阅读:

  1. css selectors
  2. document.querySelectorAll
  3. Node.nextSibling
  4. Node.parentNode

参考资料(VBE>工具>参考资料):

  1. Microsoft HTML 对象库

【讨论】:

  • 效果很好!昨天还想着回答,结果用了5多分钟就放弃了! :)
  • @Vityata 我有兴趣看到另一个答案。我担心我会陷入困境,错过解决这些问题的其他方法。另外,希望能提供有关如何改进我的答案的反馈。前几天我在帮助一个 OP 聊天,并意识到我现在倾向于在答案中给出不那么冗长的解释。
  • @Vityata 我忘了说谢谢你的编辑!赞赏。
  • 不客气:) 我写了我的版本,它基于 OP 的代码。这几乎就是我昨天打算写的内容。
【解决方案2】:

正如@QHarr's answer 中提到的,代码的问题是在集合上调用了文档/模式方法。无论如何,遵循 OP 的方法,并尽可能多地使用他们的代码,这是我创建的逻辑:

  • 打开 URL 并转到网站
  • 使用allRowsOfData = appIE.document.getElementsByClassName("strat") 获取所有行
  • 开始循环遍历.ChildrenallROwsOfData
  • 通过.Children.Children 开始一个内部循环,又名“孙子”:)
  • 对于每一个“孙子”,看看是否是innerText中带有“London Cocoa(LCE)”的那个。
  • 如果是,则创建found = True,然后在接下来的几行中,我们将找到Dec20Mar21 的数据并将其写下来。
  • 需要进行另一项检查,以确保我们不会用下一个表覆盖数据 - If InStr(1, child2.outerhtml, "th class=") And Not CBool(InStr(1, child2.outerhtml, target))。因此,如果child2.outerhtml 包含“th class=”并且它不是来自London Cocoa(LCE) 的那个,那么found = False
  • 如果found 为True,则开始循环遍历带有句点的字典并检查第5 列的值。这不是很灵活,只要设计改变,就会出错。
  • 最后我们从字典写入 Excel 工作表

代码:

Sub TestMe()

    Dim appIE As Object
    Set appIE = CreateObject("InternetExplorer.Application")

    With appIE
        .Navigate "https://www.mrci.com/ohlc/ohlc-all.php"
        .Visible = True
    End With

    Do While appIE.Busy: DoEvents: Loop

    Dim allRowsOfData As Variant
    allRowsOfData = appIE.document.getElementsByClassName("strat")

    Dim found As Boolean: found = False
    Dim target As String: target = "London Cocoa(LCE)"

    Dim targetPeriods As Object
    Set targetPeriods = CreateObject("Scripting.Dictionary")
    targetPeriods.Add "Dec20", "Not found"
    targetPeriods.Add "Mar21", "Not found"

    Dim child As Variant
    Dim child2 As Variant
    Dim myKey As Variant

    For Each child In allRowsOfData.Children
        For Each child2 In child.Children

            If InStr(1, child2.innerText, target) Then found = True
            If InStr(1, child2.outerhtml, "th class=") And _
                            Not CBool(InStr(1, child2.outerhtml, target)) Then
                found = False
            End If

            If found Then
                For Each myKey In targetPeriods.keys
                    If Left$(child2.innerText, Len(myKey)) = myKey Then
                        targetPeriods(myKey) = child2.Children(5).innerText
                        Debug.Print child2.Children(5).innerText
                    End If
                Next
            End If
        Next
    Next

    Dim i As Long: i = 1
    For Each myKey In targetPeriods
        With Worksheets(1)
            .Cells(i, 1) = myKey
            .Cells(i, 2) = targetPeriods(myKey)
            i = i + 1
        End With
    Next

    appIE.Quit

End Sub

奖金 - 整个代码,分为子和函数,在我的网站 - https://www.vitoshacademy.com/vba-extracting-financial-data-from-a-website-in-table-format/

【讨论】:

  • 将 For Each 与集合一起使用会得到提升。好的。对类型化函数进行比较会稍微快一些,例如左$。
  • @QHarr - 谢谢。不知怎的,我已经好几年没用过$ 函数了,但它们确实更快。
猜你喜欢
  • 2019-03-05
  • 1970-01-01
  • 1970-01-01
  • 2021-01-23
  • 1970-01-01
  • 1970-01-01
  • 2021-10-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多