Элементы веб-скребков по классу и имени тега - PullRequest
4 голосов
/ 19 апреля 2019

Я пытаюсь скопировать данные с нижеуказанного веб-сайта, мне нужен весь диапазон размеров, цена, удобства, скидки, резерв. Я создаю кадр ниже кода, но я могу правильно скопировать элемент. Первым делом только три элемента справляются с дублированием, и я не получаю результат для Удобств и Резервирования. Кто-нибудь, пожалуйста, посмотрите на это?

Sub text()


Dim ie As New InternetExplorer, ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Unit Data")
With ie
    .Visible = True
    .Navigate2 "https://www.safeandsecureselfstorage.com/self-storage-lake-villa-il-86955" 

    While .Busy Or .readyState < 4: DoEvents: Wend

    Sheets("Unit Data").Select


    Dim listings As Object, listing As Object, headers(), results()
    Dim r As Long, list As Object, item As Object
    headers = Array("size", "features", "Specials", "Price", "Reserve")
    Set list = .document.getElementsByClassName("units_table")
    '.unit_size medium, .features, .Specials, .price, .Reserve
    Dim rowCount As Long
    rowCount = .document.querySelectorAll(".tab_container li").Length
    ReDim results(1 To rowCount, 1 To UBound(headers) + 1)
    For Each listing In list
            For Each item In listing.getElementsByClassName("unitinfo even")
            r = r + 1

          results(r, 1) = listing.getElementsByClassName("size secondary-color-text")(0).innerText
          results(r, 2) = listing.getElementsByClassName("amenities")(0).innerText
           results(r, 3) = listing.getElementsByClassName("offer1")(0).innerText
        results(r, 4) = listing.getElementsByClassName("rate_text primary-color-text rate_text--clear")(0).innerText
          results(r, 5) = listing.getElementsByClassName("reserve")(0).innerText





        Next
    Next
    ws.Cells(1, 1).Resize(1, UBound(headers) + 1) = headers
    ws.Cells(2, 1).Resize(UBound(results, 1), UBound(results, 2)) = results
    .Quit
End With

  Worksheets("Unit Data").Range("A:G").Columns.AutoFit
End Sub

Ответы [ 3 ]

2 голосов
/ 25 апреля 2019

tl; dr;

Извиняюсь заранее (некоторым) за длину ответа, но я подумал, что воспользуюсь этим педагогическим моментом, чтобы подробно рассказать о происходящем.

Общий подход, который я использую, такой же, как и в вашем коде: найдите селектор css для изоляции строк (несмотря на то, что на разных вкладках маленькие, средние и большие все еще присутствуют на странице):

Set listings = html.querySelectorAll(".unitinfo")

Выше приведены строки.Как и прежде, мы помещаем это в новый HTMLDocument, чтобы мы могли использовать querySelector/querySelectorAll методы.


Rows:

Давайте посмотрим наПервая строка HTML мы получаем.В следующих разделах эта строка будет рассмотрена в качестве примера для обсуждения того, как извлекается информация:

5x5</TD> <TD class=features>
<DIV id=a5x5-1 class="icon a5x5">
<DIV class=img><IMG src="about:/core/resources/images/units/5x5_icon.png"></DIV>
<DIV class=display>
<P>More Information</P></DIV></DIV>
<SCRIPT type=text/javascript>
                // Refine Search
                //
                $(function() {
                    $("#a5x5-1").tooltip({
                        track: false,
                        delay: 0,
                        showURL: false,
                        left: 5,
                        top: 5,
                        bodyHandler: function () {
                            return "        <div class=\"tooltip\">            <div class=\"tooltop\"></div>            <div class=\"toolmid clearfix\">                <div class=\"toolcontent\">                    <div style=\"text-align:center;width:100%\">                        <img alt=\"5 x 5 storage unit\" src=\"/core/resources/images/units/5x5.png\" />                    </div>                    <div class=\"display\">5 x 5</div>                    <div class=\"description\">Think of it like a standard closet. Approximately 25 square feet, this space is perfect for about a dozen boxes, a desk and chair, and a bicycle.</div>                </div>                <div class=\"clearfix\"></div>            </div>            <div class=\"toolfoot\"></div>            <div class=\"clearfix\"></div>        </div>        "}
                    });
                });
        </SCRIPT>
