CoderCastrov logo
CoderCastrov
Питон

Парсинг веб-страниц: Перехват запросов XHR

Парсинг веб-страниц: Перехват запросов XHR
просмотров
5 мин чтение
#Питон

Вы когда-нибудь пытались парсить AJAX-сайты? Сайты, полные JavaScript и запросов XHR? Расшифровывать тонны вложенных CSS-селекторов? Или, что еще хуже, селекторы, которые меняются каждый день? Возможно, вам больше никогда не понадобится это делать. Продолжайте читать, парсинг XHR может стать вашим идеальным решением!

Предварительные требования

Для работы кода вам понадобится установленный python3. Некоторые системы уже имеют его предустановленным. После этого установите Playwright и бинарные файлы браузеров Chromium, Firefox и WebKit.

pip install playwright
playwright install

Ответы перехвата

Как мы видели в предыдущем блоге о блокировке ресурсов, безголовые браузеры позволяют перехватывать запросы и ответы. Мы будем использовать Playwright в Python для демонстрации, но это можно сделать и на JavaScript или с использованием Puppeteer.

Мы можем быстро проверить все ответы на странице. Как видно ниже, параметр response содержит статус, URL и само содержимое. Именно это мы будем использовать вместо прямого парсинга содержимого в HTML с помощью CSS-селекторов.

page.on("response", lambda response: print( 
	"<<", response.status, response.url))

Пример использования: auction.com

Наш первый пример будет использовать auction.com. Возможно, вам понадобятся прокси или VPN, так как сайт блокирует доступ из стран, в которых не работает. В любом случае, может возникнуть проблема с парсингом с вашего IP-адреса, так как его могут заблокировать. Если у вас возникнут проблемы, ознакомьтесь с инструкцией по избежанию блокировки.

Вот простой пример загрузки страницы с использованием Playwright и записью всех ответов:

from playwright.sync_api import sync_playwright

url = "https://www.auction.com/residential/ca/"

with sync_playwright() as p:
    browser = p.firefox.launch()
    page = browser.new_page()

    page.on("response", lambda response: print(
        "<<", response.status, response.url))
    page.goto(url, wait_until="networkidle", timeout=90000)

    print(page.content())

    page.context.close()
    browser.close()

auction.com загрузит оболочку HTML без интересующего нас содержимого (цены на дома или даты аукционов). Затем он загрузит несколько ресурсов, таких как изображения, CSS, шрифты и JavaScript. Если мы хотим сэкономить трафик, мы можем отфильтровать некоторые из них. На данный момент мы сосредоточимся на интересующих нас частях.

Как мы видим во вкладке "Сеть", почти все интересующее нас содержимое поступает из вызова XHR к конечной точке "assets". Игнорируя остальное, мы можем изучить этот вызов, проверив, содержит ли URL ответа строку: if ("v1/search/assets?" in response.url).

Возникают проблемы с размером и временем: страница будет загружать трекинг и карту, что займет более минуты при использовании прокси и 130 запросов 😮. Мы могли бы улучшить это, блокируя определенные домены и ресурсы. В наших тестах мы смогли сделать это за менее чем 20 секунд с загрузкой всего 7 ресурсов. Мы оставим это вам в качестве упражнения 😉

<< 407 https:_//www.auction.com/residential/ca/_
<< 200 https:_//www.auction.com/residential/ca/_
<< 200 https:_//cdn.auction.com/residential/page-assets/styles.d5079a39f6.prod.css_
<< 200 https:_//cdn.auction.com/residential/page-assets/framework.b3b944740c.prod.js_
<< 200 https:_//cdn.cookielaw.org/scripttemplates/otSDKStub.js_
<< 200 https:_//static.hotjar.com/c/hotjar-45084.js?sv=5_
<< 200 https:_//adc-tenbox-prod.imgix.net/resi/propertyImages/no_image_available.v1.jpg_
<< 200 https:_//cdn.mlhdocs.com/rcp_files/auctions/E-19200/photos/thumbnails/2985798-1-G_bigThumb.jpg_
_# ..._

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

При изучении результатов мы видим, что оболочка присутствует с момента загрузки. Но содержимое каждого дома отсутствует. Поэтому мы будем ждать появления одного из них: "h4[data-elm-id]".

