Изучите простой способ получения данных с веб-сайтов JavaScript

Splash — это служба рендеринга JavaScript, разработанная Scrapinghub, той же компанией, которая разрабатывает популярную платформу Scrapy Scrapy. Это особенно полезно для парсинга веб-страниц, созданных с помощью фреймворков JavaScript, таких как Angular, React, Vue и т. д. Это сложно, если вообще возможно, с традиционными инструментами веб-парсинга, которые загружают только необработанный HTML. Поскольку эти JavaScript-фреймворки со временем становятся все более популярными, парсинг веб-страниц, созданных с помощью JavaScript, будет все чаще использоваться в нашей работе.

На самом деле, Splash — это легкий безголовый веб-браузер, доступ к которому можно получить с помощью HTTP API. Таким образом, он отличается от Selenium, который представляет собой платформу автоматизации браузера, которая позволяет программно управлять веб-браузерами, такими как Firefox, Chrome, Edge и т. д.

Splash проще настроить, чем Selenium, и его можно использовать с различными языками программирования или фреймворками, если поддерживаются HTTP-запросы. В этом посте мы представим базовое использование Splash для парсинга веб-страниц JavaScript с простыми примерами на Python, которые можно использовать для практического парсинга проектов напрямую с небольшими изменениями.

Запустите Splash в контейнере Docker.

Рекомендуемый способ локального запуска Splash-сервера — использовать Docker:

docker run -it -p 8050:8050 --rm scrapinghub/splash:3.5

Если вы предпочитаете использовать файл docker-compose.yaml для простоты обмена и контроля версий, вы можете использовать его в качестве отправной точки:

version: '3.8'

services:
  splash:
    image: scrapinghub/splash:3.5
    ports:
      - target: 8050
        published: 8050
    restart: always

Отметьте этот пост, если хотите узнать больше о Docker и Docker Compose.

Когда контейнер запущен, вы можете перейти по адресу http://localhost:8050, чтобы открыть пользовательский интерфейс Splash локально, где вы можете увидеть краткое введение в Splash вместе с некоторыми удобными скриптами Lua для веб-скрейпинга:

Установите библиотеки Python

Нам нужно использовать библиотеку requests для выполнения HTTP-запросов и библиотеку lxml для очистки отображаемой HTML-разметки с помощью селекторов XPath или CSS.

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

Мы будем использовать conda для создания виртуальной среды. В частности, мы установим последнюю версию Python в виртуальной среде:

(base) $ conda create --name splash python=3.11
(base) $ conda activate splash
(splash) $ pip install -U "requests>=2.30,<2.31" "lxml>=4.9,<4.10"

Очистить веб-страницу JavaScript

В этом посте мы будем использовать http://quotes.toscrape.com/js/ в качестве примера веб-сайта, который сгенерирован кодом JavaScript и поэтому не может быть очищен напрямую.

Давайте сначала создадим функцию, которая может извлекать данные из HTML-кода:

from lxml import html

def scrape_data(html_code):
    tree = html.fromstring(html_code)

    quotes = tree.xpath('//div[@class="quote"]')

    for quote in quotes:
        author = quote.xpath('./span/small[@class="author"]/text()')
        text = quote.xpath('./span[@class="text"]/text()')
        print(f"{author[0]}: {text[0]}")

Прочтите эту публикацию, чтобы узнать больше о простом парсинге веб-страниц с использованием requests и lxml.

Теперь давайте очистим страницу котировок напрямую и посмотрим, сможем ли мы получить данные:

import requests

html_code = requests.get("http://quotes.toscrape.com/js/").text

scrape_data(html_code)

При выполнении приведенного выше кода ничего не очищается, то есть данные не могут быть удалены непосредственно с веб-страницы, созданной с помощью JavaScript.

Затем мы будем использовать Splash для рендеринга веб-страницы JavaScript и посмотрим, можно ли на этот раз очистить данные.

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

import requests
from urllib.parse import quote

def get_rendered_html(url, splash_url="http://localhost:8050", wait=0.5):
    api_url = f"{splash_url}/render.html?url={quote(url)}&wait={wait}"

    response = requests.get(api_url)

    return response.text

В этой функции мы делаем запрос GET на локальный сервер Splash в конечной точке render.html, который отображает веб-страницу и возвращает полученный HTML-код.

Обратите внимание, что процесс рендеринга требует времени, поэтому обычно нам нужно указать аргумент wait для Splash, чтобы страница могла быть полностью визуализирована до ее возврата. Время ожидания является произвольным числом и обычно устанавливается методом проб и ошибок. Позже мы увидим более надежный способ ожидания отрендеренных веб-страниц с использованием скриптов Lua, где веб-страница гарантированно будет отрисована до ее возврата.

Давайте попробуем очистить отрендеренную веб-страницу, возвращенную Splash:

