본문 바로가기

내일배움캠프 TIL

내일 배움 캠프 23_08_18 TIL

어제 웹페이지 크롤링을 통해서 편의점의 데이터들을 가져오는 것을 했었는데

거기서 생긴 사항이 있었어서 그거에 대한 TIL을 작성하려한다

 

const express = require("express");
const puppeteer = require("puppeteer");
const cors = require("cors");
const app = express();
const port = 3001;

app.use(cors());

app.get("/data", async (req, res) => {
  try {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    await page.goto(
    );

    await page.waitForSelector("#dataTable");

    async function scrapeData() {
      const items = await page.evaluate(() => {
        const items = [];

        const nameElements = document.querySelectorAll(
          "#dataTable > div.prodListWrap > ul > li > div > div.prod_wrap > div.prod_text > div.name > p"
        );
        const priceElements = document.querySelectorAll(
          "#dataTable > div.prodListWrap > ul > li > div > div.prod_wrap > div.prod_text > div.price > strong"
        );
        const imgElements = document.querySelectorAll(
          "#dataTable > div.prodListWrap > ul > li > div > div.prod_wrap > div.prod_img > img"
        );

        for (let i = 0; i < nameElements.length; i++) {
          const name = nameElements[i].textContent.trim();
          const price = priceElements[i].textContent.trim();
          const img = imgElements[i].getAttribute("src");
          items.push({ name, price, img });
        }

        return items;
      });

      return items;
    }

    let previousHeight = await page.evaluate("document.body.scrollHeight");

    while (true) {
      await page.evaluate(`window.scrollTo(0, ${previousHeight})`);
      await page.waitForTimeout(3000);
      const newHeight = await page.evaluate("document.body.scrollHeight");

      if (newHeight === previousHeight) {
        break;
      }

      previousHeight = newHeight;

      const loadMoreButton = await page.$(
        "#dataTable > div.prodListBtn > div.prodListBtn-w > a"
      );

      if (loadMoreButton) {
        await loadMoreButton.click();
        await page.waitForTimeout(5000);
      }
    }

    const finalData = await scrapeData();

    await browser.close();

    res.json(finalData);
  } catch (error) {
    console.error("Error:", error);
    res.status(500).json({ error: "An error occurred" });
  }
});

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

어제 CU를 크롤링하던 서버 코드인데 추가된 것을 이야기하자면 Timeout의 시간을 더욱 크게 늘려주었는데

이유는 어제 페이지마다 돌면서 모든 제품들을 크롤링하고 있었는데 어떤 카테고리는 잘 데이터가 들어오는데

어떤 카테고리는 잘 들어오지 않아서 무엇이 문제일까해서 요소들을 다시 찍어보고 여러가지 찾고 있었는데

팀원분이 어떤 문제인지 고민을 하시다가 로딩이 좀 느린것 같다는 의견을 주셔서 보니 정말 다른 카테고리보다 느리길래

그래서 돔요소를 조작할 때 얘가 데이터를 크롤링하는것보다 더 느리게 데이터가 들어오니 그냥 끝나버리는 것 같아서

더욱 대기시간을 늘려주었더니 조금 오래 걸리긴 했지만 데이터를 불러와서 DB에 저장하는데는 성공해내었다
지금은 그냥 명시적으로 시간을 늘려준건데 아마 코드상으로는 더욱 효율적으로 데이터가 패치가 됐을 때 크롤링을 시작하게 바꿀 수 있을 것 같기는하다.

이에 더불어서 페이지마다 요소가 다르고 다음 데이터를 패치해오는 방식이 다르기때문에

const express = require("express");
const puppeteer = require("puppeteer");
const cors = require("cors");
const app = express();
const port = 3001;

app.use(cors());

