Парсинг магазина
В главе о реальном проекте, вы составили список информации, которую хотели собрать о товарах в примере магазина Warehouse. Давайте рассмотрим эти данные и разберем способы их получения.
- URL
- Производитель
- Артикул
- Название
- Текущая цена
- Наличие на складе
Получение URL, производителя и артикула
Некоторая информация лежит прямо перед нами, даже без необходимости заходить на страницы товаров. URL
у нас уже есть - это request.url
. А внимательно посмотрев на него, мы понимаем, что можем извлечь производителя прямо из URL (так как все ссылки на товары начинаются с /products/<производитель>
). Достаточно разделить строку
и получить нужные данные!
request.loaderUrl
и request.url
Вы также можете использовать request.loadedUrl
. Помните разницу: request.url
- это то, что вы ставите в очередь, а request.loadedUrl
- это то, что обрабатывается (после возможных перенаправлений).
// request.url = https://warehouse-theme-metal.myshopify.com/products/sennheiser-mke-440-professional-stereo-shotgun-microphone-mke-440
const urlPart = request.url.split('/').slice(-1); // ['sennheiser-mke-440-professional-stereo-shotgun-microphone-mke-440']
const manufacturer = urlPart[0].split('-')[0]; // 'sennheiser'
Это дело вкуса - хранить ли эту информацию отдельно в итоговом наборе данных или нет. Пользователи данных могут легко получить производителя
из URL
, так стоит ли дублировать данные? Наше мнение: если увеличение объема данных не критично, лучше сделать набор данных максимально полным. Например, кому-то может понадобиться фильтрация по производителю
.
Обратите внимание, что в названии производителя
может быть символ -
. В таком случае лучше извлекать его со страницы товара, хотя это не обязательно. В конечном итоге вы должны выбрать оптимальное решение для вашего случая и сайта, который парсите.
Теперь добавим больше данных в результаты. Давайте откроем страницу одного из товаров, например Sony XBR-950G
и используем DevTools 🥋 чтобы найти название товара.
Название товара
Используя инструмент выбора элементов, вы увидите, что название находится в теге <h1>
, как и должно быть. Тег <h1>
находится внутри <div>
с классом product-meta
. Мы можем использовать это для создания комбинированного селектора .product-meta h1
. Он выбирает любой элемент <h1>
, который является потомком элемента с классом product-meta
.
Помните, что вы можете нажать CTRL+F (или CMD+F на Mac) во вкладке Elements DevTools, чтобы открыть строку поиска, где можно быстро искать элементы по их селекторам. Всегда проверяйте процесс парсинга и предположения с помощью DevTools. Это быстрее, чем постоянно менять код парсера.
Чтобы получить название, нужно найти его с помощью Playwright
и локатора .product-meta h1
, который выбирает нужный элемент <h1>
или выбрасывает ошибку, если находит больше одного. Это хорошо. Лучше остановить парсер, чем молча вернуть неверные данные.
const title = await page.locator('.product-meta h1').textContent();
Артикул
С помощью DevTools можно найти, что артикул товара находится в теге <span>
с классом product-meta__sku-number
. И поскольку на странице нет других тегов <span>
с этим классом, его можно безопасно использовать.
const sku = await page.locator('span.product-meta__sku-number').textContent();
Текущая цена
DevTools показывает, что текущая цена
находится в элементе <span>
с классом price
. Но также видно, что она вложена как обычный текст вместе с другим элементом <span>
с классом visually-hidden
. Нам это не нужно, поэтому используем помощник hasText
для фильтрации.
const priceElement = page
.locator('span.price')
.filter({
hasText: '$',
})
.first();
const currentPriceString = await priceElement.textContent();
const rawPrice = currentPriceString.split('$')[1];
const price = Number(rawPrice.replaceAll(',', ''));
На первый взгляд может показаться сложным, но давайте разберем по шагам. Сначала находим нужную часть спана price
(конкретно саму цену), фильтруя элемент, содержащий знак $
. После этого получаем строку вида Sale price$1,398.00
. Сама по себе она не очень полезна, поэтому извлекаем числовую часть, разделяя по знаку $
.
После этого у нас есть строка, представляющая цену, но мы преобразуем ее в число. Для этого заменяем все запятые на пустоту (чтобы можно было преобразовать в число), затем преобразуем в число с помощью Number()
.
Наличие на складе
Заканчиваем с проверкой наличия
. Есть спан с классом product-form__inventory
, содержащий текст In stock
. Снова можем использовать помощник hasText
для фильтрации нужного элемента.
const inStockElement = await page
.locator('span.product-form__inventory')
.filter({
hasText: 'In stock',
})
.first();
const inStock = (await inStockElement.count()) > 0;
Здесь важно только наличие элемента, поэтому используем метод count()
для проверки существования элементов, соответствующих селектору. Если они есть, значит товар в наличии.
Вот и все! Все необходимые данные собраны. Для полноты картины соберем все свойства вместе.
const urlPart = request.url.split('/').slice(-1); // ['sennheiser-mke-440-professional-stereo-shotgun-microphone-mke-440']
const manufacturer = urlPart.split('-')[0]; // 'sennheiser'
const title = await page.locator('.product-meta h1').textContent();
const sku = await page.locator('span.product-meta__sku-number').textContent();
const priceElement = page
.locator('span.price')
.filter({
hasText: '$',
})
.first();
const currentPriceString = await priceElement.textContent();
const rawPrice = currentPriceString.split('$')[1];
const price = Number(rawPrice.replaceAll(',', ''));
const inStockElement = await page
.locator('span.product-form__inventory')
.filter({
hasText: 'In stock',
})
.first();
const inStock = (await inStockElement.count()) > 0;
Пробуем на практике
У нас есть все необходимое, так что берем нашу новую логику парсинга, помещаем ее в исходный requestHandler()
и смотрим на магию!
import { PlaywrightCrawler } from 'crawlee';
const crawler = new PlaywrightCrawler({
requestHandler: async ({ page, request, enqueueLinks }) => {
console.log(`Processing: ${request.url}`);
if (request.label === 'DETAIL') {
const urlPart = request.url.split('/').slice(-1); // ['sennheiser-mke-440-professional-stereo-shotgun-microphone-mke-440']
const manufacturer = urlPart[0].split('-')[0]; // 'sennheiser'
const title = await page.locator('.product-meta h1').textContent();
const sku = await page.locator('span.product-meta__sku-number').textContent();
const priceElement = page
.locator('span.price')
.filter({
hasText: '$',
})
.first();
const currentPriceString = await priceElement.textContent();
const rawPrice = currentPriceString.split('$')[1];
const price = Number(rawPrice.replaceAll(',', ''));
const inStockElement = page
.locator('span.product-form__inventory')
.filter({
hasText: 'In stock',
})
.first();
const inStock = (await inStockElement.count()) > 0;
const results = {
url: request.url,
manufacturer,
title,
sku,
currentPrice: price,
availableInStock: inStock,
};
console.log(results);
} else if (request.label === 'CATEGORY') {
// Мы теперь на странице категории. Мы можем использовать это для пролистывания и добавления в очередь всех товаров,
// а также любых последующих страниц, которые мы найдем
await page.waitForSelector('.product-item > a');
await enqueueLinks({
selector: '.product-item > a',
label: 'DETAIL', // <= обратите внимание на другую метку
});
// Теперь нам нужно найти кнопку "Следующая" и добавить следующую страницу результатов (если она существует)
const nextButton = await page.$('a.pagination__next');
if (nextButton) {
await enqueueLinks({
selector: 'a.pagination__next',
label: 'CATEGORY', // <= обратите внимание на ту же метку
});
}
} else {
// Это значит, что мы на начальной странице, без метки.
// На этой странице мы просто хотим добавить в очередь все страницы категорий
await page.waitForSelector('.collection-block-item');
await enqueueLinks({
selector: '.collection-block-item',
label: 'CATEGORY',
});
}
},
// Давайте ограничим наши краулинг, чтобы наши тесты были короче и безопаснее.
maxRequestsPerCrawl: 50,
});
await crawler.run(['https://warehouse-theme-metal.myshopify.com/collections']);
При запуске парсера вы увидите обработанные URL и собранные данные в консоли. Вывод будет выглядеть примерно так:
{
"url": "https://warehouse-theme-metal.myshopify.com/products/sony-str-za810es-7-2-channel-hi-res-wi-fi-network-av-receiver",
"manufacturer": "sony",
"title": "Sony STR-ZA810ES 7.2-Ch Hi-Res Wi-Fi Network A/V Receiver",
"sku": "SON-692802-STR-DE",
"currentPrice": 698,
"availableInStock": true
}
Следующие шаги
Далее вы узнаете, как сохранить собранные данные на диск для дальнейшей обработки.