Javascript crawling googleSheet upload

JungHanMa·2024년 7월 1일
0

Crawling

목록 보기
1/2

이번에 처음 크롤링을 해보면서 해당 데이터를 실제로 이용하는것까지 엄청 좋은 경험을 했습니다.
커머스에서 멤버쉽 제도를 유료로 진행하고 있는데, 이 회원들에게 어떻게하면 좋은 경험을 줄 것인가 ?
우리 서비스를 사용할 것인가? 라는 생각에서 시작했습니다.
결국은 빠른배송(후처리 포함)과 제품의 가격경쟁력이 있어야 한다고 생각했습니다.
그중 제품의 가격경쟁력이 있어야 한다면 어떻게 해당 가격을 알수있을까 ? 크롤링이었습니다.
'puppeteer' 라는 라이브러리를 사용하여 크롤링을 진행하였고, 처음에 excel로 만들어서 슬랙에 공유드렸습니다. 하지만 ..

엑셀이 아닌 구글시트에 바로 업로드한다면 모두가 볼 수있고, 따로 따로 엑셀파일을 열어보지 않아도되니까 구글시트로 업로드한다면 편할 것 이라고 생각했습니다. 그래서 '크롤링 구글시트 업로드'라고 검색해서 찾아봤는데

전부다 파이썬이라더라구요 .. 그래서 JS로 크롤링 하시는분들을 위해 코드를 남깁니다.








  1. npm i googleapis puppeteer
  2. https://console.cloud.google.com/apis/dashboard 접속하여 회원가입 및 api 발급
    해당 방법 해당 블로그 참고하시면 됩니다.
    https://develop-davi-kr.tistory.com/entry/%ED%8C%8C%EC%9D%B4%EC%8D%AC%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EA%B5%AC%EA%B8%80-%EC%8A%A4%ED%94%84%EB%A0%88%EB%93%9C%EC%8B%9C%ED%8A%B8-%EC%97%B0%EB%8F%99-%EB%B0%8F-%EC%9E%90%EB%8F%99%ED%99%94-%EB%B0%A9%EB%B2%95
  3. 구글시트 공유 들어가서 자신이 만든 서비스계정 아이디 시트공유 and 시트이름 range와 일치시키기 !








페이지 네이션 기준 크롤링
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();
})();

결과


profile
Frontend Junior

0개의 댓글