스토어 스크래핑하기
실제 프로젝트 챕터에서 예시 Warehouse 스토어의 제품 정보를 수집하기 위한 목록을 만들었습니다. 이제 그 데이터에 어떻게 접근할 수 있는지 살펴보겠습니다.
- URL
- 제조사
- SKU
- 제품명
- 현재 가격
- 재고 여부
URL, 제조사, SKU 스크래핑하기
제품 상세 페이지를 들어가지 않아도 바로 얻을 수 있는 정보들이 있습니다. URL
은 이미 가지고 있습니다 - request.url
입니다. URL을 자세히 보면 제조사도 추출할 수 있다는 것을 알 수 있습니다(모든 제품 URL이 /products/<manufacturer>
로 시작하기 때문입니다). 문자열을 분할하기만 하면 됩니다!
request.loaderUrl
과 request.url
의 차이request.loadedUrl
도 사용할 수 있습니다. 차이점을 기억하세요: request.url
은 대기열에 넣는 URL이고, request.loadedUrl
은 (리다이렉트 후) 실제로 처리되는 URL입니다.
// 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-Fu 🥋를 사용하여 제품명을 가져오는 방법을 알아보겠습니다.
제품명
요소 선택 도구를 사용하면 제품명이 <h1>
태그 안에 있는 것을 볼 수 있습니다. <h1>
태그는 product-meta
클래스를 가진 <div>
안에 있습니다. 이를 활용하여 .product-meta h1
이라는 결합 선택자를 만들 수 있습니다. 이는 product-meta
클래스를 가진 요소의 자식인 모든 <h1>
요소를 선택합니다.
DevTools의 Elements 탭에서 CTRL+F(Mac의 경우 CMD+F)를 눌러 검색창을 열고 선택자를 사용하여 요소를 빠르게 검색할 수 있다는 것을 기억하세요. 항상 DevTools를 사용하여 스크래핑 과정과 가정을 확인하세요. 크롤러 코드를 계속 변경하는 것보다 빠릅니다.
제품명을 가져오려면 Playwright
와 .product-meta h1
로케이터를 사용하여 찾아야 합니다. 이는 찾고자 하는 <h1>
요소를 선택하거나, 둘 이상을 찾으면 오류를 발생시킵니다. 이는 좋은 것입니다. 일반적으로 잘못된 데이터를 조용히 반환하는 것보다 크롤러가 중단되는 것이 낫습니다.
const title = await page.locator('.product-meta h1').textContent();
SKU
DevTools를 사용하면 제품 SKU가 product-meta__sku-number
클래스를 가진 <span>
태그 안에 있다는 것을 알 수 있습니다. 페이지에 이 클래스를 가진 다른 <span>
이 없으므로 안전하게 사용할 수 있습니다.
const sku = await page.locator('span.product-meta__sku-number').textContent();
현재 가격
DevTools를 통해 currentPrice
가 price
클래스를 가진 <span>
요소 안에 있다는 것을 알 수 있습니다. 하지만 visually-hidden
클래스를 가진 다른 <span>
요소와 함께 원시 텍스트로 중첩되어 있습니다. 이를 필터링하기 위해 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
span의 올바른 부분(특히 실제 가격)을 찾습니다. 이렇게 하면 Sale price$1,398.00
와 같은 문자열이 나옵니다. 이것만으로는 유용하지 않으므로 $
기호로 분할하여 실제 숫자 부분을 추출합니다.
그러면 가격을 나타내는 문자열이 생기는데, 이를 숫자로 변환할 것입니다. 모든 쉼표를 제거하고(숫자로 파싱할 수 있도록) Number()
를 사용하여 숫자로 파싱합니다.
재고 여부
마지막으로 availableInStock
입니다. product-form__inventory
클래스를 가진 span이 있고, 여기에 '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(`처리 중: ${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: '재고 있음',
})
.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
}
다음 단계
다음으로, 스크래핑한 데이터를 추가 처리를 위해 디스크에 저장하는 방법을 알아보겠습니다.