Excel VBA Web Scraping Tables не отвечает; MSXML2.ServerXMLhttp.6.0 Метод - PullRequest
0 голосов
/ 06 ноября 2018

Я создал веб-скребок с помощью Excel VBA, который выполняет следующие действия:

  1. Считывает одну ссылку за раз из списка ссылок на листе под названием "CIK_Links".
  2. Идет по ссылке, читает ее текст ответа, и если в этом тексте ответа он находит гиперссылку, для которой innerHTML читает: «(Список всех фондов и классов / контрактов для», то сохраняет эту ссылку в переменную и создает другую. Объект MSXML2.ServerXMLhttp.6.0.
  3. После создания объекта он находит третью таблицу в тексте ответа, просматривает и находит определенные элементы этой таблицы, а затем выводит эти значения в Excel на листе с именем «Parsed_Tables».
  4. Затем код переходит к следующей ссылке на листе «CIK_Links» и повторяет шаги 1-3. Примечание: на листе около 640 000 ссылок, но я выполняю цикл только для нескольких тысяч одновременно. И да, я пытался запустить его всего за 10, 20, 100 одновременно, но проблема все еще сохраняется.

Проблема, с которой я столкнулся, заключается в том, что, как только я нажимаю «Выполнить», я получаю сообщение «Excel не отвечает», но код все еще работает в фоновом режиме. Код работает отлично и очень быстро, учитывая то, что я прошу его сделать, но, очевидно, мне нужно оптимизировать его еще больше, чтобы он не перегружал Excel. Было бы полезно найти какой-либо способ избежать записи разобранного HTML в Excel на каждой итерации, однако я не знаю, как я мог бы записать данные в нужном мне формате без этого. Решение с массивом было бы неплохо, но перед записью в Excel пришлось бы довольно много манипулировать данными в массиве, возможно, даже помещая подмножество / нарезая массив. Мне нужна помощь, поскольку я исчерпал все свои знания, и я провел немало исследований в ходе создания этого приложения. Я даже открыт для использования других технологий, таких как Python и библиотека Beautifulsoup, я просто не знаю, как выводить данные таблицы в файл CSV в нужном мне формате. Заранее спасибо!

Вот файл: TrustTable_Parse.xlsb

Отказ от ответственности: У меня есть степень бакалавра. в математике я научился кодировать на VBA, SQL и R, реализовав множество своих проектов на каждом языке. Дело в том, что если мой код выглядит странно или вы думаете, что я делаю вещи неэффективно, то это потому, что я не кодирую годами и не знаю ничего лучше, смеется.

Ниже мой код:

Option Explicit

Sub Final_Parse_TrustTables()

Dim HTML As New HTMLDocument
Dim http As Object
Dim links As Object
Dim Url, Trst As String
Dim link As HTMLHtmlElement
Dim i As Long

Dim http2 As Object
Dim HTML2 As New HTMLDocument
Dim tbl As Object
Dim ele As HTMLHtmlElement

Dim wb As Workbook
Dim ws, ws_2 As Worksheet

    'sets ScreenUpdating to false _ 
     turns off event triggers, ect.
    OptimizeCode_Begin


 Set wb = ThisWorkbook

 Set ws = wb.Sheets("CIK_Links")

 'Creates this object to see if Trust table exists
 Set http = CreateObject("MSXML2.ServerXMLhttp.6.0")

  'Loops through the list of URL's _
  in the 'CIK_Links' Sheet
  For i = 2 To 3000

   'List of URL's
    Url = ws.Range("C1").Cells(i, 1).Value2

    'Gets webpage to check _
    if Trust table exists
    On Error Resume Next
    http.Open "GET", Url, False
    http.send


    'Runs code If the website sent a valid response to our request _
    for FIRST http object
    If Err.Number = 0 Then

     If http.Status = 200 Then

      'If the website sent a valid response to our request _
      for SECOND http object "http2"
      If Err.Number = 0 Then

       If http2.Status = 200 Then

        HTML.body.innerHTML = http.responseText

        Set links = HTML.getElementsByTagName("a")

        'Determines if there is a trust table and if so _
        then it creates the http2 object and gets the _
        trust table responsetext 
        Trst = "(List all Funds and Classes/Contracts for"
        For Each link In links
            'Link is returned in responsetext with "about:/" at _
            the beginning instead of https://www.sec.gov/, so I _
            used this to replace the "about:/"
            If InStr(link.innerHTML, Trst) > 0 Then
                link = Replace(link, "about:/", "https://www.sec.gov/")
                Debug.Print link

        'Creates this object to go to trust table webpage
        Set http2 = CreateObject("MSXML2.ServerXMLhttp.6.0")

        'Gets webpage to parse _
        trust table
        On Error Resume Next
        http2.Open "GET", link, False
        http2.send

            HTML2.body.innerHTML = http2.responseText

                'If there exists a Trust, then this refers to the _
                3rd table on the trust table webpage; _
                note ("table")(3)
                On Error Resume Next
                Set tbl = HTML2.getElementsByTagName("table")(3)

                Set ws_2 = wb.Sheets("Parsed_Tables")

                With ws_2

                    For Each ele In tbl.getElementsByTagName("tr")
                    'First finds rows with Class/Con numbers
                    If InStr(ele.innerText, "C00") Then
                     'Pulls Class/Con Numbers, note children(2)
                       'output to col E sheet
                        .Cells(Rows.Count, "E"). _
                        End(xlUp).Offset(1, 0).Value2 = ele.Children(2).innerText

                      'Outputs Share Class, children(3)
                        'Output to col F sheet
                        .Cells(Rows.Count, "F"). _
                        End(xlUp).Offset(1, 0).Value2 = ele.Children(3).innerText

                      'Not not all Funds have Ticker _
                       so this keeps the module from _
                       asking for object to be set
                      On Error Resume Next
                      'Outputs Ticker to excel
                         'Reads the last value in Col F and offsets Ticker to _
                         to show directly in adjacent cel in Col G
                         .Cells(Rows.Count, "F"). _
                         End(xlUp).Offset(0, 1).Value2 = ele.Children(4).innerText

                    'Pulls SIC number
                    ElseIf InStr(ele.innerText, "S00") Then
                        'Offsets from col F to be placed in col C
                        .Cells(Rows.Count, "F"). _
                        End(xlUp).Offset(1, -3).Value2 = ele.Children(1).innerText

                      'Pulls Fund Name
                        'Offsets from col F to col D
                        .Cells(Rows.Count, "F"). _
                        End(xlUp).Offset(1, -2).Value2 = ele.Children(2).innerText

                    'Pulls CIK number
                    ElseIf InStr(ele.Children(0).innerText, "000") Then
                        'Offset from col F to col A
                        .Cells(Rows.Count, "F"). _
                        End(xlUp).Offset(1, -5).Value2 = ele.Children(0).innerText

                      'Pulls Trust Name
                        'Offsets from col F to col B
                        .Cells(Rows.Count, "F"). _
                        End(xlUp).Offset(1, -4).Value2 = ele.Children(1).innerText

                    End If

                    'Counts the number of iterations of the loop _
                     and places it in the lower left corner of the _
                     workbook
                     Application.StatusBar = "Current Iteration: " & i

                   Next

               End With

            End If

         Next

        End If

        Else
        MsgBox "Error loading webpage", vbExclamation, "Alert!!!"
        Exit Sub

      End If
      On Error GoTo 0

     End If

     Else
     MsgBox "Error loading webpage", vbExclamation, "Alert!!!"
     Exit Sub

    End If

On Error GoTo 0

 If i Mod 1000 = 0 Then
  ActiveWorkbook.Save
  Application.Wait (Now + TimeValue("0:00:03"))
 End If

Next i

    'sets everything back to normal after running code 
    OptimizeCode_End

End Sub

Ниже приведен пример ссылок, перечисленных в листе CIK_Links:

https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=2&owner=include&count=02
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=3&owner=include&count=02
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=11&owner=include&count=02
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=13&owner=include&count=02
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=14&owner=include&count=02
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=17&owner=include&count=02
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=18&owner=include&count=02
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=2110&owner=include&count=02
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=2135&owner=include&count=02
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=2145&owner=include&count=02
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=2663&owner=include&count=02
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=2664&owner=include&count=02
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=2691&owner=include&count=02
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=2768&owner=include&count=02
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=3521&owner=include&count=02
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=3794&owner=include&count=02
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=4123&owner=include&count=02
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=4405&owner=include&count=02
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=4568&owner=include&count=02

1 Ответ

0 голосов
/ 06 ноября 2018

Я не думаю, что ваш код будет работать, если не будет хотя бы одного On Error Resume Next, скрывающего некоторые ошибки времени выполнения. Например, у вас есть If http2.Status = 200 Then до создания экземпляра объекта http2.

Ниже приведен метод, который определенно можно улучшить, но он использует класс для хранения объекта xmlhttp и предоставляет методы для получения необходимой информации. Макет желаемой таблицы делает анализ текущей веб-страницы особенно сложным. Вы можете остаться с этим. Я решил использовать структуру таблицы как есть. Возможно, это может дать вам основу по крайней мере. Вы бы добавили в него свои пользовательские дополнительные вызовы оптимизации.


TODO:

Посмотрите, можно ли сделать оценку для массива результатов увеличенного размера, который может содержать все результаты, а не массив массивов, так что выписка может быть выполнена на ходу. Если у меня будет время, я внесу эту поправку.


Класс clsHTTP

Option Explicit

Private http As Object
Const SEARCH_TERM As String = "(List all Funds and Classes/Contracts"

Private Sub Class_Initialize()
    Set http = CreateObject("MSXML2.XMLHTTP")
End Sub

Public Function GetString(ByVal Url As String, Optional ByVal search As Boolean = False) As String
    Dim sResponse As String
    searchTermFound = False
    With http
        .Open "GET", Url, False
        .setRequestHeader "If-Modified-Since", "Sat, 1 Jan 2000 00:00:00 GMT"
        .send
        sResponse = StrConv(.responseBody, vbUnicode)
        If InStr(sResponse, SEARCH_TERM) > 0 Then searchTermFound = True
        GetString = sResponse
    End With
End Function

Public Function GetLink(ByVal html As HTMLDocument) As String
    Dim i As Long, nodeList As Object
    Set nodeList = html.querySelectorAll("a")
    GetLink = vbNullString
    For i = 0 To nodeList.Length - 1
        If InStr(nodeList.item(i).innerText, SEARCH_TERM) > 0 Then
            GetLink = Replace$(nodeList.item(i).href, "about:/", "https://www.sec.gov/")
            Exit For
        End If
    Next
End Function

Public Function GetInfo(ByVal html As HTMLDocument) As Variant
    Dim CIK As String, table As HTMLTable, tables As Object, tRows As Object
    Dim arr(), tr As Object, td As Object, r As Long, c As Long

    Set tables = html.querySelectorAll("table")

    If tables.Length > 3 Then
        CIK = "'" & html.querySelector(".search").innerText
        Set table = tables.item(3)
        Set tRows = table.getElementsByTagName("tr")
        ReDim arr(1 To tRows.Length, 1 To 6)
        Dim numColumns As Long, numBlanks As Long

        For Each tr In tRows
            numColumns = tr.getElementsByTagName("td").Length
            r = r + 1: c = 2: numBlanks = 0
            If r > 4 Then
                arr(r - 4, 1) = CIK
                For Each td In tr.getElementsByTagName("td")
                    If td.innerText = vbNullString Then numBlanks = numBlanks + 1
                    arr(r - 4, c) = td.innerText
                    c = c + 1
                Next td
                If numBlanks = numColumns Then Exit For
            End If
        Next
    Else
        ReDim arr(1, 1)
        GetInfo = arr
        Exit Function
    End If

    arr = Application.Transpose(arr)
    ReDim Preserve arr(1 To 6, 1 To r - 4)
    arr = Application.Transpose(arr)
    GetInfo = arr
End Function

Стандартный модуль 1

Option Explicit
Public searchTermFound As Boolean

Public Sub GetInfo()
    Dim wsLinks As Worksheet, links(), link As Long, http As clsHTTP
    Dim lastRow As Long, html As HTMLDocument, newURL As String
    Set wsLinks = ThisWorkbook.Worksheets("CIK_Links")
    Set http = New clsHTTP
    Set html = New HTMLDocument
    With wsLinks
        lastRow = GetLastRow(wsLinks, 3)
        If lastRow = 2 Then
            ReDim links(1, 1)
            links(1, 1) = .Range("C2").Value
        Else
            links = .Range("C2:C" & lastRow).Value
        End If
    End With
    Dim results(), arr(), i As Long, j As Long
    ReDim results(1 To UBound(links, 1))
    For link = LBound(links, 1) To UBound(links, 1)

        If InStr(links(link, 1), "https://www.sec.gov") > 0 Then

            html.body.innerHTML = http.GetString(links(link, 1), True)

            If searchTermFound Then

                newURL = http.GetLink(html)
                html.body.innerHTML = http.GetString(newURL, False)
                arr = http.GetInfo(html)

                If UBound(arr, 1) > 1 Then
                    i = i + 1
                    results(i) = arr
                End If
            End If
        End If
    Next

    Dim wsOut As Worksheet
    Set wsOut = ThisWorkbook.Worksheets("Parsed_Tables")

    For j = 1 To i
        arr = results(j)
        With wsOut
             .Cells(GetLastRow(wsOut, 1), 1).Resize(UBound(arr, 1), UBound(arr, 2)) = arr
        End With
    Next
End Sub

Public Function GetLastRow(ByVal ws As Worksheet, Optional ByVal columnNumber As Long = 1) As Long
    With ws
        GetLastRow = .Cells(.Rows.Count, columnNumber).End(xlUp).Row
    End With
End Function
...