rendered_html_code = get_rendered_html("http://quotes.toscrape.com/js/")
scrape_data(rendered_html_code)

На этот раз данные успешно очищены, что доказывает, что веб-страница JavaScript успешно отображается Splash:

Albert Einstein: “The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”
J.K. Rowling: “It is our choices, Harry, that show what we truly are, far more than our abilities.”
Albert Einstein: “There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.”
....

Используйте сценарии Lua для более эффективного ожидания элементов

С кодом, продемонстрированным выше, мы уже можем начать использовать Splash и некоторые библиотеки Python для очистки простых веб-страниц, созданных с помощью JavaScript. Однако Splash может сделать гораздо больше. У нас могут быть тонко настроенные конфигурации для процесса рендеринга с некоторыми сценариями Lua, которые могут понадобиться в различных случаях.

В приведенном выше примере мы ждем 0,5 секунды, прежде чем будет возвращен ответ. Однако 0,5 секунды — это просто произвольное число, и в большинстве других случаев это не лучший выбор. Если он слишком короткий, веб-страницы не будут отображаться должным образом, и мы не сможем получить HTML-код для парсинга. С другой стороны, если он слишком длинный, это приведет к ненужным задержкам в нашем процессе парсинга и снизит эффективность.

Мы можем использовать сценарии Lua, чтобы проверить, доступен ли определенный элемент HTML, прежде чем будет возвращен ответ.

В этом примере давайте проверим, доступен ли заголовок «Цитаты для очистки», прежде чем будет возвращен ответ.

Поскольку сейчас будет выполняться Lua-скрипт, мы больше не можем использовать конечную точку render.html и вместо этого должны использовать конечную точку execute. Для этого создадим новую функцию для удобства:

import requests
from urllib.parse import quote

def get_rendered_html_by_lua(*, url, splash_url="http://localhost:8050", payload):
    api_url = f"{splash_url}/execute?url={quote(url)}"

    response = requests.post(api_url, json=payload)
    data = response.json()

    return data["html"]

Небольшой трюк с использованием звездочки * в определении функции. Он делает все аргументы аргументами ключевого слова, даже если значения по умолчанию не заданы. Это необходимо здесь, потому что я хочу сохранить последовательность аргументов указанным образом, но не хочу указывать значение по умолчанию для аргумента payload.

Еще одна вещь, которую следует отметить здесь, заключается в том, что мы не должны возвращать HTML-код напрямую с помощью response.text, иначе данные будут в формате JSON и не могут быть правильно очищены, поскольку все кавычки будут экранированы.

Затем нам нужно создать сценарий Lua, чтобы дождаться, пока заголовок станет доступным, прежде чем будет возвращен ответ:

payload = {
    "lua_source": """
    function main(splash, args)
        assert(splash:go(args.url))

        while not splash:select('div.header-box > div > h1') do
            splash:wait(0.1)
            print('waiting...')
        end
        
        return {html=splash:html()}
    end
"""
}

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

Обратите внимание, что splash:select() использует селекторы CSS, а не селекторы XPath.

Давайте теперь воспользуемся новой функцией и скриптом Lua, чтобы снова очистить веб-страницу котировок:

rendered_html_code = get_rendered_html_by_lua(url="http://quotes.toscrape.com/js/", payload=payload)
scrape_data(rendered_html_code)

На этот раз мы также успешно очищаем данные. И что еще более важно, он очищается намного быстрее, чем пример выше. На самом деле нам не нужно ждать 0,5 секунды, прежде чем будет возвращен обработанный ответ.

Используйте скрипты Lua, чтобы нажимать на некоторые элементы

В веб-скрапинге часто нам нужно щелкнуть какой-либо элемент, чтобы отобразить определенный контент. Это можно реализовать с помощью скриптов Lua в Splash.

Нажимаем кнопку «Далее», чтобы очистить содержимое следующей страницы:

payload = {
    "lua_source": """
    function main(splash, args)
        assert(splash:go(args.url))

        while not splash:select('div.header-box > div > h1') do
            splash:wait(0.1)
            print('waiting...')
        end

        local next_button = splash:select('li.next > a')

        if next_button then
            next_button:mouse_click()
        else 
            return {error='Next button not found'}
        end

        splash:wait(0.5)
        
        return {html=splash:html()}
    end
    """
}

Некоторые вопросы, которые следует отметить здесь:

  1. Нам нужно подождать некоторое время до и после нажатия кнопки. Перед тем, как кнопка будет нажата, мы можем дождаться, пока заголовок отобразится. Однако после нажатия следующей кнопки мы не можем снова использовать ту же логику ожидания, потому что заголовок всегда будет отображаться при нажатии следующей кнопки. Это часть контента, которая обновляется. В этом случае нам нужно выждать какое-то «магическое» количество секунд, которое можно точно настроить методом проб и ошибок.
  2. Чтобы избежать возможных ошибок, лучше проверить наличие кнопки или ссылки до того, как вы нажмете на нее.

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