with sync_playwright() as p:
    def handle_response(response):
        # интересующая нас конечная точка
        if ("v1/search/assets?" in response.url): 
            print(response.json()["result"]["assets"]["asset"])
    # ...
    page.on("response", handle_response)
    # очень долгий таймаут, так как иногда он застревает
    page.goto(url, timeout=120000)
    page.wait_for_selector("h4[data-elm-id]", timeout=120000)

Вот вывод, с еще большим количеством информации, чем предлагает интерфейс! Все чисто и аккуратно отформатировано 😎

[{
    "item_id": "E192003",
    "global_property_id": 2981226,
    "property_id": 5444765,
    "property_address": "13841 COBBLESTONE CT",
    "property_city": "FONTANA",
    "property_county": "San Bernardino",
    "property_state": "CA",
    "property_zip": "92335",
    "property_type": "SFR",
    "seller_code": "FSH",
    "beds": 4,
    "baths": 3,
    "sqft": 1704,
    "lot_size": 0.2,
    "latitude": 34.10391,
    "longitude": -117.50212,
    ...

Мы могли бы пойти дальше и использовать пагинацию, чтобы получить весь список, но оставим это вам.

Сценарий использования: twitter.com

Еще один типичный случай, когда изначально нет контента, - это Twitter. Чтобы иметь возможность парсить Twitter, вам необходимо использовать рендеринг JavaScript. Как и в предыдущем случае, после полной загрузки контента вы можете использовать CSS-селекторы. Однако будьте осторожны, так как классы Twitter являются динамическими и часто меняются.

То, что, скорее всего, останется неизменным, это внутренняя точка доступа API, которую они используют для получения основного контента: TweetDetail. В таких случаях самый простой способ - проверить вызовы XHR во вкладке "Сеть" в инструментах разработчика и найти какой-либо контент в каждом запросе. Это отличный пример, потому что Twitter может делать от 20 до 30 JSON- или XHR-запросов на каждое просмотренное странице.

Снимок экрана Twitter.com с открытыми инструментами разработчика

После того, как мы определили вызовы и ответы, которые нас интересуют, процесс будет похожим.

import json
from playwright.sync_api import sync_playwright
 
url = "https://twitter.com/playwrightweb/status/1396888644019884033"
 
with sync_playwright() as p:
	def handle_response(response):
		_# точка доступа, которая нас интересует_
		if ("/TweetDetail?" in response.url):
			print(json.dumps(response.json()))

	browser = p.firefox.launch()
	page = browser.new_page()
	page.on("response", handle_response)
	page.goto(url, wait_until="networkidle")
	page.context.close()
	browser.close()

Вывод будет значительным JSON-файлом (80 КБ) с большим количеством контента, чем мы просили. Более десяти вложенных структур, пока мы не дойдем до содержимого твита. Хорошая новость в том, что теперь мы можем получить доступ к количеству избранных, ретвитов или ответов, изображениям, датам, ответным твитам с их содержимым и многому другому.

Использование: nseindia.com

Фондовые рынки - это постоянно изменяющийся источник важных данных. Некоторые сайты, предлагающие эту информацию, такие как National Stock Exchange of India, начинают с пустой оболочки. После нескольких минут просмотра сайта мы видим, что данные рынка загружаются через XHR.

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

Снимок экрана NSE с открытыми инструментами разработчика

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

from playwright.sync_api import sync_playwright

url = "https://www.nseindia.com/market-data/live-equity-market"

with sync_playwright() as p:
	def handle_response(response):
		_# интересующий нас эндпоинт_
		if ("equity-stockIndices?" in response.url):
			items = response.json()["data"]
			[print(item["symbol"], item["lastPrice"]) for item in items]

	browser = p.firefox.launch()
	page = browser.new_page()

	page.on("response", handle_response)
	page.goto(url, wait_until="networkidle")

	page.context.close()
	browser.close()

_# Вывод:_
_# NIFTY 50 18125.4_
_# ICICIBANK 846.75_
_# AXISBANK 845_
_# ..._

Заключение

Мы хотим выделить три основных момента:

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

  2. Помимо XHR-запросов, существует множество других способов парсинга данных, выходящих за рамки селекторов. Не все из них будут работать на данном веб-сайте, но добавление их в свой инструментарий может часто помочь.

  3. Спасибо за чтение. Помог ли вам контент? Пожалуйста, поделитесь им. 👇


Оригинальная публикация на веб-сайте https://www.zenrows.com