본문으로 건너뛰기

스토어 스크래핑하기

실제 프로젝트 챕터에서 예시 Warehouse 스토어의 제품 정보를 수집하기 위한 목록을 만들었습니다. 이제 그 데이터에 어떻게 접근할 수 있는지 살펴보겠습니다.

  • URL
  • 제조사
  • SKU
  • 제품명
  • 현재 가격
  • 재고 여부

데이터 스크래핑

URL, 제조사, SKU 스크래핑하기

제품 상세 페이지를 들어가지 않아도 바로 얻을 수 있는 정보들이 있습니다. URL은 이미 가지고 있습니다 - request.url입니다. URL을 자세히 보면 제조사도 추출할 수 있다는 것을 알 수 있습니다(모든 제품 URL이 /products/<manufacturer>로 시작하기 때문입니다). 문자열을 분할하기만 하면 됩니다!

request.loaderUrlrequest.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로 선택자 확인하기

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>이 없으므로 안전하게 사용할 수 있습니다.

제품 SKU 선택자

const sku = await page.locator('span.product-meta__sku-number').textContent();

현재 가격

DevTools를 통해 currentPriceprice 클래스를 가진 <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()에 넣고 마법이 일어나는 것을 보세요!

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

다음 단계

다음으로, 스크래핑한 데이터를 추가 처리를 위해 디스크에 저장하는 방법을 알아보겠습니다.