app.get("/data", async (req, res) => {
  try {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    await page.goto(
    );

    async function scrapeData() {
      const items = [];

      while (true) {
        const nameElements = await page.$$(
          "#contents > div.yCmsComponent.span-24.section1.cms_disp-img_slot > div > div > div > div > div > div.tblwrap.mt20 > div.tab_cont.on > ul > li > div > p.tit"
        );
        const priceElements = await page.$$(
          "#contents > div.yCmsComponent.span-24.section1.cms_disp-img_slot > div > div > div > div > div > div.tblwrap.mt20 > div.tab_cont.on > ul > li > div > p.price > span"
        );
        const imgElements = await page.$$(
          "#contents > div.yCmsComponent.span-24.section1.cms_disp-img_slot > div > div > div > div > div > div.tblwrap.mt20 > div.tab_cont.on > ul > li > div > p.img > img"
        );

        for (let i = 0; i < nameElements.length; i++) {
          const name = await nameElements[i].evaluate((element) =>
            element.textContent.trim()
          );
          const price = await priceElements[i].evaluate((element) =>
            element.textContent.trim()
          );
          const img = await imgElements[i].evaluate((element) =>
            element.getAttribute("src")
          );
          items.push({ name, img, price });
        }

        const nextPageButton = await page.$(
          "#contents > div.yCmsComponent.span-24.section1.cms_disp-img_slot > div > div > div > div > div > div.tblwrap.mt20 > div.paging > span > a.on"
        );

        if (nextPageButton) {
          const pageNumber = await nextPageButton.evaluate((element) =>
            parseInt(element.textContent)
          );

          if (pageNumber === 13) {
            break;
          }
        } else {
          break;
        }

        const loadMoreButton = await page.$(
          "#contents > div.yCmsComponent.span-24.section1.cms_disp-img_slot > div > div > div > div > div > div.tblwrap.mt20 > div.paging > a.next"
        );

        await loadMoreButton.click();
        await page.waitForTimeout(1000);
      }

      return items;
    }

    const finalData = await scrapeData();

    await browser.close();

    res.json(finalData);
  } catch (error) {
    console.error("Error:", error);
    res.status(500).json({ error: "An error occurred" });
  }
});

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

위 코드는 페이지 방식이기에 페이지를 넘어가는 버튼을 클릭하고 넘기면서 하려했는데 이 페이지는

다음페이지 버튼이 비활성화 되거나 없어지는게 아니라 무조건 존재하기에 page의 수를 계산해서

13번째 페이지엑 도달하면 루프를 종료하는 코드를 짜보았고

 

const express = require("express");
const puppeteer = require("puppeteer");
const cors = require("cors");
const app = express();
const port = 3001;

app.use(cors());

app.get("/data", async (req, res) => {
  try {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();


    async function scrapeData() {
      const items = new Set();

      while (true) {
        const nameElements = await page.$$(".dosirak_list_01_02 .name");
        const priceElements = await page.$$(
          ".dosirak_list_01_02 .price > span"
        );
        const imgElements = await page.$$(".dosirak_list_01_02 img");

        for (let i = 0; i < nameElements.length; i++) {
          const name = await nameElements[i].evaluate((element) =>
            element.textContent.trim()
          );
          const price = await priceElements[i].evaluate((element) =>
            element.textContent.trim()
          );
          const relativeImgPath = await imgElements[i].evaluate((element) =>
            element.getAttribute("src")
          );
          const img = `http://www.7-eleven.co.kr${relativeImgPath}`;

         
          items.add(JSON.stringify({ name, img, price }));
        }

        const loadMoreButton = await page.$("#moreImg > a");

        if (!loadMoreButton) {
          break
        }

        await loadMoreButton.click();
        await page.waitForTimeout(1000);
      }

      return Array.from(items).map((item) => JSON.parse(item)); 
    }

    const finalData = await scrapeData();

    await browser.close();

    res.json(finalData);
  } catch (error) {
    console.error("Error:", error);
    res.status(500).json({ error: "An error occurred" });
  }
});

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

요 코드는 또 내가 코드를 잘못 짠 것인지 데이터가 중복되게 들어오고 데이터가 찔끔찔끔 4개씩 늘어나기에

매우 깊은 뎁스가 되어버려서 중복되는 데이터가 많아지길래 중복을 제거해서 데이터를 가져온다음 DB에 저장을 하였다

이런식으로  puppeteer를 이용해서 크롤링을 할 떄에는 페이지를 잘 보고 DOM요소를 조작해서 크롤링을 해야할 것 같다

백그라운드로 실행되는 페이지에서 크롤링을 하는것이기에 눈에 보이는 부분이 없어서 생각보다 시간은 걸렸지만

