이번에 처음 크롤링을 해보면서 해당 데이터를 실제로 이용하는것까지 엄청 좋은 경험을 했습니다.
커머스에서 멤버쉽 제도를 유료로 진행하고 있는데, 이 회원들에게 어떻게하면 좋은 경험을 줄 것인가 ?
우리 서비스를 사용할 것인가? 라는 생각에서 시작했습니다.
결국은 빠른배송(후처리 포함)과 제품의 가격경쟁력이 있어야 한다고 생각했습니다.
그중 제품의 가격경쟁력이 있어야 한다면 어떻게 해당 가격을 알수있을까 ? 크롤링이었습니다.
'puppeteer' 라는 라이브러리를 사용하여 크롤링을 진행하였고, 처음에 excel로 만들어서 슬랙에 공유드렸습니다. 하지만 ..
엑셀이 아닌 구글시트에 바로 업로드한다면 모두가 볼 수있고, 따로 따로 엑셀파일을 열어보지 않아도되니까 구글시트로 업로드한다면 편할 것 이라고 생각했습니다. 그래서 '크롤링 구글시트 업로드'라고 검색해서 찾아봤는데
전부다 파이썬이라더라구요 .. 그래서 JS로 크롤링 하시는분들을 위해 코드를 남깁니다.
페이지 네이션 기준 크롤링
const puppeteer = require('puppeteer');
const { google } = require('googleapis');
const path = require('path');
// 배민 카테고리 설정
const baeminCategories = [
// 가공식품
{ name: '소시지/햄/육가공품', itemGrpNo: 102793, range: "'배민상회'!A1" }, // 구글시트 이름과 반드시 일치해야함 !
];
(async () => {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
const auth = new google.auth.GoogleAuth({
keyFile: path.resolve(''), // 자신의 키 경로 적으셔야합니다.
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
const sheets = google.sheets({ version: 'v4', auth });
const spreadsheetId = ''; // Replace with your Google Sheets ID
for (const category of baeminCategories) {
const allProducts = [];
let pageValue = 0;
let hasNextPage = true;
let lastPage = '';
try {
while (hasNextPage) {
console.log(`Scraping page ${pageValue} for category ${category.name}`);
await page.goto(`https://mart.baemin.com/goods/list/${category.itemGrpNo}?p=${pageValue}&s=RECOMMEND`, {
waitUntil: 'networkidle2',
});
// 총 상품 수 요소가 나타날 때까지 대기
const totalCountSelector = '.sc-hgrVWv.hbIPmZ span';
await page.waitForSelector(totalCountSelector);
// 총 상품 수 가져오기
const totalCountElement = await page.$(totalCountSelector);
const totalCountText = await page.evaluate(el => el.innerText.replace(/,/g, ''), totalCountElement);
const totalCount = parseInt(totalCountText, 10);
try {
// 상품 리스트 요소들이 나타날 때까지 기다림
await page.waitForSelector('.sc-lfIeSx.JBaqi', { timeout: 3000 });
// 상품 데이터 추출
const products = await page.evaluate(() => {
const items = document.querySelectorAll('.sc-lfIeSx.JBaqi');
const data = [];
items.forEach(item => {
const product = {};
const titleElement = item.querySelector('.sc-fZhKUl.eqkrRz');
const priceElement = item.querySelector('.sc-cVLQNM.liVQqU');
product.title = titleElement ? titleElement.innerText.trim() : null;
product.price = priceElement ? priceElement.innerText.trim() : null;
data.push(product);
});
return data;
});
// 추출된 상품 데이터를 전체 상품 배열에 추가
if (products.length === 0) {
hasNextPage = false;
} else {
if (pageValue > 0 && lastPage === '0') {
hasNextPage = false;
} else {
allProducts.push(...products);
pageValue++;
}
}
// 만약 allProducts.length가 totalCount와 같으면 크롤링 종료
if (allProducts.length === totalCount) {
hasNextPage = false;
}
} catch (error) {
console.error(`Error during scraping: ${error}`);
hasNextPage = false; // 에러 발생 시 크롤링 중지
}
}
// 모든 상품 데이터 추출 완료
console.log(`Extracted all products for category ${category.name}:`, allProducts);
// Google Sheets 업데이트를 위한 데이터 준비
const values = [
['카테고리', category.name],
['Title', 'Price'],
...allProducts.map(product => [product.title, product.price])
];
const resource = { values };
// Google Sheets 업데이트
await sheets.spreadsheets.values.update({
spreadsheetId,
range: category.range,
valueInputOption: 'RAW',
resource,
});
console.log(`Updated Google Sheets for category ${category.name}`);
} catch (error) {
console.error(`Error during processing category ${category.name}:`, error);
}
}
// 브라우저 종료
await browser.close();
})();
더보기 버튼누르면 추가 product 생기는경우
(async () => {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
const auth = new google.auth.GoogleAuth({
keyFile: path.resolve(''), // key넣기
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
const sheets = google.sheets({ version: 'v4', auth });
const spreadsheetId = ''; // 시트 id넣기
for (const category of foodSpringCategories) {
try {
console.log(`Scraping category: ${category.name}`);
await page.goto(`https://www.foodspring.co.kr/category/goods_detail/${category.itemGrpNo}`, {
waitUntil: 'networkidle2',
});
// Click the "더보기" button until it's no longer visible
while (true) {
try {
await page.waitForSelector('button > span', { timeout: 5000 }); // Wait for any button with a span child
const buttons = await page.$$('button > span'); // Get all buttons with a span child
// Find the button with span innerText "더보기"
let moreButton;
for (const button of buttons) {
const buttonText = await page.evaluate(el => el.innerText.trim(), button);
if (buttonText === '더보기') {
moreButton = await button.evaluateHandle(el => el.parentElement); // Get the parent button element
break;
}
}
if (moreButton) {
await moreButton.click();
console.log('Button clicked');
await page.waitForSelector('.flex.flex-wrap a div.relative', { timeout: 20000 }); // 대기 시간을 20초로 늘림
} else {
console.log('No more "더보기" button found.');
break; // Break the loop if button not found
}
} catch (error) {
console.log('Error while clicking "더보기" button:', error);
break; // Break the loop on error
}
}
// Extract product information
const products = await page.evaluate(() => {
const items = document.querySelectorAll('.flex.flex-wrap a div.relative');
const data = [];
items.forEach(item => {
const product = {};
const titleElement = item.querySelector('p.break-all');
const priceElement = item.querySelector('.text-red strong.text-base');
product.title = titleElement ? titleElement.innerText.trim() : null;
product.price = priceElement ? priceElement.innerText.trim() : null;
if (product.title && product.price) {
data.push(product);
}
});
return data;
});
console.log(`Extracted all products for category ${category.name}:`, products);
// Prepare data for Google Sheets update
const values = [
['카테고리', category.name],
['Title', 'Price'],
...products.map(product => [product.title, product.price])
];
const resource = {
values,
};
// Update Google Sheets
await sheets.spreadsheets.values.update({
spreadsheetId,
range: category.range,
valueInputOption: 'RAW',
resource,
});
console.log(`Updated Google Sheets for category ${category.name}`);
} catch (error) {
console.error(`Error during processing category ${category.name}:`, error);
}
}
await browser.close();
})();
결과