rendered_html_code = get_rendered_html_by_lua(url="http://quotes.toscrape.com/js/", payload=payload)
scrape_data(rendered_html_code)
Marilyn Monroe: “This life is what you make it. No matter what, you're going to mess up sometimes, it's a universal truth. But the good part is you get to decide how you're going to mess it up. Girls will be your friends - they'll act like it anyway. But just remember, some come, some go. The ones that stay with you through everything - they're your true best friends. Don't let go of them. Also remember, sisters make the best friends in the world. As for lovers, well, they'll come and go too. And baby, I hate to say it, most of them - actually pretty much all of them are going to break your heart, but you can't give up because if you give up, you'll never find your soulmate. You'll never find that half who makes you whole and that goes for everything. Just because you fail once, doesn't mean you're gonna fail at everything. Keep trying, hold on, and always, always, always believe in yourself, because if you don't, then who will, sweetie? So keep your head high, keep your chin up, and most importantly, keep smiling, because life's a beautiful thing and there's so much to smile about.”
J.K. Rowling: “It takes a great deal of bravery to stand up to our enemies, but just as much to stand up to our friends.”
Albert Einstein: “If you can't explain it to a six year old, you don't understand it yourself.”
......

Используйте сценарии Lua для отключения изображений и рекламы для более быстрого рендеринга.

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

Отключить изображения очень просто, нам просто нужно добавить следующий код в сценарий Lua перед функцией splash:go():

splash.images_enabled = false

Отключение рекламы немного сложнее. Сначала нам нужно загрузить список правил для блокировки рекламы. Adblock Plus — популярная опция, которую легко настроить.

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

Затем нам нужно остановить существующий контейнер Splash и перезапустить его с правилами EasyList, связанными как том:

docker run -p 8050:8050 \
    -v ./easylist.txt:/etc/splash/filters/easylist.txt \
    scrapinghub/splash

Или используйте Docker Compose:

version: '3.8'

services:
  splash:
    image: scrapinghub/splash:3.5
    ports:
      - target: 8050
        published: 8050
    volumes:
      - type: bind
        source: ./easylist.txt
        target: /etc/splash/filters/easylist.txt
    restart: always

Обратите внимание, что нам не нужно указывать --filters-path в командной строке, потому что он уже добавлен в ENTRYPOINT образа Splash Docker.

Рекомендуется использовать библиотеку pyre2, если используется большое количество правил, иначе рендеринг может быть очень медленным. Итак, давайте также установим pyre2 в нашу виртуальную среду:

(splash) pip install -U "pyre2>=0.3,<0.4"

В Ubuntu вам нужно будет запустить эту команду, чтобы установить некоторые зависимости для pyre2.

sudo apt-get install build-essential cmake ninja-build python3-dev cython3 pybind11-dev libre2-dev

Зависимости для macOS и Windows также можно найти по этой ссылке.

Наконец, мы можем добавить фильтры в наш сценарий Lua:

payload = {
    "lua_source": """
    function main(splash, args)
        -- Disable images.
        splash.images_enabled = false
        -- Disable ads.
        args['filters'] = 'easylist'

        assert(splash:go(args.url))

        -- Change to what you should wait for in your own case.
        while not splash:select('div.header-box > div > h1') do
            splash:wait(0.1)
            print('waiting...')
        end
        
        return {html=splash:html()}
    end
"""
}

Обратите внимание, что Splash предотвращает загрузку изображений только в собственном браузере для более быстрого рендеринга и фактически не удаляет теги <img> в возвращаемом HTML-коде. Поэтому, если вы откроете возвращенный HTML-код в другом браузере, изображения все равно будут отображаться.

Мы можем использовать ту же команду выше, чтобы очистить целевую веб-страницу:

rendered_html_code = get_rendered_html_by_lua(url="http://quotes.toscrape.com/js/", payload=payload)
scrape_data(rendered_html_code)

Вышеупомянутые обновления по-прежнему работают для примера котировок. Вы можете протестировать его на некоторых веб-сайтах, на которых есть изображения и реклама.

В этом посте мы представили основные концепции и распространенные варианты использования Splash. Теперь вы можете настроить сервер Splash локально и использовать его для очистки веб-страниц, созданных с помощью JavaScript. Представлены некоторые расширенные функции с использованием сценариев Lua, в том числе ожидание появления определенного элемента HTML, нажатие кнопки или ссылки HTML, а также отключение изображений и рекламы. Эти функции очень часто используются в практических проектах парсинга.

В следующих постах мы расскажем, как использовать прокси с Splash, как использовать Splash в качестве микросервиса и как интегрировать Splash со Scrapy. Вы можете следовать за мной, если вам интересны эти темы.

Статьи по Теме:

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .