Перейти к основному содержимому

Парсинг магазина

В главе о реальном проекте, вы составили список информации, которую хотели собрать о товарах в примере магазина 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.

Проверка селекторов в DevTools

Помните, что вы можете нажать 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() и смотрим на магию!

Run on
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
}

Следующие шаги

Далее вы узнаете, как сохранить собранные данные на диск для дальнейшей обработки.