프로젝트를 진행하기 위해 필요한 데이터의 패칭은 모두 끝이났다 이제 내가 맡은 부분에 대한 개발을 시작해서 빠른 시일내에 끝내고 많은 데이터를 조회하는 이번 프로젝트이기에 어떻게 데이터를 관리해야 더 효율적이고 서버에 부하가 없는

프로젝트가 될 지 고민을 해봐야 할 것 같다 아래에는 오늘도 CS를 공부한 내용을 적어놓겠다.

 

1.CPU 심화
ALU 우뇌(연산력)
CU 좌뇌 (구성력)
레지스터 (오른손)
캐시 (왼손)

레지스터 : 처리할 명령어를 저장한다
캐시 메모리 : 처리속도를 높여주는 역할을 한다

범용 레지스터 : 연산에 필요한 데이터나 연산 결과를 임시로 저장한다
특수목적 레지스터 : 특별한 용도로 사용하는 레지스터이다.
특수목적 레지스터에는 : 메모리 주소 레지스터(MAR) : 읽고 쓰기 위한 주기억장치의 주소를 저장한다.
프로그램 카운터 (PC) : 다음에 수행할 명령어의 주소를 저장
명령어 레지스터 (IR) : 현재 실행 중인 명령어를 저장
메모리 버퍼 레지스터 (MBR) : 주기억장치에서 읽어온 데이터나 저장할 데이터를 임시로 저장
누산기(AC,ACCUMULATOR) : 연산 결과를 임시로 저장
이러한 기능을 하는 레지스터들이 존재한다.
 
제어장치 (CU)

제어부와 내부버스로 나뉘며
제어부는 주기억 장치에 저장되어 있는 명령어를 순서대로 호출하여 해독하고, 제어 신호를 발생시켜
컴퓨터의 각 장치를 동작하도록 하는 장치이다.

내부버스는 기억, 연산, 제어 기능을 실현하기 위한 CPU와 주기억장치 등 제어부 사이를 연결하는 버스이다.

연산장치 (ALU) 덧셈 뺼셈 같은 산술연산과 논리 연산을 계산하는 디지털 회로이다.

*********** 스케줄링  무조건 기억하기

프로그램을 실행해주는 주체 : 프로세스 라고 한다
작업을 처리해주는 주체 : 쓰레드 (쓰레드는 프로세스안에 들어있다)

CPU를 잘 사용하기 위해 프로세스를 잘 배정해야 하며
OS는 실행 대기중인 프로그램 들에게 CPU 자원 배정을 적절히 하여 성능을 끌어올릴 수 있다.

공통 배정조건으로는 오버헤드가 낮을수록 사용률이 높을수록 기아 현상이 낮을수록 좋다

오버헤드 : 프로세스가 필요한 자원보다 더 많이 사용하는것을 말한다
사용률 : 프로세스가 최대한 자원을 많이받고 빨리 처리하도록
기아 현상 : 프로세스가 자원할당을 못받아서 대기하지 않도록하는것

목표에 따른 배정조건 
1.배치 시스템 : 가능하면 많은 일을 수행, 시간보단 처리량이 중요하다
2.대화형 시스템 : 빠른 응답 시간, 적은 대기 시간이 중요
3.실시간 시스템 : 실시간 즉 최소 응답시간이 중요하다.
-----------------------------------------------------------------------------------
스케줄링의 단위
CPU, I/O Burst Cycle

-프로세스 실행은 "CPU실행 <-> 입/출력 대기"의 반복을 의미한다.

-CPU Burst : 프로세스의 사용중에 연속적으로 CPU를 사용하는 구간을 의미한다.
즉, 실제 CPU를 사용하는 스케줄링의 단위이다.

-I/O Burst : 프로세스의 실행 중에 I/O 작업이 끝날떄까지 Block되는 구간을 의미한다.
-----------------------------------------------------------------------------------
스케줄링의 종류

선점 스케줄링 : OS가 CPU의 사용권을 선점할 수 있는 경우, 강제 회수하는 경우 (처리 시간 예측 어려움)
 -가장 자원이 필요한 프로레스에게 CPU를 분배하며 상황에 따라 강제로 회수도 가능하다.
-빠른 응답시간을 요하는 대화식 시분할 시스템에 적합하며 긴급한 프로세스를 제어할 수 있다.