</TD><TD class=rates>
<DIV class="discount_price secondary-color-text standard_price--left">
<DIV class=price_text>Web Rate: </DIV>
<DIV class="rate_text primary-color-text rate_text--clear">$39.00 </DIV></DIV>
<SCRIPT>
$( document ).ready(function() {
    $('.units_table tr.unitinfo').each(function(index, el) {
        if ($(this).find('.standard_price').length != '' && $(this).find('.discount_price').length != '') {
            $(this).parents('.units_table').addClass('both');
            $(this).addClass('also-both');
            $(this).find('.rates').addClass('rates_two_column');
        }
    });
});
</SCRIPT>
</TD><TD class=amenities>
<DIV title="Temperature Controlled" class="amenity_icon icon_climate"></DIV>
<DIV title="Interior Storage" class="amenity_icon icon_interior"></DIV>
<DIV title="Ground Floor" class="amenity_icon icon_ground_floor"></DIV></TD><TD class=offers>
<DIV class=offer1>Call for Specials </DIV>
<DIV class=offer2></DIV></TD><TD class=reserve><A id=5x5:39:00000000 class="facility_call_to_reserve cta_call primary-color primary-hover" href="about:blank#" rel=nofollow>Call </A></TD>

Каждая строка, с которой мы будем работать, будет иметь одинаковый html внутри переменной html2.Если вы сомневались, посмотрите на javascript в функции, показанной выше:

$('.units_table tr.unitinfo').each(function(index, el) 

, он использует тот же селектор (но также указывает класс родительской таблицы и тип элемента (tr)).По сути, эта функция вызывается для каждой строки в таблице.


Размер:

По какой-то причине открывающий тег td удаляется, поэтомупо размеру, а не по классам, я ищу начало закрывающего тега и извлекаю строку там.Я делаю это, передавая возвращаемое значение, заданное Instr (где <было найдено в строке) -1, в функцию <code>Left$ (напечатано).

enter image description here

results(r, 1) = Left$(html2.body.innerHTML, InStr(html2.body.innerHTML, "<") - 1)

Возвращает 5x5.


Описание:

Столбец описания заполняется функцией, которую мывидел выше (который применяется к каждой запоминаемой строке)

Этот бит - $("#a5x5-1").tooltip - сообщает ему, куда нацеливаться, а затем оператор return функции предоставляет html с div классомdescription, содержащий текст, который мы хотим.Поскольку мы не используем браузер, и я нахожусь на 64-битных окнах, я не могу оценить этот сценарий, но я могу использовать split, чтобы извлечь строку (описание) между "description\"> и началом закрытия, связанного div tag:

results(r, 2) = Split(Split(html2.querySelector("SCRIPT").innerHTML, """description\"">")(1), "</div>")(0)

Возвращает:

"Думайте об этом как о стандартном шкафу. Приблизительно 25 квадратных футов, это место идеально подходит для примерно дюжины ящиков, столаи стул, и велосипед. "


Тип ставки и цена:

Они просты и используют имя класса для цели:

results(r, 3) = Replace$(html2.querySelector(".price_text").innerText, ":", vbNullString)
results(r, 4) = Trim$(html2.querySelector(".rate_text").innerText)

Возврат (соответственно)

Интернет-тариф, £ 39,00


Удобства:

Здесь все немного сложнее.

Давайте еще раз рассмотрим приведенный выше HTML-код для этой строки, относящейся к удобствам:

<TD class=amenities>
<DIV title="Temperature Controlled" class="amenity_icon icon_climate"></DIV>
<DIV title="Interior Storage" class="amenity_icon icon_interior"></DIV>
<DIV title="Ground Floor" class="amenity_icon icon_ground_floor"></DIV></TD>

Мы можем видеть, что родительский td имеет класс amenities, который имеет дочерние div элементы, которые имеют составные имена классов;последний из которых, в каждом случае, служит идентификатором для типа удобства, например icon_climate.

Когда вы наводите курсор на них, на странице отображается информация всплывающей подсказки:

enter image description here

Мы можем проследить местоположение этой подсказкив html текущей страницы:

enter image description here

При наведении указателя мыши на различные удобства этот контент обновляется.

Короче говоря, длинная история (он говорит, пока на полпути вниз по странице!) , этот контент обновляется из php-файла на сервере.Мы можем сделать запрос на файл и создать словарь, который отображает имя класса каждого объекта, например, amenity_icon icon_climate (которые, как и составные классы, должны "" заменяться на "." При преобразовании в соответствующий селектор css из .amenity_icon.icon_climate)к связанным описаниям.Вы можете изучить файл php здесь .

Файл php:

Давайте рассмотрим только начало файла, чтобы разобратьБазовая единица повторяющегося рисунка:

function LoadTooltips() {
        $(".units_table .amenity_icon.icon_climate").tooltip({
        track: false,
        delay: 0,
        showURL: false,
        left: -126,
        top: -100,
        bodyHandler: function () {
            return "<div class=\"sidebar_tooltip\"><h4>Temperature Controlled</h4><p>Units are heated and/or cooled. See manager for details.</p></div>"
        }
    });

Функция, отвечающая за обновление всплывающей подсказки, - LoadTooltips.Селекторы классов CSS используются для нацеливания на каждый значок:

$(".units_table .amenity_icon.icon_climate").tooltip

И у нас есть bodyhandler, задающий текст возврата:

bodyHandler: function () {
            return "<div class=\"sidebar_tooltip\"><h4>Temperature Controlled</h4><p>Units are heated and/or cooled. See manager for details.</p></div>"

У нас есть три бита полезной информации, которые появляются в повторяющихся группах.Селектор имени класса для элемента, краткое описание и полное описание, например,

  1. .amenity_icon.icon_climate: мы используем это для сопоставления описаний файлов php с именем класса значка удобства в нашей строке. Селектор CSS
  2. Temperature Controlled;внутри h4 тега всплывающей подсказки, возвращающего текст. Краткое описание
  3. Units are heated and/or cooled. See manager for details.;внутри p тега всплывающей подсказки, возвращающего текст. Длинное описание

Я пишу 2 функции, GetMatches и GetAmenitiesDescriptions, которые используют регулярное выражение для извлечения всех повторяющихся элементов для каждого значкаи вернуть словарь, в котором ключом является селектор css, а в качестве значения - короткий description : long description.

Когда я соберу все значки в каждой строке:

Set icons = html2.querySelectorAll(".amenity_icon")

Iиспользуйте словарь для возврата описания всплывающей подсказки на основе имени класса значка

For icon = 0 To icons.Length - 1 'use class name of amenity to look up description
    amenitiesInfo(icon) = amenitiesDescriptions("." & Replace$(icons.item(icon).className, Chr$(32), "."))
Next        

. Затем я объединяю описания с vbNewLine, чтобы обеспечить вывод в разных строках в выходной ячейке.

Вы можете изучить регулярное выражение здесь .

Регулярное выражение использует синтаксис | (Или), поэтому я возвращаю все сопоставленные шаблоны в одном списке.

arr = GetMatches(re, s, "(\.amenity_icon\..*)""|<h4>(.*)<\/h4>|<p>(.*)<\/p>")

Поскольку мне потребуются различные подсовпадения (селектор класса css 0,1 или 2, короткий дескриптор, длинный деск), я использую Select Case i mod 3, с переменной счетчика i, чтобы извлечь соответствующий подпункт-matches.

Пример соответствия для сопоставления в php-файле:

enter image description here


Скидки:

Мы вернулись к селекторам классов.Offer2 не заполнено, поэтому вы можете удалить.

results(r, 6) = html2.querySelector(".offer1").innerText
results(r, 7) = html2.querySelector(".offer2").innerText

возвращает (соответственно):

Вызов Specials, пустая строка


Заключительные замечания:

Итак, вышеизложенное проведет вас через один ряд.Это просто промыть и повторить в цикле по всем рядам.Для удобства данные добавляются в массив results;который затем записывается Sheet1 за один раз.Я вижу некоторые незначительные улучшения, но это быстро.


VBA:

Option Explicit
Public Sub GetInfo()
    Dim ws As Worksheet, html As HTMLDocument, s As String, amenitiesDescriptions As Object
    Const URL As String = "https://www.safeandsecureselfstorage.com/self-storage-lake-villa-il-86955"

    Set ws = ThisWorkbook.Worksheets("Sheet1")
    Set html = New HTMLDocument
    Set amenitiesDescriptions = GetAmenitiesDescriptions

    With CreateObject("MSXML2.XMLHTTP")
        .Open "GET", URL, False
        .setRequestHeader "User-Agent", "Mozilla/5.0"
        .send
        s = .responseText

        html.body.innerHTML = s

        Dim headers(), results(), listings As Object, amenities As String

        headers = Array("Size", "Description", "RateType", "Price", "Amenities", "Offer1", "Offer2")
        Set listings = html.querySelectorAll(".unitinfo")

        Dim rowCount As Long, numColumns As Long, r As Long, c As Long
        Dim icons As Object, icon As Long, amenitiesInfo(), i As Long, item As Long

        rowCount = listings.Length
        numColumns = UBound(headers) + 1

        ReDim results(1 To rowCount, 1 To numColumns)
        Dim html2 As HTMLDocument
        Set html2 = New HTMLDocument
        For item = 0 To listings.Length - 1
            r = r + 1
            html2.body.innerHTML = listings.item(item).innerHTML
            results(r, 1) = Left$(html2.body.innerHTML, InStr(html2.body.innerHTML, "<") - 1)
            results(r, 2) = Split(Split(html2.querySelector("SCRIPT").innerHTML, """description\"">")(1), "</div>")(0)
            results(r, 3) = Replace$(html2.querySelector(".price_text").innerText, ":", vbNullString)
            results(r, 4) = Trim$(html2.querySelector(".rate_text").innerText)

            Set icons = html2.querySelectorAll(".amenity_icon")
            ReDim amenitiesInfo(0 To icons.Length - 1)

            For icon = 0 To icons.Length - 1 'use class name of amenity to look up description
                amenitiesInfo(icon) = amenitiesDescriptions("." & Replace$(icons.item(icon).className, Chr$(32), "."))
            Next

            amenities = Join$(amenitiesInfo, vbNewLine) 'place each amenity description on a new line within cell when written out

            results(r, 5) = amenities
            results(r, 6) = html2.querySelector(".offer1").innerText
            results(r, 7) = html2.querySelector(".offer2").innerText
        Next

        ws.Cells(1, 1).Resize(1, UBound(headers) + 1) = headers
        ws.Cells(2, 1).Resize(UBound(results, 1), UBound(results, 2)) = results
    End With
End Sub

Public Function GetAmenitiesDescriptions() As Object 'retrieve amenities descriptions from php file on server
    Dim s As String, dict As Object, re As Object, i As Long, arr() 'keys based on classname, short desc, full desc
    ' view regex here: https://regex101.com/r/bII5AL/1
    Set dict = CreateObject("Scripting.Dictionary")
    Set re = CreateObject("vbscript.regexp")

    With CreateObject("MSXML2.XMLHTTP")
        .Open "GET", "https://www.safeandsecureselfstorage.com/core/resources/js/src/common.tooltip.php", False
        .setRequestHeader "User-Agent", "Mozilla/5.0"
        .send
        s = .responseText

        arr = GetMatches(re, s, "(\.amenity_icon\..*)""|<h4>(.*)<\/h4>|<p>(.*)<\/p>")
        For i = LBound(arr) To UBound(arr) Step 3  'build up lookup dictionary for amenities descriptions
            dict(arr(i)) = arr(i + 1) & ": " & arr(i + 2)
        Next
    End With
    Set GetAmenitiesDescriptions = dict
End Function

Public Function GetMatches(ByVal re As Object, inputString As String, ByVal sPattern As String) As Variant
    Dim matches As Object, iMatch As Object, s As String, arrMatches(), i As Long

    With re
        .Global = True
        .MultiLine = True
        .IgnoreCase = False
        .Pattern = sPattern
        If .test(inputString) Then
            Set matches = .Execute(inputString)
            ReDim arrMatches(0 To matches.Count - 1)
            For Each iMatch In matches
                Select Case i Mod 3
                Case 0
                    arrMatches(i) = iMatch.SubMatches.item(0)
                Case 1
                    arrMatches(i) = iMatch.SubMatches.item(1)
                Case 2
                    arrMatches(i) = iMatch.SubMatches.item(2)
                End Select
                i = i + 1
            Next iMatch
        Else
            ReDim arrMatches(0)
            arrMatches(0) = vbNullString
        End If
    End With
    GetMatches = arrMatches
End Function

Вывод:

enter image description here


Ссылки (VBE> Инструменты> Ссылки):

  1. Microsoft HTMLБиблиотека объектов
1 голос
/ 24 апреля 2019

Вот один из способов сделать это:

Sub test()
Dim req As New WinHttpRequest
Dim doc As New HTMLDocument
Dim targetTable As HTMLTable
Dim tableRow As HTMLTableRow
Dim tableCell As HTMLTableCell
Dim element As HTMLDivElement
Dim sht As Worksheet
Dim amenitiesString As String
Dim i As Long
Dim j As Long
Set sht = ThisWorkbook.Worksheets("Sheet1")
With req
    .Open "GET", "https://www.safeandsecureselfstorage.com/self-storage-lake-villa-il-86955", False
    .send
    doc.body.innerHTML = .responseText
End With

Set targetTable = doc.getElementById("units_small_units") 'You can use units_medium_units or units_large_units to get the info from the other tabs
i = 0
For Each tableRow In targetTable.Rows
    i = i + 1
    j = 0
    For Each tableCell In tableRow.Cells
    amenitiesString = ""
    j = j + 1
        If tableCell.className = "amenities" And tableCell.innerText <> "Amenities" Then
            For Each element In tableCell.getElementsByTagName("div")
                amenitiesString = amenitiesString & element.Title & ","
            Next element
            sht.Cells(i, j).Value = amenitiesString
        ElseIf tableCell.className <> "features" Then
            sht.Cells(i, j).Value = tableCell.innerText
        End If
    Next tableCell
Next tableRow

End Sub

Я использую HTTP-запрос вместо Internet Explorer для получения HTML. Кроме того, я думаю, вы можете получить представление о том, как получить доступ к нужным элементам.

Вот скриншот с результатом.

enter image description here

Презентация немного примитивна, но вы поняли: -P

В основном это:

listing.getElementsByClassName("amenities")(0).innerText

вернет пробел, потому что в этих элементах нет внутреннего текста. Информация создается скриптом, но ее также можно найти в элементах title из div.

Использованные ссылки:

Microsoft HTML Object Library и WinHTTP Services Version 5.1

0 голосов
/ 19 апреля 2019

Можете ли вы попробовать подход Jquery, как показано ниже:

$. Get ('url', function (data) {

// Loop through elements
$(data).find("ul").find("li").each( function(){

    var text = $(this).text();

} )

});

...