우선순위 스케줄링
-정적/동적으로 우선순위를 부여하여 우선순위가 높은 순서대로 처리
-우선 순위가 낮은 프로세스가 무한정 기다리는 기아 현상 발생 가능
-Aging 방법으로 기아현상 문제 해결 가능
-----------------------------------------------------------------------------------
RR (Round Robin)
FCFS에 의해 프로세스들이 보내지면 각 프로세스는 동일한 시간의 Time Quantum 만큼 CPU를 할당

정해진 시간 할당량 만큼 프로세스를 할당한 뒤, 작업이 끝난 프로세스는 준비완료 큐(순환 큐)의 가장 마지막에 가서 재할당을
기다리는 방법이다.

시간 할당량이 매우 중요한데, 너무 작으면 빈번한 문맥 전환이 발생하고, 너무 길면 FCFS와 다를 바 없어진다.
-----------------------------------------------------------------------------------
Multilevel-Queue(다단계 큐)
준비완료 큐를 여러개의 큐로 분류하여 각 큐가 각각 다른 스케쥴링 알고리즘을 가지는 방식이다
메모리 크기, 우선순위, 유형 등 프로세스의 특성에 따라 하나의 큐에 영구적으로 할당된다. 따라서 큐와 큐 사이에도
스케줄링이 필요하다. 우선순위 방식 혹은 시분할 방식으로 하며
고정 우선순위의 선점형 방식으로 구현되고, 이에따라 우선순위에 따른 큐의 스케줄링은 절대적이다.

-----------------------------------------------------------------------------------
비선점 스케줄링 : 프로세스 종료 or In/Out 등 이벤트가 있을 때까지 실행을 보장한다
-순서대로 처리되는 공정성이 있고, 다음에 처리해야할 프로세스와 상관없이 응답시간을 예상할 수 있다.
-선점방식보다 스케줄러 호출 빈도가 낮고, 문맥교환에 의한 오버헤드가 적다.
-일괄처리 시스템에 적합하며 자칫 CPU 사용시간이 긴 프로세스가 다른 프로세스들을 대기시킬 수 있으므로 처리율이 떨어질 수 있다.

비선점1. FCFS(First Come, First Server)
-큐에 도착한 순서대로 CPU 할당
-실행 시간이 짧은 게 뒤로 가면 평균 대기 시간이 길어짐

비 선점형이며 FIFO큐를 이용하여 간단하게 구현한다
다만 Convoy Effect(호위효과)가 발생하는데, 긴 처리시간의 프로세스가 선점되어버리면 나머지 프로세스들은 
끝날때 까지 대기해야 한다.
따라서 먼저 도착한 프로세스의 버스트 타임에 따라서 평균 대기시간의 편차가 크다.
-----------------------------------------------------------------------------------
SJF(Shorted Job First)
수행시간이 가장 짧은 작업을 먼저 수행하며
FCFS 보다 평균 대기 시간이 감소되고, 짧은 작업에 유리하다

-가정 적은 평균 대기 시간을 달성할 수 있으며
-만약 버스트 시간이 동일하다면 FCFS 방식을 따른다
-다만 선점형인 경우에는 위와같이 진행이 되지만 비 선점형일 경우엔 최소잔여시간우선 법칙을 따른다.
-----------------------------------------------------------------------------------
HRN(Highest Response-ratio Next)
-우선순위를 계산하여 점유 불평등 보완 (SJF의 단점을 보완)
-우선순위 = (대기시간 + 실행시간) / (실행시간)
-----------------------------------------------------------------------------------
스케줄링 동작시점
-프로세스의 상태변화와 스케줄링
 스케줄링 알고리즘에 따라 프로세스들은 상태변화가 일어나며 준비/수행 상태일때 CPU를 사용하게 된다.

 

'내일배움캠프 TIL' 카테고리의 다른 글

내일 배움 캠프 23_08_22 TIL  (0) 2023.08.22
내일 배움 캠프 23_08_21 TIL  (0) 2023.08.21
내일 배움 캠프 23_08_17 TIL  (0) 2023.08.17
내일 배움 캠프 23_08_10 TIL  (0) 2023.08.10
내일 배움 캠프 23_08_08 TIL  (0) 2023.08.08