728x90
반응형

이전 글에서 간략하게 프록시가 무엇이며

프록시를 활용해서 IP를 변경하는 방법을 알아보았습니다

 

더 자세한 정보를 알고 싶으시다면 아래의 링크를 클릭하시길 바랍니다.

 

 

2022.09.02 - [IT_Web/Nodejs] - 웹 크롤링 puppeteer 프록시 IP 적용하기 그런데 대체 프록시 란? 뭘까?

 

웹 크롤링 puppeteer 프록시 IP 적용하기 그런데 대체 프록시 란? 뭘까?

이전 글 웹 크롤링 마우스 조작하기에 대해서 태그를 도저히 못 찾겠다면 마우스를 조작하여 직접 마우스 클릭하는 방법을 구현해서 웹크롤링을 합니다 하지만 마우스 조작하는 방법은 대도록

tantangerine.tistory.com

 

 

 

웹 크롤링은 정보를 추출하여 파일을 만들어 업무 효율높는데 목적이 있습니다

하지만 그 정보를 DB에 보관한다면 중복검사와 데이터 관리에 있어 보다 더 효율적으로

관리가 가능합니다

 

그래서 DB 연동은 웹크롤링을 보다 편하게 하기 위해서는 필수 조건이라고 생각합니다

 

그럼 DB연동 방법을 알아보도록 하겠습니다

 

mysql 설치방법은 다른 여러 블로그에 나와있습니다

그러니 생략하고 sequelize 라이브러리를 설치하는 방법을 알아보겠습니다

 

sequelize 라이브러리 설치하기

아래의 코드를 실행시켜 라이브러리를 설치합니다

 

npm i -g sequelize-cli
npm i --save sequelize
sequelize init
npm i mysql2

 

 

4개의 라이브러리를 설치하고 나면 디렉터리에 새로운 폴더가 추가가 되어있을 것입니다

 

config 폴더와 models폴더가 추가됩니다 

 

 

웹크롤링-DB연동
웹크롤링 DB연동

 

 

DB 연결 함수 및 config.json 설정값 변경하기

 

 

models의 index파일은 DB를 연결하는 설정 함수들이 포함되어있습니다

대문자 sequelize와 Sequelize가 다르니 꼭 확인 하시길바랍니다

 

 

웹크롤링-DB연동2
웹크롤링 연동 설정함수

 

 

config.jsonDB 연결을 위한

host주소 및 DB 패스워드, username 등이 정의되어있습니다

 

그래서 위의 이미지와 같이 사용하는 코드들이 작성되어있습니다

 

아래는 DB config파일입니다

 

웹크롤링-config
웹크롤링 config

 

이제 테이블을 만들어야 합니다

하지만 sequelize를 활용해서 models 폴더에 파일을 만들면 

테이블을 만들 수 있습니다

 

 

위의 디렉터리 구조에서 proxy.js가 보일 것입니다

그 파일을 보면 아래와 같이 칼럼이 정의되어있습니다

null값을 허용할지와 컬럼이 저장될 자료형이 어떤 것 인지도 정할 수 돼있으며

테이블마다 id는 자동부여됩니다

 

 

웹크롤링-proxy테이블
웹크롤링 proxy테이블

 

 

DB 조회 문 함수

sequelizeORM방식이라서 sql을 몰라도 DB를 다를 수 있지만 

DB를 조금 더 자세하게 알고 싶다면 sql을 필히 공부해야 합니다

너무 sequelize 믿지 마시고 sql을 조금씩 공부를 하시길 바랍니다

 

그럼 데이터를 조회해보는 

몇 가지 함수를 찾아보았습니다

한번 확인해보시고 더 많은 조회 문을 찾아보시길 바랍니다

 

 

 

await db.sequelize.sync(); //db를 연결하는 함수입니다

await Promise.all(fristProxies.map(async (v) => { // 테이블 insert문
  return await db.Proxy.create({ // Proxy파일이 module.export되어 데이터를 가져온다
    ip: v.ip,
    type: v.type,
    latency: v.latency,
    anonymity: anonymity.toString() //''만으로 문자열이 구성되면 에러가 발생한다
  });								// ""을 꼭 확인해야합니다
}))

const fastestProxy = await db.Proxy.findOne({ // 한개의 행을 가져오기
  order: [['latency', 'ASC']]
});

 

 

이것으로 DB 연동은 끝났습니다

간단하게 DB 연동을 할 수 있으니 꼭 사이드 프로젝트를 만들면서

자기의 역량을 키울 수 있는 계기가 되었으면 합니다

 

이것으로 웹크롤링 준비단계는 모두 끝이 났습니다

아래는 제일 처음 웹크롤링 포스팅입니다

관심 있으신 분들은 한번 읽어보시길 바랍니다

아니면 카테고리를 클릭해서 전체 글을 보셔도 되겠지요?

 

 

2022.08.17 - [IT_Web/Nodejs] - node.js로 엑셀 및 CSV 파일 데이터 간단하게 가져오기

 

node.js로 엑셀 및 CSV 파일 데이터 간단하게 가져오기

node.js 관련은 처음 소개하는 것 같습니다 다음에 node.js로 서버 구축하는 것도 알아볼 테니 많은 관심 부탁드립니다 우선 node 다운로드 링크 주소는 사이드바에 생성해 놓을 테니 확인해 보시길

tantangerine.tistory.com

 

 

그럼 다음 포스팅도 기대해주시길 바랍니다

 

우리의 길고 긴 IT 대모험은 항상 밝기만을 기원하겠습니다

그럼 다음 포스팅에서 만나요~ 

728x90
반응형
728x90
반응형

이전 글 웹 크롤링 마우스 조작하기에 대해서

 

태그를 도저히 못 찾겠다면 마우스를 조작하여

직접 마우스 클릭하는 방법을 구현해서 웹크롤링을 합니다

하지만 마우스 조작하는 방법은 대도록이면 안 사용하는 것이 좋겠지요?

 

더 상세한 정보가 궁금하시다면 아래의 링크를 클릭해주세요

 

 

2022.09.01 - [IT_Web/Nodejs] - 웹 크롤링 마우스 조작해서 클릭하기 태그를 도저히 못 찾겠다면!?

 

웹 크롤링 마우스 조작해서 클릭하기 태그를 도저히 못찾겠다면!?

이전 포스팅 얼럿창 컨트롤하기에 대하여 웹 크롤링 중 갑자기 얼럿창이 노출될 경우 닫지 않고서는 다음 진행이 어렵습니다 그래서 puppeteer는 이벤트 리스너와 같은 기능을 하는 함수를 가지고

tantangerine.tistory.com

 

 

프록시 란 컴퓨터 네트워크에서 다른 서버 상의 자원을 찾는

클라이언트로부터 요청을 받아 중계하는 서버를 말합니다

사용자의 입장에서는 자신의 웹 서핑 기록을 익명 화하기 위해 웹 프록시를 사용하기도 합니다

 

이러한 프록시 서버를 이용하여 우리의 IP를 다른 새로운 IP로 변경을 하여 웹크롤링을 시도합니다

그렇게 되면 익명의 유저로 또 다시 접근이 가능하게 됩니다

 

 

웹 프록시에서 새로운 IP 크롤링하여 웹 페이지 접속하기

 

setNewProxy 함수를 활용해서 다른 IP를 받는 작업을 합니다

 

먼저 나의 IP가 어떤 값으로 설정되어있는지

Daum 홈페이지 검색창 input에 value값을 할당하여

변경해가는 IP주소가 변경되는 과정을 볼 수 있을 것입니다

 

 

const puppeteer = require('puppeteer');
const writekeyboardList = require('./writekeyboardList');
const dotenv = require('dotenv'); // npm i dotenv
dotenv.config();

const setCrawler = async (production, isMouse = false) => {
  let browser = await puppeteer.launch({
    headless: false,
    args:['--window-size=1920,1080']
  });
  let page = await browser.newPage();
  await page.setViewport({
    width: 1080,
    height:1080,
  })
  
  const setNewProxy = async(crawlerUrl) => {
    const newProxies = await page.evaluate(async() => {
      const ips = Array.from(document.querySelectorAll('tr > td:first-of-type > .spy14')).map((v) => v.innerText)
      const types = Array.from(document.querySelectorAll('tr > td:nth-of-type(2)')).map((v) => v.innerText)
      const anonymity = Array.from(document.querySelectorAll('tr > td:nth-of-type(3)')).map((v) => v.innerText)
      const latencies = Array.from(document.querySelectorAll('tr > td:nth-of-type(6) > .spy1')).map((v) => v.innerText)
      return ips.map((v, i) => {
        return {
          ip: v,
          type: types[i],
          latency: latencies[i],
          anonymity: anonymity[i]
        }
      });
    });
    const fristProxies =  newProxies.filter((v) => v.type === 'HTTP' && v.anonymity === 'HIA').sort((p, c) => p.latency - c.latency)[0]
    await page.close();
    await browser.close();
    browser = await puppeteer.launch({
      headless: false,
      args:['--window-size=1920,1080', '--disable-notifications', `--proxy-server=${fristProxies.ip}`]
    });

    page = await browser.newPage();
    await page.goto(crawlerUrl);
    await page.waitForSelector('.tf_keyword')
    await inputValue('.tf_keyword', '내아이피'),
    await Promise.all([
      buttonClick(['.ico_pctop.btn_search', 'ico_ksearch.btn_ksearch']),
      page.waitForNavigation(),
    ]);
  }

  const handleWriteKeyboard = async (selector, id) => {
    const writeList = writekeyboardList()
    const idOfList = [...id]
    if(selector){
      await page.click(selector);
      console.log(`idOfList`, idOfList)
      for (const id of idOfList) {
        const inputValue = await writeList.filter(element => element.ShiftLeft === id || element.key === id )
        if (inputValue.length > 0) {
          const code = inputValue[0].code
          const ShiftLeft = inputValue[0].ShiftLeft === id ? 'ShiftLeft' : ''
          if (ShiftLeft) {
            await page.keyboard.down(ShiftLeft);
            await setTime(selector, 500)
            await page.keyboard.press(code);
            await setTime(selector,500)
            await page.keyboard.up(ShiftLeft);
          } else {
            await page.keyboard.press(code);
          }
          await setTime(selector,500)
        }
      }
    } else {
      await page.keyboard.press(id);
    }
  }

  const inputValue = async (selector, value) => {
    await page.evaluate((selector, value) => {
      document.querySelector(selector).value = value
    }, selector, value)
    await setTime(selector)
  }

  const buttonClick = async (selector, nextSelector, type) => {
    await page.evaluate((selector) => {
      if (document.querySelector(selector[0])) {
        document.querySelector(selector[0]).click();
      } else {
        document.querySelector(selector[1]).click();
      }
    }, selector)
    if(type === 'login'){
      // waitForRequest(요청 대기), waitForResponse(응답 대기)
      const res = await page.waitForResponse(res => {
        return res.url().includes('graphql')
      })
      const result = await res.json()
      console.log('login', result);
      if(await result.extensions.is_final){
        await setTime(type, 2000)
        await page.keyboard.press('Escape')
      }
    } else if(type === 'logOut'){
      await page.waitForNavigation()
    } else {
      if(nextSelector){
        await page.waitForSelector(nextSelector)
      }
      if(type === 'wait') await setTime(selector)
    }
  }

  const setTime = async (selector = 'normal', waitTime = 0) => {
    const setTime = await page.evaluate(async(waitTime) => {
      const timeList = await [3975, 4547, 5231, 6397, 7894, 3394, 4206, 5147, 6932, 7430, 3561, 4896, 5877, 6407, 7133];
      let randomTime = await timeList[Math.floor(Math.random() * timeList.length)]
      if (waitTime > 0) randomTime = waitTime
      await new Promise(r => setTimeout(r, randomTime))
      return randomTime
    }, waitTime)
    console.log(`waitTime[${selector}]: `, setTime)
    return setTime
  } 

  await page.goto('https://www.daum.net/');
  await page.waitForSelector('.tf_keyword')
  await inputValue('.tf_keyword', '내아이피'),
  await Promise.all([
    buttonClick(['.ico_pctop.btn_search']) ,
    page.waitForNavigation(),
  ]);
  
  await page.goto('http://spys.one/free-proxy-list/KR/');


  return { buttonClick, inputValue, handleWriteKeyboard, setTime, setNewProxy, page, browser }
}


const crawler = async () => {
  try {
    const { buttonClick, inputValue, handleWriteKeyboard, setTime, setNewProxy, page, browser } = await setCrawler(JSON.parse(process.env.PRODUCTION));
    const id = await process.env.EMAIL;
    const password = await process.env.PASSWORD;

    await setNewProxy('https://www.daum.net/')

    // await page.close();
    // await browser.close();

  } catch (e) {
    console.log(e);
  }
}

crawler()

 

 

위의 코드가 상당히 길어서 조금 부담될 수 있지만

 

이전 포스팅에서 알아보았던 것들을 재사용한 것이니

궁금하시면 이전 포스팅이나 카테고리를 클릭하시길 바랍니다

 

아래의 코드는 실질적인 크롤링 실행문이 있습니다

단지 setCrawler()가 실행되어 매서드 및 화면 노출을 하는 초기 세팅값을 설정합니다

 

Daum 홈페이지에서 검색창에 내 아이피를 인자 값으로 넣어

value를 할당하고

검색 버튼을 클릭해서 검색을 하여 IP주소를 확인합니다

 

그리고 setNewProxy()호출해서 다른 IP주소를 할당합니다

 

그럼 아래에서 setNewProxy함수에 대해 알아보도록 하겠습니다

 

 

웹크롤링-시작부
웹크롤링 시작부

 

 

 

 

우선 코딩 분석 전에 페이지의 형태를 확인해 봅시다

IP주소 그리고 proxy type, Anonymity, Latency가 보입니다

 

Proxy address는 IP주소이며,

Proxy type 우리가 흔히 알고 있는 http 관련 사항입니다

Anonymity는 ANM, HIA, NOA 이 있으며 이것은 익명성을 보장하는 정도라고 생각하시면 됩니다

HIA가 제일 높은 수치로 익명성을 보장받을 수 있습니다

Latency는 속도를 의미하며 수치가 낮을수록 빠르다고 할 수 있습니다

 

그럼 4가지의 값들을 가져오기 위해 웹크롤링을 진행합니다

 

 

웹크롤링-프록시
웹크롤링 프록시 페이지

 

 

 

4가지 타입의 태그에 접근하여 innerText 값을 가져옵니다

그것을 배열로 만들어서 newProxies 변수에 담아서 filter함수를 적용해서 제일 빠른 IP주소를 가져옵니다

가져온 IP주소를 변수에 담으면 기존 페이지와 브라우저를 닫고

새 브라우저와 새 페이지를 프록시 서버 설정하여 활성화합니다 

 

이때 Daum 홈페이지에 다시 한번 내 아이피를 확인하면

변경된 IP주소를 확인할 수 있습니다

 

그리고 페이지 중간중간 이동할 경우 waitForSelector()을 사용해서 새로운 페이지의 지정된 태그를 기다리게 됩니다

그리고 buttonClick함수가 실행되고 페이지가 이동된다면

Promise.all()을 사용하여 순차적으로 넣어 줍니다

 

* inputValue, buttonClick 함수의 설명은 이전 포스팅을 참고하세요 

 

 

웹크롤링-함수부
웹크롤링 함수부

 

 

이렇게 설정하면 프록시 IP주소를 가져올 수 있습니다

 

한번 직접 코딩을 해서 웹크롤링을 돌려보는 것이 더욱 이해가 빠르다고 생각합니다

그럼 오늘도 이렇게 공부를 하였습니다

 

IT 개발이란 것은 영원히 공부를 해야 하지 않을까? 합니다

새로운 언어들이 나오고 웹의 새로운 환경들을 나오면서

더욱 우리는 공부를 해야 할 듯합니다

 

아직 IT 대모험이 끝나지 않았으니까

앞으로도 힘내시길 바랍니다

 

그럼 다음 포스팅도 기대해주세요

 

 

 

 

 

 

 

728x90
반응형
728x90
반응형

이전 포스팅 얼럿창 컨트롤하기에 대하여

 

웹 크롤링 중 갑자기 얼럿창이 노출될 경우 닫지 않고서는

다음 진행이 어렵습니다

그래서 puppeteer이벤트 리스너와 같은 기능을 하는 함수를 가지고 있습니다 

그 함수는  page.on 이며, 여러 인자 값을 받아 여러가지 이벤트를 핸들링할 수 있습니다

 

더 상세한 정보를 알고 싶으시면 아래의 글을 참고해주시길 바랍니다

 

 

2022.08.31 - [IT_Web/Nodejs] - 웹 크롤링 puppeteer로 alert, prompt, confirm 창 얼럿창 제어 및 컨트롤 하기

 

웹 크롤링 puppeteer로 alert, prompt, confirm 창 얼럿창 제어 및 컨트롤 하기

이전 포스팅 키보드 코드 입력하기에 대해서 puppeteer로 웹 크롤링할 때 키보드를 입력하는 방법을 알아보았습니다 키보드의 자판만큼이나 코드도 엄청 많았습니다 그래서 그 문서를 Array형 Object

tantangerine.tistory.com

 

 

웹 크롤링 puppeteer 활용해서  마우스 조작하기

 

웹 크롤링에서 마우스를 조작하는 이유는 무엇일까요?

간단한 매크로를 만들거나, 태그를 정말 찾기 어려울 때 어쩔 수 없이 사용하게 될 것입니다

마우스를 조작해야 될 경우가 없는 게 좋겠지만 해야 할 경우가 생길 수 있으니

알아두면 편하겠죠?

 

그럼 코드를 보면서 그 방법을 알아보도록 하겠습니다

 

 

const puppeteer = require('puppeteer');
const callElement = require('./mouseHandleHtml');
const dotenv = require('dotenv'); // npm i dotenv
dotenv.config();

const setCrawler = async (production) => {
  const browser = await puppeteer.launch({
    headless: production,
    args:['--window-size=1920,1080']
  });
  const page = await browser.newPage();
  await page.setViewport({
    width: 1080,
    height:1080,
  })
  await page.goto('https://facebook.com');

  if (!production) {
    await callElement(page)
  }

  const setTime = async (selector = 'normal', waitTime = 0) => {
    const setTime = await page.evaluate(async(waitTime) => {
      const timeList = await [3975, 4547, 5231, 6397, 7894, 3394, 4206, 5147, 6932, 7430, 3561, 4896, 5877, 6407, 7133];
      let randomTime = await timeList[Math.floor(Math.random() * timeList.length)]
      if (waitTime > 0) randomTime = waitTime
      await new Promise(r => setTimeout(r, randomTime))
      return randomTime
    }, waitTime)
    console.log(`waitTime[${selector}]: `, setTime)
    return setTime
  } 

  return { setTime, page: page, browser: browser }
}


const crawler = async () => {
  try {
    const production = await process.env.PRODUCTION;
    console.log('production', production, typeof(JSON.parse(production)))
    const { setTime, page, browser } = await setCrawler(JSON.parse(production));

    await setTime('', 2000)
    await page.mouse.move(100, 100);
    await page.mouse.down()
    await setTime('', 2000)
    await page.mouse.move(1000, 400);
    await setTime('', 2000)
    await page.mouse.click(1000, 400);
    await setTime('', 2000)

  } catch (e) {
    console.log(e);
  }
}

crawler()

 

env파일이 아래와 같이 설정되어있다고 합시다

PRODUCTION이 true 될 경우 실 운영 버전으로 개발이 다 끝난 경우에 설정하는 값이라고 합시다

지금은 false로 개발을 진행한다고 설정하고 웹 크롤링 과정을 지켜보며 개발을 하게 됩니다

 

 

웹크롤링-env
웹 크롤링 env 파일

 

production 변수에 envPRODUCTION을 할당합니다

그 설정된 값을 자료형을 boolean으로 변경하고

setCrawler() 함수에 매개변수 인자 값으로 production을 할당합니다

 

 

웹크롤링-env관리
웹 크롤링 env 관리하기

 

빨간 테두리에는 할당된 인자 값인 production을 headles를 false로 할당해서

웹 크롤링하는 과정을 화면으로 지켜볼 수 있게 false로 지정합니다

headless 머리가 없다는 뜻으로 false로 머리가 있다는 뜻이 됩니다

그럼 화면을 볼 수 있다는 것이겠지요?

 

 

 

 

위의 노랑 테두리는 

production이 false이면 if문을 안을 실행하게 됩니다

그래서 callElement()에 page 객체를 인자 값으로 호출하게 됩니다

즉, 운영이 아닌 환경설정값으로 마우스를 노출시킬 수 있는 Element를 생성하는 함수를 호출하게 됩니다

운영이라면 if문을 실행하지 않고 지나치게 될 것입니다

 

callElement는 아래와 같이./mouseHandleHtml 파일에서 함수를 가져오게 되어있습니다

 

웹크롤링-마우스조작
웹크롤링 마우스 조작 노출 element 생성

 

 

아래의 코드로 정의된 callElement함수는 

마우스가 어디에 있는지 화면에서 확인할 수 있는 html, CSS을 생성하게 됩니다

위와 말했듯이 개발에만 생성하게 되고  그 이유는 웹 크롤링하는 과정을 보면서 개발하기 위해서입니다

 

 

module.exports = async function  callElement(page) {
  await page.evaluate(() => {
      (() => {
          const box = document.createElement('div');
          box.classList.add('mouse-helper');
          const styleElement = document.createElement('style');
          styleElement.innerHTML = `.mouse-helper {
              pointer-events: none;
              position: absolute;
              z-index: 1000000000;
              top: 0;
              left: 0;
              width: 20px;
              height: 20px;
              background: rgba(0,0,0,.4);
              border: 1px solid white;
              border-radius: 10px;
              margin-left: -10px;
              margin-top: -10px;
              transition: background .2s, border-radius .2s, border-color .2s;
            }
            .mouse-helper.button-1 {
              transition: none;
              background: rgba(0,0,0,0.9);
            }
            .mouse-helper.button-2 {
              transition: none;
              border-color: rgba(0,0,255,0.9);
            }
            .mouse-helper.button-3 {
              transition: none;
              border-radius: 4px;
            }
            .mouse-helper.button-4 {
              transition: none;
              border-color: rgba(255,0,0,0.9);
            }
            .mouse-helper.button-5 {
              transition: none;
              border-color: rgba(0,255,0,0.9);
            }
            `;
          document.head.appendChild(styleElement);
          document.body.appendChild(box);
          document.addEventListener('mousemove', event => {
            box.style.left = event.pageX + 'px';
            box.style.top = event.pageY + 'px';
            updateButtons(event.buttons);
          }, true);
          document.addEventListener('mousedown', event => {
            updateButtons(event.buttons);
            box.classList.add('button-' + event.which);
          }, true);
          document.addEventListener('mouseup', event => {
            updateButtons(event.buttons);
            box.classList.remove('button-' + event.which);
          }, true);
          function updateButtons(buttons) {
            for (let i = 0; i < 5; i++)
              box.classList.toggle('button-' + i, !!(buttons & (1 << i)));
          }
        })();
    });
}

 

 

 

그리고 실질적으로 웹 크롤링을 실행하는 함수를 보시면

웹 크롤링 화면 설정과 필요 기능 함수를 선언하는 setCrawler()가 있습니다

 

그리고 setTime으로 잠시 대기하여 마우스를 움직이며 드래그하는 모습을 볼 수 있습니다

 

 

 

웹크롤링-마우스조작2
웹크롤링 마우스 조작 코드

 

 

아래의 이미지를 보시면 드래그를 한 후 그 자리 좌표에서 클릭을 하면

드래그가 없어지는 것을 확인할 수 있습니다

 

 

 

 

 

 

 

이렇게 웹 크롤링 마우스 조작 법을 알아보았습니다

그렇게 어렵지 않고 함수 기능만 알고 있다면

간편하게 구현할 수 있다는 것을 알게 되었습니다

 

저는 웹 크롤링을 공부하는 이유는 여러 가지 정보를 아침에 모아서 보려고 합니다

그래서 같이 공부하고 끝이 나면

개인적인 프로젝트를 진행하는 과정도 포스팅을 하도록 하겠습니다

 

후~ 그럼 오늘도 고생하셨습니다

 

공부가 힘들고 어렵겠지만 조금만 힘내시면 

그 시기는 지나갈 것이라 생각합니다

 

난중에는 너무 자연스럽게 코딩하는 자신의 모습을 볼 수 있을 것입니다

그럼 그때까지 파이팅 하시고

 

다음 포스팅도 기대해주세요!

감사합니다

728x90
반응형
728x90
반응형

이전 포스팅 키보드 코드 입력하기에 대해서

puppeteer로 웹 크롤링할 때 키보드를 입력하는 방법을 알아보았습니다

키보드의 자판만큼이나 코드도 엄청 많았습니다

그래서 그 문서를 Array형 Object객체로 만들어 자동으로 입력할 수 있게 만들었습니다

같은 함수를 계속 나열해서 쓰지 않아도 되니 코드도 간결해지고 좋았던 것 같습니다

그리고 무엇보다도 사용자의 유저 최적화를 할 수 있는 장점이 있지요

id를 입력하는 경우 키보드 코드로 입력 안 할 경우에는 한 번의 순간에 ID 값이 입력되니

분명 유저처럼 보이지는 않을 것입니다

 

그래서 봇으로 보이지 않게 하기 위한 방법이니 아주 유용할 것 같습니다

 

더 자세한 사항은 아래의 링크를 클릭해서 확인해주세요

 

 

 

2022.08.30 - [IT_Web/Nodejs] - 웹 크롤링 puppeteer 활용 및 keyboard press 함수로 키보드 코드 입력하기

 

웹 크롤링 puppeteer 활용 및 keyboard press 함수로 키보드 코드 입력하기

이전 포스팅 웹 크롤링 로그인 로그아웃에 대하여 로그인과 로그아웃에 대하여 알아보았습니다 코드를 함수형으로 만들어서 이전 코드보다 간결하게 만들어 보았지요 그리고 웹 크롤링에서는

tantangerine.tistory.com

 

웹 크롤링 puppeteer로 alert, prompt, confirm 창 컨트롤 하기

 

웹 크롤링 도중 alert, confirm, prompt창이 노출되면 아무것도 하지 못하게 된다

이럴 경우 창을 제어하는 함수를 구현해보려고 합니다

아래와 같이 setCrawler 함수에 여러 가지가 세팅되어있다

천천히 분석해보시길 바라며 상세한 설명은 아래에서 하겠습니다

 

const puppeteer = require('puppeteer');
const writekeyboardList = require('./writekeyboardList');
const dotenv = require('dotenv'); // npm i dotenv
dotenv.config();

const setCrawler = async () => {
  const browser = await puppeteer.launch({
    headless: false,
    args:['--window-size=1920,1080']
  });
  const page = await browser.newPage();
  await page.setViewport({
    width: 1080,
    height:1080,
  })
  await page.goto('https://facebook.com');

  await page.on('dialog', async (dialog) => { 
    console.log(`type: ${dialog.type()} / message: ${dialog.message()}`) 
    if(dialog.type() === 'prompt') {
      await dialog.accept('https://www.tistory.com/')
    }
    if(dialog.type() === 'alert'){
      await dialog.dismiss(); 
    }
    if(dialog.type() === 'confirm'){
      if (dialog.message() === '확인을 누르시면 tistory홈페이지로 이동합니다.') {
        await dialog.accept();
      } else {
        await dialog.dismiss();
      }
    }
  })

.... 이하 생략 이전 포스팅 참고 바랍니다.

 

 

 

아래의 코드는 실질적으로 웹 크롤링을 실행하는 함수들이 선언되어있습니다.

보시면 setCrawler()가 보이시죠? 브라우저를 띄우고 새 탭 화면을 노출시키고 화면의 비율을 맞추는 등

기본적인 환경설정을 하고 나서 선언된 함수들을 return 해서 사용하고 있습니다

 

오늘 포스팅에서는 사용하지는 않지만 이전 포스팅을 참고해주세요

 

그럼 본론으로 alert, confirm, prompt 컨트롤하는 함수를 선언해서 확인해보겠습니다

우선 한 개씩 하기 때문에 나머지는 주석을 하면서 진행해 보도록 하겠습니다

 

const crawler = async () => {
  try {
    const { buttonClick, inputValue, handleWriteInuput, setTime, page, browser } = await setCrawler();
    await page.evaluate(async() => {
      alert('alert창을 닫으면 티스토리 홈페이지로 이동합니다.')
      location.href = 'http://www.tistory.com'
    })

    await page.evaluate(() => {
      if (confirm('확인을 누르시면 tistory홈페이지로 이동합니다.')) {
        location.href = 'http://www.tistory.com'
      }
    })

    await page.evaluate(() => {
      const promptData = prompt('url 주소를 입력하세요.')
      location.href = promptData
    })

  } catch (e) {
    console.log(e);
  }
}

crawler()

 

 

아래와 같이 코드가 구현되어있습니다

먼저 빨간 테두리에 page.on()가 있습니다

이 함수는 EventListener로서 어떠한 이벤트 가 발생할 때 감지해서 

이벤트가 발생한 함수에 영향을 주는 함수입니다

 

매개변수('dialog', function)로 받아 콜백 함수에서 실행을 이어갑니다

그래서 콜백 함수 내에서 dialog에 대해 type, message를 분기로 사용해서

 

 

웹크롤링-이벤트리스너
웹크롤링 이벤트 리스너 함수

 

 

alert은 창이 뜨면 그냥 바로 창을 바로 제거하는 함수를 사용합니다

alert창이 사라지고 다음 단계를 진행할 수 있게 되며,

선언된 alert함수 그 이후의 선언된 함수가 실행되어

 

웨크롤링-alert제어
웹크롤링 alert창 제어

 

 

confirm창이 승낙할 경우 confirm함수 true, 거절할 경우 false 반환되게

dialog.accept()와 dialog.dismiss()를 호출하여 confirm()가 선언된 곳에 값을 전달합니다

 

웹크롤링-confirm제어
웹크롤링 confirm 창 제어

 

 

prompt창은 input이 활성화되면서 글자를 입력할 수 있으며,

그 창에 노출되고 dialog.accept() 인자 값으로 'https://www.tistory.com'로 할당하게 되면

홈페이지가 이동하게 됩니다

 

 

웹크롤링-prompt제어
웹크롤링 prompt창 제어

 

 

 

오늘은 여기까지 웹크롤링 중 EventListener를 사용하는 page.on()를 선언해서

alert, confirm, prompt 활성화될 경우 제어하는 방법을 알아보았습니다

 

얼럿 창이 갑자기 활성화된다면 웹크롤링이 갑자기 멈출 수도 있기 때문에

이벤트 리스너가 꼭 필요할 것입니다

 

오늘도 이렇게 한걸음 나아갔다고 생각합니다

우리의 IT대모험은 아직 남았으니 파이팅 하자고요~!!

 

그럼 다음 포스팅도 기대해주세요~

 

 

 

728x90
반응형
728x90
반응형

이전 포스팅 웹 크롤링 로그인 로그아웃에 대하여

로그인과 로그아웃에 대하여 알아보았습니다

코드를 함수형으로 만들어서 이전 코드보다 간결하게 만들어 보았지요

그리고 웹 크롤링에서는 무엇보다

태그를 찾는것중요하여 같이 알아보았습니다

 

좀 더 상세히 알고 싶으시다면 아래의 링크를 클릭하시길 바랍니다

 

 

2022.08.29 - [IT_Web/Nodejs] - 웹 크롤링 로그인, 로그아웃 그리고 waitForResponse 응답대기 활용하기 및 태그찾기, 태그 값 할당

 

웹 크롤링 로그인, 로그아웃 그리고 waitForResponse 응답대기 활용하기 및 태그찾기, 태그 값 할당

이전 포스팅에서 웹 크롤링 인피니트 스크롤링이 무엇인가 알아보았으며, 인피니트 스크롤링이 적용된 페이지에 대해서 이미지를 다운하고 파일로 만드는 방법을 알아보았습니다 인스타그램,

tantangerine.tistory.com

 

 

앞서 웹 크롤링에서 중요한것은 태그를 찾는것이라 했습니다

하지만 중요한것이 하나 더있습니다

그것은 바로 비동기입니다

하지만 비동기에서도 순ㅍ차적으로 실행할지 비순차적으로 할것인지가 중요합니다

웹 크롤링할 경우 순차적으로 진행할 경우가 많기 때문에 비동기식이며 순차적인 방법이 중요합니다

 

이전 포스팅에서는 로그인할경우 아이디를 inputvalue에 할당하여 한번에 아이디의 문자열이 모두 들어갔습니다

하지만 그렇게 된다면 유저가아닌 봇으로 의심을 받아 제재를 받을 수 있습니다 

그래서 이 부분을 함수형으로 만들어 간결하게 만들어봅시다

 

아래 handleWriteInput 함수를 만들어서 process.env.EMAIL을 id로 받아서 id를 확장 연산자를 사용해서

배열로 만들어서 로그인창에서 한글자씩 시간을 두고 글자를 입력하게됩니다

좀 더 자세한것은 아래에서 확인해보겠습니다

 

 

const puppeteer = require('puppeteer');
const writekeyboardList = require('./writekeyboardList');
const dotenv = require('dotenv'); // npm i dotenv
dotenv.config();

const setCrawler = async () => {
  const browser = await puppeteer.launch({
    headless: false,
    args:['--window-size=1920,1080']
  });
  const page = await browser.newPage();
  await page.setViewport({
    width: 1080,
    height:1080,
  })

  const handleWriteInuput = async (selector, id) => {
    const writeList = writekeyboardList()
    const idOfList = [...id]
    await page.click(selector);
    console.log(`idOfList`, idOfList)
      for (const id of idOfList) {
        const inputValue = await writeList.filter(element => element.ShiftLeft === id || element.key === id )
        if (inputValue.length > 0) {
          const code = inputValue[0].code
          const ShiftLeft = inputValue[0].ShiftLeft === id ? 'ShiftLeft' : ''
          if (ShiftLeft) {
            await page.keyboard.down(ShiftLeft);
            await setTime(selector, 500)
            await page.keyboard.press(code);
            await setTime(selector,500)
            await page.keyboard.up(ShiftLeft);
          } else {
            await page.keyboard.press(code);
          }
          await setTime(selector,500)
        }
      }
  }

  const inputValue = async (selector, value) => {
    await page.evaluate((selector, value) => {
      document.querySelector(selector).value = value
    }, selector, value)
    await setTime(selector)
  }

  const buttonClick = async (selector, nextSelector, type) => {
    await page.evaluate((selector) => {
      document.querySelector(selector).click();
    }, selector)
    if(type === 'login'){
      // waitForRequest(요청 대기), waitForResponse(응답 대기)
      const res = await page.waitForResponse(res => {
        return res.url().includes('graphql')
      })
      const result = await res.json()
      console.log('login', result);
      if(await result.extensions.is_final){
        await setTime(type, 2000)
        await page.keyboard.press('Escape')
      }
    } else if(type === 'logOut'){
      await page.waitForNavigation()
    } else {
      if(nextSelector){
        await page.waitForSelector(nextSelector)
      }
      await setTime(selector)
    }
  }

  const setTime = async (selector = 'normal', waitTime = 0) => {
    const setTime = await page.evaluate(async(waitTime) => {
      const timeList = await [3975, 4547, 5231, 6397, 7894, 3394, 4206, 5147, 6932, 7430, 3561, 4896, 5877, 6407, 7133];
      let randomTime = await timeList[Math.floor(Math.random() * timeList.length)]
      if (waitTime > 0) randomTime = waitTime
      await new Promise(r => setTimeout(r, randomTime))
      return randomTime
    }, waitTime)
    console.log(`waitTime[${selector}]: `, setTime)
    return setTime
  } 
  return { buttonClick, inputValue, handleWriteInuput, setTime, page: page, browser: browser }
}


const crawler = async () => {
  try {
    const { buttonClick, inputValue, handleWriteInuput, setTime, page, browser } = await setCrawler();
    await page.goto('https://facebook.com');

    const id = await process.env.EMAIL;
    const password = await process.env.PASSWORD;
    
    // 로그인
    //await inputValue('#email', id);
    handleWriteInuput('#email', id)
    await inputValue('#pass', password);
    await buttonClick('[name="login"]', '', 'login');
   

    // 로그아웃
    await buttonClick('.i8zpp7h3', '.i1n1lj7b.mmwt03ec');
    await buttonClick('.i1n1lj7b.mmwt03ec > div:last-child > div', '', 'logOut')
    await page.close();
    await browser.close();
  } catch (e) {
    console.log(e);
  }
}

crawler()

 

 

아래의 함수를 확인하면 각자의 역할들이 존재합니다

아래에서 차근차근 설명해 보겠습니다

 

웹크롤링-키보드코드
키보드 코드 사용하여 입력하기 함수형!

 

 

먼저 빨강 테두리의 함수는 키보드 코드 리스트가 담겨져 있으며,

require()를 사용해서 파일에 접근해서 함수를 사용할 수 있게 합니다

 

 

웹크롤링-nodejs
웹크롤링 키보드 코드 파일

 

 

파일에는 아래와 같은 함수가 export되고 있습니다

code가 위의 keyboard.press()의 매개변수 인자값이 되며

Shift를 누른 상태 값은 ShiftLeft의 값이 대체되어 값이 입력된다

 

 

 

 

module.exports = function writekeyboardList() {
  const keyboard = [
    {'keyCode': 48, 'code': 'Digit0', 'ShiftLeft': ')', 'key': '0'},
    {'keyCode': 49, 'code': 'Digit1', 'ShiftLeft': '!', 'key': '1'},
    {'keyCode': 50, 'code': 'Digit2', 'ShiftLeft': '@', 'key': '2'},
    {'keyCode': 51, 'code': 'Digit3', 'ShiftLeft': '#', 'key': '3'},
    {'keyCode': 52, 'code': 'Digit4', 'ShiftLeft': '$', 'key': '4'},
    {'keyCode': 53, 'code': 'Digit5', 'ShiftLeft': '%', 'key': '5'},
    {'keyCode': 54, 'code': 'Digit6', 'ShiftLeft': '^', 'key': '6'},
    {'keyCode': 55, 'code': 'Digit7', 'ShiftLeft': '&', 'key': '7'},
    {'keyCode': 56, 'code': 'Digit8', 'ShiftLeft': '*', 'key': '8'},
    {'keyCode': 57, 'code': 'Digit9', 'ShiftLeft': '\(', 'key': '9'},
    {'keyCode': 65, 'code': 'KeyA', 'ShiftLeft': 'A', 'key': 'a'},
    {'keyCode': 66, 'code': 'KeyB', 'ShiftLeft': 'B', 'key': 'b'},
    {'keyCode': 67, 'code': 'KeyC', 'ShiftLeft': 'C', 'key': 'c'},
    {'keyCode': 68, 'code': 'KeyD', 'ShiftLeft': 'D', 'key': 'd'},
    {'keyCode': 69, 'code': 'KeyE', 'ShiftLeft': 'E', 'key': 'e'},
    {'keyCode': 70, 'code': 'KeyF', 'ShiftLeft': 'F', 'key': 'f'},
    {'keyCode': 71, 'code': 'KeyG', 'ShiftLeft': 'G', 'key': 'g'},
    {'keyCode': 72, 'code': 'KeyH', 'ShiftLeft': 'H', 'key': 'h'},
    {'keyCode': 73, 'code': 'KeyI', 'ShiftLeft': 'I', 'key': 'i'},
    {'keyCode': 74, 'code': 'KeyJ', 'ShiftLeft': 'J', 'key': 'j'},
    {'keyCode': 75, 'code': 'KeyK', 'ShiftLeft': 'K', 'key': 'k'},
    {'keyCode': 76, 'code': 'KeyL', 'ShiftLeft': 'L', 'key': 'l'},
    {'keyCode': 77, 'code': 'KeyM', 'ShiftLeft': 'M', 'key': 'm'},
    {'keyCode': 78, 'code': 'KeyN', 'ShiftLeft': 'N', 'key': 'n'},
    {'keyCode': 79, 'code': 'KeyO', 'ShiftLeft': 'O', 'key': 'o'},
    {'keyCode': 80, 'code': 'KeyP', 'ShiftLeft': 'P', 'key': 'p'},
    {'keyCode': 81, 'code': 'KeyQ', 'ShiftLeft': 'Q', 'key': 'q'},
    {'keyCode': 82, 'code': 'KeyR', 'ShiftLeft': 'R', 'key': 'r'},
    {'keyCode': 83, 'code': 'KeyS', 'ShiftLeft': 'S', 'key': 's'},
    {'keyCode': 84, 'code': 'KeyT', 'ShiftLeft': 'T', 'key': 't'},
    {'keyCode': 85, 'code': 'KeyU', 'ShiftLeft': 'U', 'key': 'u'},
    {'keyCode': 86, 'code': 'KeyV', 'ShiftLeft': 'V', 'key': 'v'},
    {'keyCode': 87, 'code': 'KeyW', 'ShiftLeft': 'W', 'key': 'w'},
    {'keyCode': 88, 'code': 'KeyX', 'ShiftLeft': 'X', 'key': 'x'},
    {'keyCode': 89, 'code': 'KeyY', 'ShiftLeft': 'Y', 'key': 'y'},
    {'keyCode': 90, 'code': 'KeyZ', 'ShiftLeft': 'Z', 'key': 'z'},
    {'keyCode': 190, 'code': 'Period', 'ShiftLeft': '>', 'key': '.'},
  ]
  return keyboard
};

 

 

 

 

더 많은 키보드 코드들을 알고 싶다면,

사이드 바에 공식문서 & 유용한 도구 목록을 확인해보세요!

 

 

웹크롤링-공식문서
웹크롤링 공식문서

 

그렇게 지정된 코드들을 아래의 특정 문자열을 받아와서 filter()를 적용해서 code, ShiftLeft를 가져옵니다

ShiftLeft가 값이 할당되면 대문자라는 뜻이므로 

대문자라면 keyboard.down(), keyboard.up()을 적용합니다

 

아래의 함수를 선언해서 handleWrite를 호출하는 것만으로 간단하게 적용할 수 있습니다

또 한, for(const id of idOfList)에서 비동기식 순차적으로 실행하게되어

글자를 한글자씩 입력하게됩니다

 

for (const id of idOfList) {
    const inputValue = await writeList.filter(element => element.ShiftLeft === id || element.key === id )
    if (inputValue.length > 0) {
      const code = inputValue[0].code
      const ShiftLeft = inputValue[0].ShiftLeft === id ? 'ShiftLeft' : ''
      if (ShiftLeft) {
        await page.keyboard.down(ShiftLeft);
        await setTime(selector, 500)
        await page.keyboard.press(code);
        await setTime(selector,500)
        await page.keyboard.up(ShiftLeft);
      } else {
        await page.keyboard.press(code);
      }
      await setTime(selector,500)
    }
}

 

 

 

이렇게 웹크롤링에서 키보드 코드를 입력해서 사용자 친화적으로 만들어서

봇으로 의심을 조금이라도 피하게하기위함입니다

웹크롤링의 포스팅 글이 점점 길어지고 있습니다

그래도 알게되는 것도 많으며,  앞으로 확실히 나아가는 것같습니다

그럼 다음 포스팅도 기대해주시고 

 

다음 웹 크롤링 관련 글로 찾아뵙겠습니다

 

대모험이 끝나기를 기대하며 화이팅!

 

 

 

728x90
반응형
728x90
반응형

이전 포스팅에서 웹 크롤링 인피니트 스크롤링무엇인가 알아보았으며,

인피니트 스크롤링이 적용된 페이지에 대해서

이미지를 다운하고 파일로 만드는 방법을 알아보았습니다

 

인스타그램, 페이스북 여러 웹사이트에서 인피니트 스크롤링을 적용하고 있으니

꼭 알아두어야 할 것 같습니다

 

그럼 더 자세히 알고 싶으시면 아래의 링크를 확인해주세요

 

 

2022.08.27 - [IT_Web/Nodejs] - 웹 크롤링 인피니트 스크롤링 조작하여 이미지 다운로드하고 파일 만들기

 

웹 크롤링 인피니트 스크롤링 조작하여 이미지 다운받고 파일만들기

이전 포스팅에서 웹 크롤링을 하여 스크린샷을 찍는 방법에 대해 풀 스크린과 어느 특정 영역, 또는 화면 전체 이렇게 3가지 방법으로 스크린샷 찍는 방법을 알아보았습니다 웹크롤링할 때 화면

tantangerine.tistory.com

 

 

본론에 앞서 웹 크롤링을 하실 때는

본인이 책임져야 한다는 것을 잊지 마세요

합법적이지 못한 방법은 언제나 

꼭 책임이 따른다는 것을 명심하시길 바랍니다

 

그러니 회사 업무의 효율성을 위해서만 사용하시고

올바르게 사용하시길 바랍니다

 

그리고

웹 크롤링이 벌써 7번째(?) 포스팅인 것 같습니다

그래서 환경설정과 여러 라이브러리 설치 방법은 생략하고 진행하겠습니다

보고 싶으신 분이 계시다면 이전 포스팅에서 확인하시길 바랍니다

메뉴에 Node 쪽을 확인해보시면 관련 포스팅을 찾을 수 있습니다 

 

그럼 이제 본론으로 넘어가 보겠습니다

 

웹 크롤링 로그인, 로그아웃 및 waitForResponse(응답 대기) 알아보기 

 

아래의 코드를 보시면

약간 코드를 수정해서 값을 할당하는 함수와 웹 크롤링하는 함수를 불리하여

함수의 재사용을 높일 수 있게 변경하였습니다

 

그럼 아래에서 더 상세 한 설명을 이어나가겠습니다

기초적인 브라우저 환경설정 등은 주석으로 설명을 대신하겠습니다

로그인 로그아웃에 초점을 맞추겠습니다

 

const puppeteer = require('puppeteer');
const dotenv = require('dotenv'); // npm i dotenv
//const userAgent = require('new-user-agent')
dotenv.config();

const setCrawler = async () => {
  const browser = await puppeteer.launch({ // 웹 사이트의 브라우저를 띄우는 함수이다
    headless: false, // 웹크롤링할 때 사이트 노출 여부를 결정하는 값(운영을 하게될때 노출은 필요없다)
    args:['--window-size=1920,1080'] // 브라우저의 size를 설정한다
  });
  const page = await browser.newPage(); // 브라우저를 노출하고 새 탭보기로 페이지를 오픈한다
  await page.setViewport({ // 오픈할 때 가로 세로 길이를 설정한다
    width: 1080,
    height:1080,
  })

  //userAgent의 값을 랜덤으로 셋팅해주는 함수입니다 하지만 
  // 하지만 버전이 너무 낮아 현제 웹페이지에는 맞는게 없네요
  //await page.setUserAgent(userAgent.test());
  //console.log('userAgent', userAgent.test());

  const inputValue = async (selector, value) => {
    await page.evaluate((selector, value) => {
      document.querySelector(selector).value = value
    }, selector, value)
    await setTime(selector)
  }
  const buttonClick = async (selector, nextSelector, type) => {
    await page.evaluate((selector) => {
      document.querySelector(selector).click();
    }, selector)
    if(type === 'login'){
      const res = await page.waitForResponse(res => {
        return res.url().includes('graphql')
      })
      const result = await res.json()
      console.log('login', result);
      if(await result.extensions.is_final){
        await setTime(type)
        await page.keyboard.press('Escape')
      }
    } else if(type === 'logOut'){
      await page.waitForNavigation()
    } else {
      if(nextSelector){
        // waitForRequest(요청 대기), waitForResponse(응답 대기)
        await page.waitForSelector(nextSelector)
      }
      await setTime(selector)
    }
  }
  const setTime = async (selector = 'normal') => {
    const setTime = await page.evaluate(async() => {
      const timeList = await [3975, 4547, 5231, 6397, 7894, 3394, 4206, 5147, 6932, 7430, 3561, 4896, 5877, 6407, 7133];
      const randomTime = await timeList[Math.floor(Math.random() * timeList.length)]
      await new Promise(r => setTimeout(r, randomTime))
      return randomTime
    })
    console.log(`waitTime[${selector}]: `, setTime)
    return setTime
  } 
  return { buttonClick, inputValue, setTime, page: page, browser: browser }
}

// keyboard.press(); 인자값 목록 https://github.com/puppeteer/puppeteer/blob/v1.12.2/lib/USKeyboardLayout.js
// $('.i8zpp7h3')
// $('.i1n1lj7b.mmwt03ec')
// 

const crawler = async () => {
  try {
    const { buttonClick, inputValue, setTime, page, browser } = await setCrawler();
    const url = 'https://facebook.com';
    await page.goto(url);

    const id = await process.env.EMAILL;
    const password = await process.env.PASSWORD;
    
    // 로그인
    await inputValue('#email', id);
    await inputValue('#pass', password);
    await buttonClick('[name="login"]', '', 'login');
   

    // 로그아웃
    await buttonClick('.i8zpp7h3', '.i1n1lj7b.mmwt03ec');
    await buttonClick('.i1n1lj7b.mmwt03ec > div:last-child > div', '', 'logOut')
    // await page.close();
    // await browser.close();
  } catch (e) {
    console.log(e);
  }
}

crawler()

 


 

로그인 input 태그 찾기 및 로그인 하기

 

아래의 이미지를 보시면

<input> 태그 id, name속성 값이 email로 지정되어있습니다

 

 

웹크롤링-태그찾기
웹크롤링 태그 찾기

 

 

위에서 찾은 태그 이름인 #email, #pass를 함수 호출의 매개변수 첫 번째 인자 값으로 설정합니다

그리고 두 번째에는 로그인할 id와 password를 지정합니다

 

다음으로 선언된 inputValue 함수에 page.evaluate()를 호출하여 document.querySelector()로 태그에 접근합니다

이때 첫 번째 매개변수인 selector 인자 값을  document.querySelector(selector) 할당하여 호출합니다

그럼 태그를 가져오게 되고 그때 .value값을 새로 지정하거나 click()를 호출하여 버튼을 클릭하여

로그인을 시도하게 됩니다

 

 

여기서 잠깐!!

page.evalueate() 함수 안에서 document객체에 접근할 수 있다는 것 기억하시죠??

그리고 evaluate()에서 두 번째 매개변수부터 값을 할당해서 호출하면 

evaluate() 내부에서 사용이 가능하다는 것을 잊지 마세요

 

 

 

 

 

 

웹크롤링-로그인
웹크롤링 로그인 함수

 

 

evaluate() 함수에서 document객체를 활용해서 id, password를 할당하는 것과

puppeteer를 활용해서 하는 방법 두 가지가 있습니다

 

아래의 코드에서 page.type(), page.hover(), page.click() 3개의 함수를 확인할 수 있습니다

간단해 보이는 이 함수들은 그 사용법과 활용법이 다르며

언제 이 함수들이 버전업으로 활용방법이 변경될지 모르는 일입니다

그러니 대도록이면 함수를 선언하여 위와 같이 호출하는 방식으로 사용하시길 바랍니다

 

 

const crawler = async () => {
  try {
    const { buttonClick, inputValue, setTime, page, browser } = await setCrawler();
    await page.goto('https://facebook.com');

    const id = await process.env.EMAILL;
    const password = await process.env.PASSWORD;

    await page.type('#email', id);
    //await inputValue('#email', id);

    await page.type('#pass', password);
    //await inputValue('#pass', password);

    await page.hover('[name="login"]'); // 마우스를 선택자에 올리기
    await Promise.all([
      page.click('[name="login"]'),
      page.waitForNavigation(),
    ]);
    //await buttonClick('[name="login"]');
    await setTime()
    await page.keyboard.press('Escape');
        console.log('Escape')
    // await page.close();
    // await browser.close();
  } catch (e) {
    console.log(e);
  }
}

crawler()

 


 

waitForResponse 응답 대기 활용방법

 

 

로그인을 성공하면 아래와 같이 권한 페이지 설정을 하기 위해 dim처리가 되어 있습니다

dim처리를 없애보도록 하겠습니다

 

 

 

 

로그인을 클릭하게 되면 서버 호출을 시도하게 됩니다

그래서 page.waitForResponse()를 호출해서

웹 페이지에서 서버를 호출한 res.url()과 우리가 알고 있는 url주소를 비교하여

같으면 true값을 return 합니다

그리고 res값을 json()를 사용하여 Object형태로 받아올 수 있으며,

아래와 같이 접근이 가능합니다

 

 

 

웹크롤링 waitForResponse 함수 결과값

 

 

 

 

result.extensions.is_final에 접근을 하여 true일 경우

로그인 호출 후 페이지 랜더링 시간을 예방하기 위해 setTime()를 호출합니다

그리고 keyboard.press() 함수를 사용해서 Escape 즉 우리가 알고 있는 esc를 누르는 효과로 

dim처리를 해제할 수 있습니다 

 

사이드 바 공식문서 카테고리에 키보드를 누를 수 있는 인자 값으로 뭐가 있는지

링크를 걸어두도록 하겠습니다

 

 


 

 

로그인 아웃 태그 찾기 및 로그아웃하기

 

 

아래와 같이 빨간색 테두리를 클릭하게 되면 

class명을 확인할 수 있습니다

그럼 앞서 로그인 하기와 같이 선언된 함수를 사용해서 프로필 서브 창을 활성화합니다

 

 

 

웹크롤링-태그찾기2
웹크롤링 로그아웃 태그 찾기

 

 

 

태그를 찾기 위해서는 창에 태그를 확인해야 합니다

아래와 같이 불확실할 경우에는 특정 지역을 찾을 수밖에 없습니다

어차피 웹크롤링은 웹사이트 하나에 대해서 대응할 수밖에 없으니

아래와 같이 지정해 봅시다

서브 창에서 div를 찾아 전체 div태그를 찾습니다

그리고 > div:last-child > div를 해서 최하위 div태그를 찾아서 로그인 버튼을 클릭하게 합니다

 

 

 

 

웹크롤링-태그찾기3
웹크롤링 서브창 태그 찾기

 

 

 

서브 창이 활성화될 때까지 기다려서 그다음 로그아웃 메뉴를 클릭해야 합니다

 

그것을 구현하기 위해 서브 창을 활성화할 태그 첫 번째 인자 값과

활성화될 때까지 기다릴 두 번째 인자 값을 할당하여 호출합니다

 

 

그래서 클릭한 다음 아래와 같이 함수를 호출하여 

다음 태그를 기다리고 그다음 함수를 실행하게 됩니다

 

 

웹크롤링 서브창 활성화 대기

 

 

 

이렇게 웹 크롤링해서 로그인과 로그아웃 그리고 서버 응답 대기,

그리고 현재 페이지 내에서 클릭 이벤트 등을 이용하여 페이지가 변화될 때

기다리는 함수를 알아보았습니다

 

웹크롤링은 개발할 때 한눈에 확보여서 재밌기도 합니다

이런 것을 배워서 제가 하고자 하는 것에 더 좋은 시스템을 만들어서 활용해보도록 하겠습니다

 

이렇게 이번 포스팅은 마치도록 하겠습니다

오늘도 너무너무 고생했지만 그래도 보람이 있는 하루인 것 같습니다

 

이렇게 보람찬 생활을 한다면 언젠가는 이 대모험도 끝나지 않을까? 합니다

 

그럼 끝날 수 있다고 믿으면 파이팅합시다

그럼 다음 포스팅에 뵙겠습니다

 

 

 

 

 

 

 

 

 

728x90
반응형
728x90
반응형

이전 포스팅에서 웹 크롤링을 하여 스크린샷을 찍는 방법에 대해

풀 스크린과 어느 특정 영역, 또는 화면 전체 이렇게 3가지 방법으로

스크린샷 찍는 방법을 알아보았습니다

 

웹크롤링할 때 화면 비율을 적용하는 등 여러가지를 알아보았습니다

더 자세히 알고 싶으시다면 아래 링크를 클릭해 주시길 바랍니다

 

그리고 이전 포스팅에서 설명한 세팅엑셀파일 쓰기, 읽기

자세한 설명은 생략할 수 있으니 참고 하시길 바랍니다

 

 

2022.08.26 - [IT_Web/Nodejs] - 웹 크롤링 퍼펫티어 활용하여 스크린샷 이미지 폴더에 저장하기

 

웹 크롤링 퍼펫티어 활용하여 스크린샷 이미지 폴더에 저장하기

이전 포스팅 웹크롤링 이미지 다운로드 적용하기 퍼펫티어를 활용해 웹 크롤링을 하여 이미지 다운로드를 적용해보았다 navigator.userAgent에 대한 속성 값도 변경해보고, 이미지를 웹에서 다운받

tantangerine.tistory.com

 

 


 

인피니트 스크롤링이란?

 

먼저 인피니트 스크롤링이란 무엇일까요?

이 효과는 페이스북, 인스타그램 등 많은 사이트에서 사용을 하고 있는 방법입니다

사용자가 클릭을 하지않고 컨텐츠에 계속 집중할 수 있도록

스크롤링만으로 사용자에게 컨텐츠가 무한 노출하여

사이트 이탈율을 방지하는 효과가 있습니다

 

그래서 조회하는 버튼형식이 아닌 스크롤을 조작해서

어느 위치에 오면 컨텐츠를 조회하는 것입니다

 

그래서 이제는 웹크롤링할 때 스크롤을 조작해서 

이미지를 다운받는 방법 구현하려합니다

 

하지만 인피니트 스크롤링을 구현할 경우에는

싱글 페이지 어플리케이션을 활용하는 웹 사이트가 대부분입니다

 

그리고 리액트를 사용했다면 클라이언트 사이드 랜더링을 사용하기 때문에

웹크롤링이 쉽지않습니다

 

그래서 이런 경우에는 puppeteer를 사용하면 웹크롤링이 수월해집니다

 

그리고 웹 크롤링하기 적당한 웹 사이트인지 판단하기위해 

postman을 사용하여 개발하기 이전에 어느정도 판단이 가능합니다

 

 

아래의 빨간영역을 보시면 웹크롤링할 사이트로 요청을 보내면

html코드, 그리고 웹 크롤링하려는 사이트의 미리보기가 가능합니다

그래서 이미지를 보거나 코드를 보면서 선택자 접근이 얼마나 가능한지도 판단해서

웹크롤링을 시작할 수 있습니다

 

웹크롤링-스크롤링이미지
웹 크롤링에 적합한 사이트 알아보기

 


 

 

 


 

스크롤조작 해서 이미지 다운받기

 

웹 스크롤 대상 웹 사이트는 unsplash.com 입니다

 

 

이미지 다운받기 이전에

태그에 접근 하는 방법을 먼저 알아보고 코드 분석에 들어가겠습니다

 

 

웹크롤링-스크롤링2
웹 스크롤링 태그 접근 미리하기

 

지금 현재 위에서 보시면 두개의 태그가 있습니다

<div class="GIFah"> <img class="YV19w"> 두 개의 태그를 볼 수 있습니다

그래서 접근이 가능한지 아니면 필요없는 태그가 붙어서 오는지 선택자를 사용해서 미리 태그에 접근을 해봅니다

아래의 이미지와 같이 태그 접근이 가능한 것을 확인할 수 있습니다

 

 

웹크롤링-스크롤3
선택자로 태그 접근하기

 

 

 

 

그럼 이제 아래의 코드를 천천히 분석해보시길 바랍니다

 

아래에 코드를 보시면

브라우저와 페이지 영역을 설정하여 해상도를 높입니다

그리고 querySelectorALL을 활용해서 이미지의 부모 엘리먼트를 태그를 가져옵니다

그리고 태그로 forEach를 사용하여 자식 img태그에 접근하여

src에 접근하여 이미지 주소를 가져와 이미지 다운받는 것까지 구현하였습니다

 

상세한 설명은 아래에서 하겠습니다

 

const axios = require('axios')
const fs = require('fs');
const puppeteer = require('puppeteer');

fs.readdir('imges', (err) => {
  if(err) {
    console.log('imges 폴더가 없어 imges 폴더를 생성합니다.')
    fs.mkdirSync('imges')
  }
})

const crawler = async () => {
  try {
  const browser = await puppeteer.launch({
      headless: false,
      args:['--window-size=1920,1080']
    });
  const page = await browser.newPage();
  await page.setViewport({
    width: 1920,
    height:1080,
  });
  await page.goto('https://unsplash.com')
  let result = []
  while(result.length < 10) {
    const srcs = await page.evaluate(() => {
      window.scrollBy(0, 0)
      const imgs = []
      const imgEls = document.querySelectorAll('.GIFah')
      if (imgEls.length) {
        imgEls.forEach((y, i) => {
          let src = y.querySelector('img.YVj9w').src
          if(i < 10){
            if(src){
                imgs.push(src)
            }
            y.parentElement.removeChild(y);
          }
        });
      }
      window.scrollBy(0, 30)
      return imgs
    });
    result = result.concat(srcs)
    await page.waitForSelector('.GIFah')
    console.log('result', result.length, result)
  }
  result.forEach(async(imgUrl, i) => {
    if (imgUrl) {
      const imgInfo = await axios.get(imgUrl.replace(/\?.*$/,''), { 
        responseType: 'arraybuffer',
      })
      fs.writeFileSync(`imges/${new Date().valueOf()}.jpeg`, imgInfo.data)
    }
  })
  await page.close();
  await browser.close();
  } catch (e) {
    console.error(e)
  }
}

crawler();

 

아래의 같이 해상도를 크게 설정합니다

스크롤을 조작하는데 너무 해상도가 작으면

스크롤 위치가 너무 많이 이동을 하게되면

스크롤링으로 다음 컨텐츠 조회가 잘 되지않는 현상이 있으니 참고하시길 바랍니다

 

웹크롤링-스크롤링4
웹크롤링 화면 비율 조정하기

 

 

아래에서 선택자를 사용해서 태그에 접근합니다

그리고 그 태그로 이미지태그를 접근하여 이미지를 다운받습니다

그리고 중요한것은 현재 가지고 있는 태그의 부모 태그에 접근해서 모든 하위 태그를 삭제합니다

그렇게 되면 처음 가져온 image 주소를 저장하고나서 필요없게 된 태그를 삭제하여

다음 스크롤링 해서 가져온 이미지 관련 태그만 접근할 수 있다는 장점이 있습니다

 

 

웹크롤링-스크롤5
태그 접속해서 삭제및 이미지 주소가져오기

 

while문으로 특정 조건이 만족할 때까지 무한 루프가 실행됩니다

그리고 concat을 해서 가져온 이미지 주소를 

최종 결과 변수에 저장을 합니다

 

 

 

웹크롤링-스크롤링6
특정 조건을 허용하기 전까지 무한루프 돌기

 

 

 

그리고 forEach로 파일을 만들어서 확인할 수있습니다

window.scrollBy()로 스크롤릉 내리는 효과를 주어 다음 컨텐츠를 조회하게 합니다

그리고 최종 결과 변수 값에 배열이 계속저장되어

결국에는 특정조건이 맞지 않게되면

무한 루프에서 나와 파일을 만들고

브라우저 및 페이지를 종료합니다

 

 

오늘은 여기까지입니다

웹 크롤링을 사이드 프로젝트를 하게되면서 여러가지 응용할 수 있을것같습니다

그래서 조금 기대도 되고 다음에 사이드 프로젝트를 하는것을

포스팅해서 올리겠습니다

 

이렇게 공부를 하면서 사이드 프로젝트를 만들어가는 과정에

흥미를 붙여서 계속 공부하려는 의지를 붙여주는게 정말 중요한 것같습니다

 

우리는 아직 너무많은 이야기들이 남았으니까요

솔직히 개발자가들의 공부가 어디가 끝인지도 잘모르겠네요

그러니 흥미 잃지마시고 끝까지 파이팅 하시길바랍니다

 

우리 IT 대모험은 끝나지 않았으니까요!!

 

 

 

 

728x90
반응형
728x90
반응형

이전 포스팅 웹크롤링 이미지 다운로드 적용하기

퍼펫티어를 활용해 웹 크롤링을 하여 이미지 다운로드를 적용해보았다

navigator.userAgent에 대한 속성 값도 변경해보고,

이미지를 웹에서 다운받아 드라이버에 저장하는 것과

받아온 이미지 링크 주소를 엑셀에도 넣어보았습니다

 

이번 포스팅도 이어지는 부분이 있으니 확인하시길 바랍니다

그리고 조금 더 상세한 부분은 아래의 링크를 확인해주세요

 

 

2022.08.24 - [IT_Web/Nodejs] - 웹크롤링 이미지 다운로드 및 puppeteer 활용해 이미지 주소 엑셀에 입력하기

 

웹크롤링 이미지 다운로드 및 puppeteer 활용해 이미지 주소 엑셀에 입력하기

이전 포스팅에서 웹크롤링 Puppeteer 및 Promise 활용 방법 Node.js로 웹 크롤링할 경우 Puppeteer를 사용하면 정말 편하게 개발을 할 수 있습니다 evaluate() 함수를 활용하여 잠시 대기시간을 갖거나, 선택

tantangerine.tistory.com

 


 

Puppeteer 활용해서 웹 사이트 스크린샷 이미지 저장하기

 

웹 크롤링을 하기위한 세팅은 이전 포스팅에서부터 이어지고 있으니

관심 있으신 분은 #웹 크롤링 태그를 클릭해서 확인해주시길 바랍니다

 

우선 아래의 코드를 작성합니다

 

launch(), setViewport(), page.screenshot()

함수에 대해서 알아보겠습니다

 

const fs = require('fs');
const puppeteer = require('puppeteer');

const xlsx = require('xlsx');
const workbook = xlsx.readFile('xlsx/data.xlsx');
const ws = workbook.Sheets.Sheet1;
const xlsxRecords = xlsx.utils.sheet_to_json(ws)

fs.readdir('screenShot', (err) => {
  if(err) {
    console.log('screenShot 폴더가 없어 screenShot 폴더를 생성합니다.')
    fs.mkdirSync('screenShot') 
  }
})

const screenshotCrawler = async () => {
  try {
    const browser = await puppeteer.launch({
      headless: false,
      args:['--window-size=1920,1080']
    });
    console.log('첫번째: records', xlsxRecords)
    const page = await browser.newPage();
    await page.setViewport({
      width: 1920,
      height:1080,
    });
    await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36')
    for(const [i, r] of xlsxRecords.entries()) {
      await page.goto(r.링크);
      await page.screenshot({ 
        path: `screenShot/${r.제목}.png`, // 저장할 위치를 지정한다
        fullPage: false, // 웹 페이지의 스크롤의 밑 전체 페이지를 스크린샷할 수 있다
        clip: {
          x: 100,
          y: 100,
          width: 300,
          height: 300,
        }// 왼쪽 상단 모서리 (x, y), 너비(width), 높이(height)
      });// 그리고 fullPage와 clip은 같이 사용할 수 없다
      
      const setTime = await page.evaluate(async() => {
        const timeList = await [3975, 4547, 5231, 6397, 7894, 3394, 4206, 5147, 6932, 7430, 3561, 4896, 5877, 6407, 7133];
        const randomTime = await timeList[Math.floor(Math.random() * timeList.length)]
        await new Promise(r => setTimeout(r, randomTime))
        return randomTime
      })
      console.log('setTime', setTime)
    }
    await page.close();
    await browser.close();
  } catch (e) {
    console.error(e)
  }
}

screenshotCrawler();

 

 

우선 초기에 해상도를 적용하지 않을 경우

아래와 같이 브라우저는 큰 화면이 아닌 반 정도인 것을 확인할 수 있습니다

 

 

웹크롤링-스크린샷2
초기 브라우저 설정

 

 

아래의 코드에 보면 기존 코드에 args:['--window-size=1920,1080']를 추가하면 됩니다

그리고 1920, 1080은 자신의 모니터 해상도를 생각하시고 지정합시다

 

 

웹크롤링-스크린샷3
브라우저 해상도 지정하기

 

 

위의 코드를 적용하면 브라우저의 크기가 커진것을 확인할 수 있습니다

이제는 화면의 해상도를 키우면 브라우저만큼 크게 넓어질 것 같습니다

 

 

웹크롤링-스크린샷4
args 값 적용 화면

 

 

아래의 코드 setViewport() 함수를 적용합니다 

브라우저 해상도와 맞게 값을 지정합니다

 

 

웹크롤링-스크린샷5
화면 해상도 지정하기

 

 

아래와 같이 화면이 해상도에 맞게 커지게 되었습니다

이제는 스크린샷을 찍는 코드를 적용하도록 하겠습니다

 

 

웹크롤링-스크린샷6
setViewport() 적용 화면

 

 

 

 

 

 

아래의 코드를 확인해봅시다

clip에 값을 지정하면 어느 한 영역을 스크린샷하여 찍을 수 있습니다

그 영역은 지정된 웹 페이지의 x, y축에서

width, height를 지정하여 사각형 영역을 만들어 스크린샷을 찍습니다

 

 

 

웹크롤링-스크린샷7
스크린샷 적용 코드

 

위와 같이 코드를 적용하면 아래와 같이 

스크린샷을 찍게 되어 path위치에 저장하게 됩니다

 

 

웹크롤링-스크린샷8
스크린샷 clip 값 적용 화면

 

 

아래의 전체 화면에 clip영역이 어떻게 지정되는지 확인이 가능합니다

이제는 fullPagetrue값을 주면 어떻게 스크린샷을 찍는지 확인해보겠습니다

 

 

웹크롤링-스크린샷9
clip 값 영역 지정

 

 

fullPage값을 true로 지정하게 되면 아래와 같이 전체 화면을 찍게 됩니다

웹 페이지 전체를 찍는 것이니 유의해 주시길 바랍니다

그리고 한 가지 더 fullPage 값이 true일 경우 clip 값을 지정하면

에러가 발생하니 주의하시길 바랍니다

 

 

웹크롤링-스크린샷10
fullPage값이 true로 지정된 스크린샷 이미지

 

 

이렇게 웹크롤링에서 스크린샷을 찍어 보았습니다

간단하게 함수 3개로 구현하였습니다

 

웹크롤링 기능은 아직 많이 남았으니

조금만 참고 포스팅을 기대해주시고 

힘내시길 바랍니다

 

아직 IT 대모험은 끝나지 않았으니까요

그럼 파이팅 하시고

 

다음 포스팅에서 뵙겠습니다

 

728x90
반응형
728x90
반응형

통계 쿼리는 조금 어려울 수도 쉬울 수도 있을 것 같습니다

하지만 대부분의 사람들은 어렵다고 할 것입니다

저 또한 그렇게 느끼고 있고요

 

한번 같이 공부하면서 알아보도록 하겠습니다

 

 

결과셋을 하나의 행으로 데이터 조회하기

 

통계 쿼리의 가장 기본인 결과셋을 하나의 행으로 만드는 것을 우선 해보겠습니다

행 그룹에서 값을 가져와서 그룹당 단일 행의 열로 변환하려고 합니다.

예를 들어 각 부서의 사원수를 표시하는 결과 셋이 있다고 해봅시다

 

 

아래와 같은 결과 셋을 하나행으로 보이도록 출력을 다시 재구성하려고 합니다.

통계쿼리1
결과셋을 하나의행으로 만들어보자

 

이 방법은 통계 쿼리에 자주 사용하는 방법이며 

가장 기초적인 방법의 예문으로 만들려고 합니다

 

이것을 바탕으로 응용해서 적용해보시길 바랍니다

 

 

우선 테이블에 어떤 데이터가 들어있는지 한번 확인해봅시다.

 

SELECT deptno FROM emp;

 

아래와 같이 데이터가 들어있으며, 총 14개의 행이 있습니다

 

통계쿼리2
원본 테이블 데이터 정보

 

이 데이터를 각 부서의 사원수를 구하기 위해서는

아래와 같이 일반적으로 생각하게 될 것입니다

하지만 이 데이터는 한 행에 출력이 않아 통계 쿼리로는 부적합합니다

그래서 하나 행으로 조회하는 방법을 알아보는 것이 최우선입니다 

 

SELECT deptno, coount(*) AS CNT FROM EMP GROUP BY deptno;

 

 

하나의 행으로 출력하는 과정을 알아보도록 하겠습니다

아래의 코드를 분석해 봅시다

 

 

select deptno,
       case when deptno=10 then 1 else 0 end as deptno_10,
       case when deptno=20 then 1 else 0 end as deptno_20,
       case when deptno=30 then 1 else 0 end as deptno_30
  from emp
 order by 1

 

 

위의 코드로 아래와 같이 조회되는 것을 확인할 수 있습니다

위의 DEPTNO 컬럼은 가독성을 위한 값이니 참고하시기 바랍니다.

CASE WHEN THEN ELSE END를 활용해서 한 컬럼에 대해서 값을 출력합니다 

그 값은 간단합니다 DEPTNO가 10이면 1을 주고 나머지는 0을 부여하게 됩니다.

그리고 두 번째 컬럼은 DEPTNO가 20, 세번째 컬럼은 DEPTNO가 30으로

아래의 데이터가 조회됩니다  그러나 아직은 조금 미흡합니다

아직 행이 하나가 아니기 때문이지요

 

그럼 조금 수정해보겠습니다

 

통계쿼리3
case문으로 쿼리 조회

 

 

아래의 코드로 합계결과 값은 하나의 행으로 반환하였습니다

위의 결과 값에서 집계 함수 SUM을 사용하여 각 DEPTNO의 발생 횟수를 계산합니다

 

 

 select sum(case when deptno=10 then 1 else 0 end) as deptno_10,
       sum(case when deptno=20 then 1 else 0 end) as deptno_20,
       sum(case when deptno=30 then 1 else 0 end) as deptno_30
  from EMP;

 

아래와 같이 최종 결과가 출력됩니다 

 

 

통계쿼리4
최종 결과

 

 

하지만 아직 조금 부족합니다 그 이유는 한 테이블에서 CASE문을 전체를 적용해서 

시스템상 성능 부하가 올 수 있습니다 그리고 단순히 CASE 표현식을 합산하는 방법을 이기 때문입니다

지금은 데이터가 많이 없기 때문에 이 같은 방법을 해도 문제가 없지만 데이터가 몇만 건씩 넘어간다면

문제가 발생할 것입니다

다른 방법을 알아보도록 하겠습니다

 

 

select case when deptno=10 then empcount else null end as deptno_10,
  case when deptno=20 then empcount else null enD as deptno_20,
  case when deptno=30 then empcount else null end as deptno_30
from (
        select deptno, count(*) as empcount
          from emp
         group by deptno
     ) x;

 

 

아래의 코드를 자세히 보시면

처음에 COUNT 함수를 사용했던 것을 기억하실 것입니다

그 쿼리를 인라인 뷰로 사용하여 부서당 사원수를 생성합니다

그럼 그 결과 값은 아래와 같이 출력되는 것을 확인할 수 있습니다

 

이 방법은 인라인 뷰로 먼저 생성된 데이터가 3개의 행이므로

CASE문 실행할 경우 전체 행을 읽지 않고 3개의 행만으로

결과를 출력할 수 있습니다 

 

 

통계쿼리5
인라인 뷰 활용 예

 

그런 다음 MAX 함수로 열을 하나의 행으로 축소를 합니다

아래의 코드를 확인해 봅시다

 

select max(case when deptno=10 then empcount else null end) as new_deptno_10,
       max(case when deptno=20 then empcount else null end) as new_deptno_20,
       max(case when deptno=30 then empcount else null end) as new_deptno_30
from (
        select deptno, count(*) as empcount
          from emp
         group by deptno
     ) x;

 

결과 값은 아래와 같이 출력되는 것을 확인할 수 있습니다

 

 

통계쿼리6
성능강화 쿼리 적용 결과값

 

아래는 사례별로 통계 쿼리를 작성하는 것을 포스팅 연결하였습니다

그리고 마지막에는 통계쿼리 응용문제가 있으니

확인하셔서 공부하는데 도움이 되셨으면 합니다 감사합니다

 

 

2022.08.08 - [IT_Web/Oracle] - Oracle mybatis 활용 DECODE CASE GROUP BY ROLLUP 회원 통계 쿼리 월별 분기별 연도별

 

Oracle mybatis 활용 DECODE CASE GROUP BY ROLLUP 회원 통계 쿼리 월별 분기별 년도별

회원 월별 분기별 년도별 통계 쿼리 오늘은 회원별 통계 쿼리를 알아보도록 하겠습니다 mybatis를 활용해서 여러 조건문을 한번에 처리하는 쿼리를 작성하도록 하겠습니다 selectType을 프론트 단에

tantangerine.tistory.com

 

 

2022.08.09 - [IT_Web/Oracle] - oracle 회원 가입 증가 현황 DECODE 통계 쿼리 CASE, GROUP BY 활용

 

oracle 회원 가입 증가현황 DECODE 통계 쿼리 CASE, GROUP BY 활용

회원 가입 증가 현황 통계 쿼리 회원 가입 통계 쿼리를 만들어 보려고 합니다 저도 아직 익숙하지 않아서 조금 더 좋은 방법 있다면 댓글 남겨주세요~ 등록일(REG_DT)을 기준으로 신규회원 가입현

tantangerine.tistory.com

 

2022.08.10 - [IT_Web/Oracle] - oracle 상품 판매 통계 쿼리 union all case group by 활용

 

oracle 상품 판매 통계 쿼리 union all case group by 활용

상품 판매 통계 쿼리 오늘은 상품 판매 통계 쿼리를 만들어 보겠습니다 상품은 여러 종류가 있습니다 그것을 어떻게 구분할지는 스스로가 정해야겠지요 아래에서는 상품구분을 단순하게 알파

tantangerine.tistory.com

 


 

 


 

 

사례로 보는 통계 쿼리 문

* 아래의 문제는 쿼리문을 집중해서 보시고 이렇게도 쿼리를 작성할 수 있구나 하고

생각해보는 시간이 되었으면 합니다

 

 

 

 

HP라는 제품은 너무 많은 등급을 가지고 있어 제품별로 분석하기를 원하고, ‘LD’라는 제품은 등급의 수가 적고 중요하므로 등급별로 분석하기를 원하며, ‘PP’라는 제품은 다양한 등급을 가지고 있으나 ‘P530C’라는 등급은 전략적으로 관리하고자 하여 등급별로 분석하고, 나머지는 기타로 모아주기를 원한다고 생각하자

 

이런 경우에는 CASE와 GROUP BY를 활용하여 풀어보자

 

SELECT

  CASE

    WHEN 제품 = ‘HP’ THEN 제품

    WHEN 제품 = ‘LD’ THEN 등급 ,

    WHEN 등급 = ‘P530C’ THEN 등급

    ELSE ’기타‘

  END

  , SUM(총매출), SUM(총수량).....

FROM 매출테이블

WHERE 매출일자 LIKE ’9808%’

GROUP BY

  CASE

    WHEN 제품 = ‘HP’ THEN 제품

    WHEN 제품 = ‘LD’ THEN 등급 ,

    WHEN 등급 = ‘P530C’ THEN 등급

    ELSE ’기타‘

  END

 


 

SALE_HIST 테이블을 참조하여 ‘PENCIL’ 총판매금액과 01번 사업장의 총판매금액과

ERASER를 판매하고 있는 사업장의 총판매 금액을 출력하고 그 나머지를 기타로 묶어 총판매금액을 출력하시오

통계쿼리7
SALE_HIST 테이블 데이터 정보

 

SELECT

  CASE

    WHEN SALE_ITEM = 'PENCIL' THEN SALE_ITEM

    WHEN SALE_ITEM = 'ERASER' THEN SALE_SITE

    WHEN SALE_SITE = 01 THEN SALE_SITE

    ELSE '기타'

  END AS 특별관리항목

  , SUM(SALE_AMT) AS 총판매금액

FROM SALE_HIST

  GROUP BY

    CASE

      WHEN SALE_ITEM = 'PENCIL' THEN SALE_ITEM

      WHEN SALE_ITEM = 'ERASER' THEN SALE_SITE

      WHEN SALE_SITE = 01 THEN SALE_SITE

      ELSE '기타'

    END

 

통계쿼리8
상품별 총판매금액 통계쿼리

 

 


 

 

 

TEST100 테이블을 참조하여 C2에 해당하는 값의 비율을 구하여라

 

통계쿼리9
TEST100 테이블 데이터 정보

 

SELECT C1, C2

, ROUND(RATIO_TO_REPORT(C2) OVER(),2) * 100 AS PER

FROM TEST100

 

통계쿼리10
TEST100 상품별 비율

 


 

 

TEST35 테이블을 참조하여 KEY1은 부서라고 생각하고 KEY2의 A는 판매액 B는 매입액이라고 할 때

부서별 판매액과 매입액의 차에 대한 값(수익)을 구하고 차액의 비율을 구하시오

 

통계쿼리11
TEST35 테이블 데이터 정보

 

 

SELECT

  CASE WHEN KEY1 IS NOT NULL THEN KEY1 ELSE '합계' END AS KEY1

  ,  SUM(CASE WHEN KEY2 = 'A' THEN AMT ELSE 0 END) AS 판매액

  ,  SUM(CASE WHEN KEY2 = 'B' THEN AMT ELSE 0 END) AS 매입액

  ,  SUM(CASE WHEN KEY2 = 'A' THEN AMT ELSE -AMT END) AS 수익

  ,  ROUND(SUM(CASE WHEN KEY2='A' THEN AMT ELSE -AMT END)

                        / SUM(CASE WHEN KEY2='A' THEN AMT END)*100 ) AS PER

FROM (SELECT KEY1, KEY2, SUM(AMT) AMT

               FROM TEST35

               GROUP BY ROLLUP(KEY1),  KEY2

               )

GROUP BY KEY1;

 

 

통계쿼리12
부서 판매액 차액 비율

 


 

 

Temp를 이용한 문제를 풀어보자

Temp에 있는 직원들을 부서별 직급별로 SALARY 합을 보려 한다 이때 부서별로 직급들이 동일행에 나오도록

보여주고 부서별 전체 급여 그리고 PER컬럼의 전체 부서 급여 합계 비율을 구하여라

 

 

통계쿼리13
TEMP 테이블 데이터 정보

 

 

SELECT CASE WHEN DEPTNO IS NOT NULL THEN DEPTNO ELSE '합계' END DEPTNO

                 , SUM(A.부장) 부장, SUM(A.차장) 차장, SUM(A.과장) 과장

                 , SUM(A.대리) 대리, SUM(A.사원) 사원, SUM(A.수습) 수습, SUM(A.PER) 전체급여비율

                 , SUM(A.SAL) 부서별급여합계

FROM (SELECT DEPT_CODE DEPTNO, SALARY SAL

                               , SUM(CASE WHEN LEV = '부장' THEN SALARY ELSE 0 END) 부장

                               , SUM(CASE WHEN LEV = '차장' THEN SALARY ELSE 0 END) 차장

                               , SUM(CASE WHEN LEV = '과장' THEN SALARY ELSE 0 END) 과장

                               , SUM(CASE WHEN LEV = '대리' THEN SALARY ELSE 0 END) 대리

                               , SUM(CASE WHEN LEV = '사원' THEN SALARY ELSE 0 END) 사원

                               , SUM(CASE WHEN LEV = '수습' THEN SALARY ELSE 0 END) 수습

                               , ROUND(RATIO_TO_REPORT(SALARY) OVER (),2) * 100 AS PER

                               , SUM(SALARY)

              FROM TEMP

              GROUP BY DEPT_CODE, SALARY, LEV

              ) A

GROUP BY ROLLUP(DEPTNO)

ORDER BY DEPTNO

 

 

통계쿼리14
부서별 전체 급여 비율

 


 

 

TEST02 테이블의 자료를 이용해 최대 CRATE를 가지는 일자의 AMT와 최소 CREATE를 가지는 일자의 AMT를 읽어오는 문장을 출력하시오

 

통계쿼리15
TEST02 테이블 정보

 

 

SELECT distinct

  LAST_VALUE(AMT) OVER( ORDER BY CRATE

                                    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) MAX_VAL

  , FIRST_VALUE(AMT) OVER( ORDER BY CRATE

                                       ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) MIN_VAL

FROM TEST02

 

 

통계쿼리16
최대 최소 금액출력

 

                    


 

 

이렇게 통계 쿼리를 작성법을 알아보았습니다

조금이나마 도움이 되었으며 합니다

저도 통계 쿼리를 접할 때 많이 힘들었던 적이 있어서 이렇게 준비하게 되었습니다

그럼 앞으로 공부할 날이 너무나도 많기에 힘내시고 

같이 파이팅합시다

 

그럼 다음 포스팅도 기대해주세요!

 

728x90
반응형
728x90
반응형

이전 포스팅에서 웹크롤링 Puppeteer 및 Promise 활용 방법

 

Node.js로 웹 크롤링할 경우 Puppeteer를 사용하면 정말 편하게 개발을 할 수 있습니다

evaluate() 함수를 활용하여 잠시 대기시간을 갖거나,

선택자를 지정하여 태그에 대해 접근하여 값을 가져오거나 등 

여러 가지 작업을 간편하게 하였습니다

 

더 자세한 사항을 알고 싶으시다면 아래의 링크를 확인해주세요

 

 

2022.08.20 - [IT_Web/Nodejs] - nodejs 웹 크롤링을 Puppeteer 및 Promise 활용 완벽 파헤치고 CSV 파일 쓰기

 

nodejs 웹 크롤링을 Puppeteer 및 Promise 활용 완벽 파헤치고 CSV파일 쓰기

이전 포스팅에 대하여 이전 포스팅에서는 간단하게 웹 크롤링을 하는 방법을 알아보았습니다 axios와 cheerio를 사용해서 웹 크롤링을 해보았습니다 하지만 axios는 한계가 있습니다 싱글 페이지로

tantangerine.tistory.com

 

 

웹크롤링 이미지 다운로드 및 이미지 주소 엑셀에 입력하기

 

 

우선 이전 포스팅에서 계속 이어져서 작업을 진행하고있으니

이전 포스팅을 꼭 확인해주세요

 

아래의 코드를 한번 천천히 살펴보시길 바랍니다

그리고 세부 설명은 아래에서 이미지와 같이 진행하겠습니다

 

const axios = require('axios')
const fs = require('fs');
const puppeteer = require('puppeteer');

const xlsx = require('xlsx');
const add_to_sheet = require('./add_to_sheet');
const workbook = xlsx.readFile('xlsx/data.xlsx');
const ws = workbook.Sheets.Sheet1;
const xlsxRecords = xlsx.utils.sheet_to_json(ws)


fs.readdir('screenShot', (err) => { // readdir는 비동기로 이벤트 루프 속에서 작동하기 때문에 밑에 함수와는 관계가 없다
  if(err) {
    console.log('screenShot 폴더가 없어 screenShot 폴더를 생성합니다.')
    fs.mkdirSync('screenShot') // 프로그램이 처음 시작될때 한번만 시작할 때는 Sync는 사용해도 된다
  }
})


fs.readdir('poster', (err) => {
  if(err) {
    console.log('poster 폴더가 없어 poster 폴더를 생성합니다.')
    fs.mkdirSync('poster')
  }
})


const crawler = async () => {
  try {
    const browser = await puppeteer.launch({headless: false});
    console.log('첫번째: records', xlsxRecords)
    const page = await browser.newPage();
    await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36')
    add_to_sheet(ws, 'C1', 's', '평점')
    add_to_sheet(ws, 'D1', 's', '이미지 주소')
    for(const [i, r] of xlsxRecords.entries()) {
      await page.goto(r.링크);
      console.log('userAgent', await page.evaluate('navigator.userAgent'))
      const selectedTag = await page.evaluate(async () => {
        let score = ''
        let imgUrl = ''
        const scoreEl = await document.querySelector('.score.score_left .star_score')
        const imgEl = await document.querySelector('.poster img')
        if (scoreEl) {
          score = scoreEl.textContent;
        }
        if (imgEl) {
          imgUrl = imgEl.src;
        }
        return { score, imgUrl } 
      })

      if (!selectedTag?.score) {
        console.log('두번째 selectedTag 1: ',r.제목, `${i+1} 번째 영화 평점`, parseFloat(selectedTag.score.trim()));
        const newCell = 'C' + (i + 2)
        add_to_sheet(ws, newCell, 'n', parseFloat(selectedTag.score.trim()))
      } else {
        const scoreEl = await page.$('.score.score_left .star_score');
        if (scoreEl) {
          const selectedTag = await page.evaluate(tag => tag.textContent, scoreEl);
          if (selectedTag) {
            console.log('두번째 score 2: ', parseFloat(selectedTag.trim(), `${i+1} 번째 영화 평점`, parseFloat(selectedTag.trim())))
            const newCell = 'C' + (i + 2)
            add_to_sheet(ws, newCell, 'n', parseFloat(selectedTag.trim()))
          }
        }
      }
      // 이미지 주소로 요청을 받아서 이미지 데이터를 받는다
      // 이미지 데이터는 버퍼로 받는다 이buffer가 연속적으로 들어있는 자료구조가 arraybuffer라고 한다
      // 이미지 주소를 가져올때는 확인을 필히 해야하며 어떠한 규칙을 가지고 있는지 확인해야 한다 쿼리스트링을 없애는것이 답이 아니다
      if (!selectedTag?.imgUrl) {
        const imgInfo = await axios.get(selectedTag.imgUrl.replace(/\?.*$/,''), { // 정규표현식으로 쿼리스트링을 삭제한다
          responseType: 'arraybuffer',
        })
        fs.writeFileSync(`poster/${r.제목}.jpg`, imgInfo.data)
        console.log('두번째 imgUrl 2: ', selectedTag, `${i+1} 번째 영화 이미지 주소`, selectedTag);
        const newCell = 'D' + (i + 2)
        add_to_sheet(ws, newCell, 's', selectedTag)
      } else {
        const imgEl = await page.$('.poster img');
        if (imgEl) {
          const selectedTag = await page.evaluate(tag => tag.src, imgEl);
         
          if (selectedTag) {
            console.log('두번째 imgUrl 2: ', selectedTag, `${i+1} 번째 영화이미지 주소`, selectedTag);
            const newCell = 'D' + (i + 2)
            add_to_sheet(ws, newCell, 's', selectedTag)
            const imgInfo = await axios.get(selectedTag.replace(/\?.*$/,''), {
              responseType: 'arraybuffer',
            })
            fs.writeFileSync(`poster/${r.제목}.jpg`, imgInfo.data)
          }
        }
      }
      
      const setTime = await page.evaluate(async() => {
        const timeList = await [3975, 4547, 5231, 6397, 7894, 3394, 4206, 5147, 6932, 7430, 3561, 4896, 5877, 6407, 7133];
        const randomTime = await timeList[Math.floor(Math.random() * timeList.length)]
        await new Promise(r => setTimeout(r, randomTime))
        return randomTime
      })
      console.log('setTime', setTime)
    }
    await page.close();
    await browser.close();
    xlsx.writeFile(workbook, 'xlsx/xlsxRsult.xlsx')
  } catch (e) {
    console.error(e)
  }
}

crawler();

 

 

라이브러리를 우선 불러옵니다

세팅방법은 이전 포스팅을 참고해주시길 바랍니다

 

여러 개가 있으니 한번 확인해보세요!

 

라이브러리 불러오는 것은 그렇게 어려운 것이 없으니

설명은 생략하겠습니다

 

workbook.Sheets.Sheet1의 의미는 

제 엑셀 파일의 Sheet의 명이 Sheet1입니다

그래서 Sheet1의 데이터를 받아오게 됩니다

 

라이브러리-불러오기
필요 라이브러리 불러오기

 

 

 

 

 

이미지를 저장할 폴더가 있는지 없는지 체크해서 폴더를 생성합니다

fs.readdir은 비동기로 이벤트 루프 속에서 작동하기 때문에 밑에 crawler함수와는 관계가 없습니다

 

또 한, fs함수 중 Sync를 사용하면 안 된다고 알고 계시겠지만

프로그램이 처음 시작될 때 한 번만 실행할 함수라면 Sncy함수를 사용해도 무관합니다

 

 

 

폴더생성
폴더 생성

 

아래의 코드처럼 puppeteerlaunch()로 실행 준비를 하고

newPage()로 브라우저를 띄우게 됩니다

이때 setUserAgent()를 사용해서 나의 Agent정보를 지정할 수 있습니다

이 정보 값은 navigator.userAgent를 개발자 모드에서 콘솔 창에 적게 되면 노출되니 참고하세요

 

wssheet1의 데이터를 가져오며 형식이 들어있기 때문에

add_to_sheet()를 사용해서

엑셀 파일의 평점과, 이미지 주소를 먼저 적습니다

 

 

 

add_to_sheet()의 코드는 아래와 같습니다

설명은 생략하며 한번 분석해보시길 바랍니다!!

 

 

const xlsx = require('xlsx');

function range_add_cell(range, cell) {
  var rng = xlsx.utils.decode_range(range);
  var c = typeof cell === 'string' ? xlsx.utils.decode_cell(cell) : cell;
  if (rng.s.r > c.r) rng.s.r = c.r;
  if (rng.s.c > c.c) rng.s.c = c.c;

  if (rng.e.r < c.r) rng.e.r = c.r;
  if (rng.e.c < c.c) rng.e.c = c.c;
  return xlsx.utils.encode_range(rng);
}

module.exports = function add_to_sheet(sheet, cell, type, raw) {
  sheet['!ref'] = range_add_cell(sheet['!ref'], cell);
  sheet[cell] = { t: type, v: raw };
};

 

 

page.goto로 페이지 이동이 가능합니다

그리고 태그 접근을 하려면 두 가지 방법이 있습니다

우선 첫 번째 방법은 evaluate()에서 document접근이 가능하기에

querySelector()를 사용해서 태그에 접근해서 값을 가져옵니다

그 값들은 비구조화 할당으로 한 번에 객체로 return 합니다

 

 

evaluate함수-활용법
evaluate 함수의 활용법

 

위에서 받아온 값에서 score값이 있으면

바로 엑셀에 값을 적용합니다

두 번째 매개변수는 엑셀의 어디 부분에 값을 넣을지 지정하는 것입니다

세 번째 매개변수는 데이터의 자료형을 지정하는 것입니다

그래서 저는 C2부터 시작하며, 'n' number를 넣겠다고 지정한 것입니다

 

그리고 score값이 없으면

다시 두 번째 방법으로 받는 것을 적용해보았습니다

이 방법은 page.$() 활용해서 태그에 접근하는 element 값을 가져옵니다

 

그다음 evaluate() 함수를 사용해서 값을 가져와 할당합니다

 

 

 

태그-접근방법
두번째 태그 접근방법

 

 

이미지를 받기 위해서는 우선 axios.get으로 이미지 주소를 값으로 요청을 합니다

responseTypearraybuffer로 설정합니다

이미지 주소로 요청받은 데이터는 버퍼이며,

이 버퍼가 연속적으로 들어가 있는 자료구조가 arraybuffer입니다

 

그리고 엑셀에 이미지 링크 주소를 입력하고

이미지를 다운로드하여. jpg로 내 컴퓨터의 poster폴더에 저장합니다

 

 

 

이미지받기
이미지 받기

 

크롤링 로봇으로 보이지 않기 위해

여러 가지 타임을 배열로 넣어 setTime을 실행합니다

그래서 다음 페이지 작업의 시간 간격을 두어 진행하게 됩니다

 

 

잠시대기시긴설정
로봇으로 보이지않게 하기위한 함수

 

 

마지막 브라우저를 닫고 

엑셀 파일에 데이터를 넣은 정보 값을 할당하여

파일로 만들어서 종료합니다

 

엑셀파일생성
엑셀 파일 생성

 

 

콘솔 창에 노출된 여러 정보 값을 확인할 수 있으며

진행상황에서 확인된 setTimescore값 등을 확인할 수 있습니다

 

 

콘솔창-노출
콘솔창 정보값 노출화면

 

 

아래와 같이 파일 구조와

poster폴더에 jpg 파일이 생성된 것이 확인할 수 있습니다

 

 

이미지다운완료결과
이미지 생성된 파일들

 

 

엑셀 파일에 입력된 이미지 주소를 확인할 수 있습니다

 

 

 

이렇게 엑셀 쓰기와 웹 크롤링 작업을 알아보았습니다

여러 작업이 동반된 개발이라 개인적인 프로젝트를 해도 유용하다고 생각합니다

이것으로 무엇을 해볼까 생각 중입니다

아무튼 이렇게 공부를 끝내겠습니다

 

앞으로도 계속 공부를 해야 하는 입장에서 힘들겠지만

파이팅 하시길 바랍니다

 

그럼 다음 포스팅도 많은 관심 부탁드립니다

728x90
반응형
728x90
반응형

이전 포스팅에서 정규화 이론이 왜 중요한가?

 

데이터 이상 현상을 제거하기 위해 정규화 이론은 중요하다고 하였습니다

데이터 구조적인 측면에서 중복되는 데이터에 대해서 독립 개체

관리하기 위해서도 정규화 이론은 필요하다고 했습니다

 

그 밖에 또 무엇이 있을까요?

더 자세히 알고 싶다면 아래의 링크를 클릭해서 확인하시길 바랍니다

 

 

2022.08.22 - [Data_Modeling/Methodology] - 데이터 모델링 정규화 이론이 너무나도 중요한 이유는?

 

데이터 모델링 정규화 이론이 너무나도 중요한 이유는?

이전 포스팅 모델러의 마음가짐이란? 이전 포스팅에서는 데이터 모델러에 있어 마음가짐을 어떻게 해야 하는지에 대하여 알아보았습니다 스키마 구조와 모델을 보는 3가지 관점이 왜 중요한지

tantangerine.tistory.com

 

 

데이터-모델링-정규화
데이터 모델링 관계

 

정규화의 의의

 

데이터 모델링의 핵심이론인 정규화 이론은 몇 가지 특징을 가지고 있습니다

 

 

첫째, 속성 간의 종속성을 기준으로 성격이 유사한 속성들은 모이고 관계없는 속성들은 분리합니다

행위와 관계를 구분하고 본질을 파악하여 범주화하는 것이 핵심입니다

 

둘 때, 정규화는 함수 종속을 없애고 밀접한 속성을 하나의 표에 집약시키는 체계적인 방법입니다

따라서 데이터는 응집도는 높고 결합도는 낮게 분리합니다

결국, 데이터 본질에 충실한 제대로 된 엔터티가 도출될 수밖에 없습니다

 

셋째, 데이터 중복이 최소화효율적이고 구조화된 모델입니다

데이터 이상 현상이 사라지게 됩니다

 

넷째, 주식별자는 인스턴스를 구분하는 기준이므로 집합의 개체 발생 규칙도 검증되어 더 장확 해집니다

즉, 주 식별자만으로 엔터티가 어떻게 관계되었는지만 본다면

어떻게 데이터가 관리되고 저장되는지

규칙을 확인할 수 있습니다

 

다섯째, 업무 변경에 따른 확장성이 좋습니다

속성과 그에 따른 데이터가 엔터티 별로 독립 개체로 구성되어

범용성이 높게 구성됩니다

그래서 사용자가 새로운 업무를 추가할 때

확장성이 좋습니다 

 

여섯째, 데이터 중복을 최소화함으로써 데이터 무결성을 극대화합니다

정규화 과정을 거치면 데이터의 중복성을 제거합니다

그리고 독립 개체를 만들어 데이터 무결성을 보장할 것입니다

 

일곱째, 정규화된 모델은 그렇지 않은 모델에 비해 대부분 성능이 좋다.

이 부분은 동의를 못하는 사람이 있을 수 있다

정규화가 된 데이터 모델링은 테이블이 늘어나서

조인이 증가하여 성능이 저하된다고 생각할 수 있습니다

 

아래에서 더 자세히 설명하겠습니다

 



데이터모델링-정규화-성능저하
데이터 모델링 정규화 성능저하

 

 

 

정규화가 정말 성능 저하의 원인일까?

 

정규화를 하게 되면 한 개의 테이블이 두 개의 테이블로 분리해야 합니다

그렇게 되면 조인이 증가하기 때문에 성능이 저하될 거라고 합니다

 

정규화되지 않은 테이블에서는 중복된 데이터를 더 읽어야 하고

정규화가 된 테이블에서는 조건에 맞는 데이터만 읽어 빠르게 처리할 수 있습니다

 

하지만

조인이 필요하다는 것은 변화가 없지요

 

인덱스만 정확히 정의되어 있다면 성능상의 차이는 미미할 것입니다

그리고 테이블의 한 행이 2K고 한 블록의 크기가 8K라면 한 블록에서 4개의 개체가 담길 수 있습니다

DBMS는 한 블록, 즉 IO의 최소 단위가 블록이기 때문에 8K를 메모리로 올립니다

그래서 SQL 최적화할 경우 조회할 레코드 수가 아닌 블록의 수가에 집착하는 이유도 바로 이것 때문입니다

 

그렇기 때문에

정규화가 IO의 대상이 되는 블록수를 줄여줄 수 있는 이야기가 됩니다

모든 속성이 한 개의 테이블에 담겨 있다면 조회하는 속성의 수

많고 적음에 상관없이 항상 전체 블록을 읽어야 합니다

 

그러나 정규화가 잘되어 있다면 훨씬 적은 블록을 읽고도 원하는 결과를 얻을 수 있습니다

물론 조인이 많이 걸려 여러 테이블을 조회하는 것이 시간 소요가 발생할 수 있지만

만약 정규화가 된 여러 테이블이 정규화하지 않은 한 개의 테이블로 관리한다면

그것 또한 관리하는 입장에서는 아찔할 수밖에 없습니다

 

OLTP에서 조인에 의한 성능 저하가 극심한 경우라면 조인의 방법이 잘못되었을 경우가 큽니다

인덱스가 없거조인 연결이 이상하게 되어 옵티마이저가 인덱스를 사용하지 못하는 등의 문제지

조인 자체가 문제의 본질인 경우는 드물기 때문입니다

 

그리고 최근에는 컴퓨터 성능이 좋아져 이전만큼 성능 저하를 많이 느끼지 못합니다

그래서 정규화 때문에 성능 저하라고 하는 것은 조금 아이러니하지요

관리하는 측면에서 정규화는 매우 중요하다는 것이며,

정규화 때문에 성능 저하가 아닌 다른 문제 해결이 정답일 것입니다

데이터 구조적 측면에서 이득이라고 생각합니다

전체적인 것을 본다면 정규화는 대부분의 경우에서 더 뛰어난 성능을 보입니다

 

 

 

프로젝트 성패를 결정짓는 데이터 모델링 이야기김상래 지음

 

 

 

이제 정규화라는 것에 대해 조금은 알 수 있었으면 합니다

몇 개의 포스팅에서 지긋지긋하게 다뤘기 때문에

이제는 조금 익숙해지지 않았을까 합니다

저도 이번 기회에 조금 더 공부하는 계기가 되었습니다

 

IT공부는 즐거울 때도 정말 지루할 때도 있지만

공부하고 그 결과가 바로바로 올 수 있는 몇 안 되는 직업이라고 생각합니다

능력만 있으면 인정받을 수 있으니 더 힘을 내 봅시다

 

그럼 우리의 IT 대모험은 끝나지 않았으니

파이팅 하시길 바랍니다

 

 

 

 

 

728x90
반응형
728x90
반응형

이전 포스팅 모델러의 마음가짐이란?

 

이전 포스팅에서는 데이터 모델러에 있어 

마음가짐을 어떻게 해야 하는지에 대하여 알아보았습니다

스키마 구조와 모델을 보는 3가지 관점이 왜 중요한지

사용자 관점에서만 모델링을 하다보면

불필요한 공수가 발생하여 장기간 운영해야 하는 서비스에서는

너무나도 불합리한 것이었지요?

 

더 궁금한 것이 있다면

아래의 링크를 클릭해서 확인하시면 되겠습니다

 

 

2022.08.16 - [Data_Modeling/Methodology] - 데이터 모델링의 중요한 마음가짐이란 무엇일까?

 

데이터 모델링의 중요한 마음가짐이란 무엇일까?

이전 포스팅에서 데이터 모델링의 기본적인 스키마 구조와 모델을 보는 3가지 관점이 왜 중요한지 알아보았습니다 각 각의 스키마 구조에 해당하는 중요한 작업들이 있었습니다 사용자가 바라

tantangerine.tistory.com

 


 

데이터 모델링 정규화 이론의 중요성

 

데이터 모델링에 있어 정규화 과정은 어느 책에든 강좌를 봐도

빠지지 않고 이야기하는 이론입니다

 

그 이유는 2차 원표(DBMS)에 어떤 데이터를 어떻게 담는 것이 최적인지 고민하는 과정입니다

그 고민에 어떻게 담는 것이 가장 적합한지 알아보기 위해서는 

정규화 이론이 가장 이상적이기 때문입니다

 

구조적으로 데이터 이상 현상이 존재하지 않는 데이터 구조가 이상적입니다

 

데이터이상현상사례
데이터 이상 현상 사례

 

하지만 위와 같이 데이터가 구조가 구축되어있다면,

아래와 같이 문제점이 발생합니다

 

- 평가 코드 결과가 홍길동이라는 학생에 대한 정보는 입력이 불가능합니다

- 과목 코드가 CR11인 과목의 이름이 '심리학 입문'으로 변경되면

박영진과 김영희의 CR11 관련 열을 모두 찾아서 수정해야 합니다

- 김영희 F학점 평가 결과를 삭제하면 논리학 개론 과목도 함께 삭제됩니다.

 

 

데이터 이상 현상은 속성의 값을 수정할 때나 새로운 개체를 삽입하거나 삭제할 경우 의도하지 않은

다른 데이터에 문제가 발생하는 현상입니다

 

그런 데이터 이상 현상이 발생하는 데이터 구조를 가지고 있다면,
이상 현상의 원인은 데이터가 독립적이지 않고 중복으로 관리되어

데이터 간의 종속성에 계속 영향을 받기 때문입니다

 

하나의 종속성이 하나의 표로 관리되도록 분해해가는 과정이 바로 정규화입니다

 

즉, 종속성을 기준으로 데이터를 어떻게 담는 것이 최적인가에 대한 방법론이 바로 정규화 이론입니다 

 


 

 


 

1 정규화 적용 사례

* 1 정규화에서는 모든 속성이 값을 반드시 하나만 가져야 합니다

 

 

자격증 정보를 사원에 종속된 정보가 아니라 독립된 개체 정보로 유일하게 관리하고자 합니다

아래와 같이 사원이 자격증을 여러 개 가지고 있다면 문제점이 발생한다

그래서 사원 보유 자격증을 만들어서 관리합니다

하지만 이것 또한 문제점이 발생합니다

자격증별로 어떠한 이슈가 있을지 모르기 때문입니다.

 

1정규화적용사례
1정규화 적용 사례

 

그래서 아래와 같이 자격증을 독립적인 개체로 만들어주는 것이 좋습니다

그러면 범용적으로 자격증에 수장 지원 여부나 진급 시 필요한 필수 자격증 여부 등 여러 값들을 관리할 수 있습니다

그래서 1: N로 개체가 연결될 가능성이 있다면 독립 개체로 관리해야 합니다

 

 

독릭개체분리사례
독립개체 분리 사례

 

 


 

 

2 정규화 적용 사례

* 모든 속성이 반드시 주 식별자 전부에 종속되어야 합니다

* 1 정규화로 생성된 집합은 자식이며, 2 정규화로 분리된 집합은 부모가 됩니다.

 

아래와 같이 데이터가 구성되어있다면

2 정규화를 거쳐서 2개로 분리가 됩니다

그 이유는 단체명이 변경된다면 데이터 이상현상이 발생하기 때문입니다

 

이때 단체 번호에 종속된 속성을 전부 별도 엔터티로 분리를 합니다

즉, 계약 번호는 같지만, 계약 주체인 단체가 다를 경우에는 계약 내용 등이 다를 수 있음을 주식 별자가 설명하고 있습니다

이렇게 설명은 안되어있지만 엔터티만 보고도 계약과 단체의 데이터 관계를 이해할 수 있어야 합니다

 

분리된집합사례
주식별자중 <단체번호>에만 종소된 속성을 별도 엔터티로 분리


 

3 정규화 적용 사례

* 주 식별자가 아닌 모든 속성이 상호 종속 관계여서는 안됩니다.

 

아래와 같이 <고객명>과 <고객등급> 속성은 <고객번호>에 함수적으로 종속되어 있습니다

오른쪽과 같이 고객 집합을 별도로 분리하여 <고객> 엔터티로 만드는 과정이 필요합니다

 

종속성별도분리
속성 간 종속성이 있으면 별도 엔터티로 분리한다

 

 

실제 현장 모델링에서 가장 근원적이고 기반이 되는 핵심사상이 바로 정규화 이론입니다

그리고 통상 4 정규화 이상의 모델은 폭넓게 사용되지 않고 있습니다

 

이렇게 데이터 이상 현상을 줄이기 위해서는 정규화 이론은 필수 이론이라고 할 수 있습니다

숙지가 잘 안 되더라도 반복적으로 보며 생각하면서

정규화에 대한 개념을 확실히 잡는 것이 중요합니다

 

그래서 엔터티를 보기만 해도 데이터의 관계성을 파악해서

모델러의 의도를 파악하는 것이 개발자로도 필요한 덕목이라고 생각합니다.

 

그럼 앞으로 언젠가는 모델러가 될 여러분의 미래를 생각하며

오늘도 한걸음 나아갑시다!

 

파이팅!!

 

프로젝트 성패를 결정짓는 데이터 모델링 이야기 中 김상래 지음

728x90
반응형
728x90
반응형

인터넷 강의로 공부하다보면...

 

전 새로운 분야에 대한 프로그래밍을 공부할 때는 책보다는 인터넷 강의를 즐겨보는 편입니다

인터넷 강의를 보다 먼가 더 심도있게 공부해 보고 싶으면 개인 사이드 프로젝트를 하면서

공식문서를 보면서 공부를 하지요

 

인터넷 강의는 정말 긴 강의라도 1~2주 정도면 끝이 나는 경우가 많기 때문입니다

그래서 살펴보는 보니 정말 많은 인터넷강의가 있었습니다

그래서 머가 좋은가 싶어서 

 

노마드 코더, 인프런, 스파르타 코딩 클럽, 패스트 캠퍼스

거의 모든 인터넷강의는 한 번씩 들어 본 것 같네요

 

수강한-강의
강의한 수강

 

그래서 오늘은 노매드 코더에 대해서 이야기를 해보려 합니다

노마드 코더는 콜롬비아인 니콜라스와 한국인 린으로 궁성 된 팀이며,

디지털 노매드로서의 삶을 공유하는 영상들을 유튜브에 올리기도 했는데 

코로나-19 팬데믹이 장기화되면서

둘은 현재 결혼해서 한국에 정착하며 살고 있습니다

 

그럼 본론으로 노매드 코더의 강사와 그가 가르치는 강의는 어떠할까요

같이 알아봅시다

 


 

반응형

 

 

노마드 코더 강의 장점 

우리나라에서 클론 코딩 강의 1세대 프로그래머가 아닐까 합니다

클론 코딩도 조금 다른 것이 실제 앱, 웹 사이트를 펼쳐두고 그것을 똑같이 만들어간다는 점입니다

그리고 다른 강의와는 달리 실무에 필요한 개발을 하면서 활용도가 높은 부분을 배울 수 있다는 것입니다

 

그리고 클론 코딩하면서 직접 그 웹 페이지에 들어가서 어떻게 구조를 잡고 있는지

확인하면서 그 사이트에 대한 구조를 가져와서 자기 것으로 만드는 과정도 중요하다고 생각합니다

 

강의화면

 

그리고 챌린지라고 하여 자기가 산 강의가 있으면 참여하여 동기부여를 키워준다는 효과가 있습니다

매일 어느 부분까지 강의를 듣고 과제를 주고 제출받아

수강생의 학습을 독려하게 합니다

그리고 챌린지를 성공적으로 마치면 할인 쿠폰을 제공합니다

 

기초 강의는 무료이며, 전문적인 강의는 유료로 구성되어있습니다

 

노매드 코더 강의 단점

강의 수강료가 비싼 편입니다

일반적인 강의료가 360,000원입니다

 

다른 강의 사이트와 비교한다면

상당히 비싼 가격이지만

챌린지를 할 수 있고 동기부여가 필요하다면 좋겠지만,

혼자서 공부가 가능하다면 비싼 가격 이긴 확실하죠

 

그리고 프로그래밍을 이제 막시 작한 사람들에게는

안 맞지 않을까 생각합니다

너무 신기술에 대한 사용하는 강좌들이 많고

어느 정도 우리나라에서 보편적인 스킬들을 배우고 나서

 신기술이 궁금해서 배우는 것은 좋겠지만

 

현재 시점에서 너무 신기술이라서

저는 초급 자라면 비추천합니다

 

 

비추천한다고 하지만

어디까지나 이것도 제가 생각하는 주관적인 생각일 것입니다

공부하는 입장에서 맞다면 좋은 것이지요

 

무료 강의를 보시고 맞는지 한번 확인해보시는 것이 좋겠네요

그럼 오늘은 여기까지 하고

 

언제나 발전하고 진취적인 개발자가 되기를 기원합니다

그럼 다음 포스팅도 기대해주시고

 

대모험을 위해 앞으로 나아갑시다!

 

728x90
반응형
728x90
반응형

이전 포스팅에 대하여

 

이전 포스팅에서는 간단하게 웹 크롤링을 하는 방법을 알아보았습니다

axioscheerio를 사용해서 웹 크롤링을 해보았습니다

하지만 axios는 한계가 있습니다

싱글 페이지로 만들어진 웹페이지는 axios로 정보를 받아올 수 없다는 점이

그 한계입니다 그래서 좀 더 강력한 라이브러리를 사용하여 웹크롤링을 하려 합니다

 

그래도 cheerio는 간단한 웹페이지를 크롤링할 때에는 

라이브러리가 가볍기 때문에 상황에 맞게 사용한다면 좋을 듯합니다

 

더 자세한 사항은 아래의 링크를 클릭해주시길 바랍니다

 

 

2022.08.18 - [IT_Web/Nodejs] - 웹 크롤링 axios 및 cheerio 활용해서 간단하게 적용해보기

 

웹 크롤링 axios 및 cheerio 활용해서 간단하게 적용해보기

이전 포스팅에서는 엑셀과 CSV 파일에 저장되어 있는 데이터를 불러오는 것을 알아보았습니다 다른 타 부서나 프로젝트진행에 필요한 파일들이 저장된 데이터를 가져오기 위해 엑셀 및 CSV 파일

tantangerine.tistory.com

 

 

지금 웹 크롤링 관련 포스팅이 이어지다 보니 

기존 소스코드를 사용하는 경우가 많으니 이전 포스팅에서 

어떻게 진행되었는지 꼭 확인하시고 오시길 바랍니다

 

그럼 CSV 파일의 정보를 가져와서

웹크롤링을 하도록 하겠습니다

CSV 파일에는 영화 제목과 url 링크 주소가 함께 있습니다

 

파일 구조와 처음 세팅이 궁금하시다면

아래의 링크를 확인해주세요

 

2022.08.17 - [IT_Web/Nodejs] - node.js로 엑셀 및 CSV 파일 데이터 간단하게 가져오기

 

node.js로 엑셀 및 CSV 파일 데이터 간단하게 가져오기

node.js 관련은 처음 소개하는 것 같습니다 다음에 node.js로 서버 구축하는 것도 알아볼 테니 많은 관심 부탁드립니다 우선 node 다운로드 링크 주소는 사이드바에 생성해 놓을 테니 확인해 보시길

tantangerine.tistory.com

 


 

Puppeteer 및 Promise 활용해서 웹 크롤링하기 

 

 

puppeteer의 공식문서는 아래의 링크를 확인해보세요!!

https://github.com/puppeteer/puppeteer/blob/v16.2.0/docs

 

GitHub - puppeteer/puppeteer: Headless Chrome Node.js API

Headless Chrome Node.js API. Contribute to puppeteer/puppeteer development by creating an account on GitHub.

github.com


 

이제 본론으로 들어가서

웹 크롤링을 시작해 봅시다

먼저 아래와 같이 라이브러리를 설치합니다

 

 

npm i puppeteer

 

 

 

아래와 같이 코드를 작성합니다

새로운 함수들과 부가적인 설명이 필요할 것 같습니다

 

 

const puppeteer = require('puppeteer');

const crawler = async () => {
    const browser = await puppeteer.launch({headless: false}); // 웹브라우저 노출여부
    const page1 = await browser.newPage() // 페이지 노출하기
    const page2 = await browser.newPage() // 탭으로 페이지가 여러개 노출할 수 있습니다
    const page3 = await browser.newPage()

    await page1.goto('https://naver.com'), //페이지 이동
    await page2.goto('https://google.com'),
    await page3.goto('https://www.daum.net')


    await page1.evaluate(async() => { // 그리고 잠시대기
      await new Promise(r => setTimeout(r, 3000))
    }),
    await page2.evaluate(async() => {
      await new Promise(r => setTimeout(r, 3000))
    }),
    await page3.evaluate(async() => {
      await new Promise(r => setTimeout(r, 3000))
    })

    await page1.goto('https://n.news.naver.com/mnews/ranking/article/437/0000310583?ntype=RANKING&sid=001'),
    await page2.goto('https://newslibrary.naver.com/search/searchByDate.naver'),
    await page3.goto('https://google.com')

    await page1.evaluate(async() => {
      await new Promise(r => setTimeout(r, 3000))
    }),
    await page2.evaluate(async() => {
      await new Promise(r => setTimeout(r, 3000))
    }),
    await page3.evaluate(async() => {
      await new Promise(r => setTimeout(r, 3000))
    })
    
    await page1.close(), // 탭 페이지 닫기
    await page2.close(),
    await page3.close()
  	await browser.close(); // 브라우저 닫기
}

crawler();

 

 

 

웹 크롤링일반적으로 비동기를 사용해야 합니다

그 이유는 브라우저를 띄우고 로그인을 하고 그런 일괄된 작업들이 진행되면서 순차적으로 작업을 

진행해야 작업이 꼬이지 않기 때문입니다

 

예로 브라우저를 띄우고 로그인을 할 경우 브라우저가 노출이 되지도 않았는데

로그인을 하려고 한다면

에러를 발생하기 때문입니다

 

그래서 puppeteer를 사용하면서 얼마나 비동기를 자유자재로 사용하는지가

가중 중요한 핵심이라고 할 수 있습니다

 

이제 코드적인 설명을 해보겠습니다

아래와 같이 launch() 함수에 headless: false가 적용되어있습니다

웹브라우저를 노출해서 웹 크롤링하는 작업을 볼 수 있습니다

 

개발 초기에는 false로 적용해서 개발을 하면서

수정사항을 고쳐나갈 것입니다

 

웹크롤링-웹페이지-노출
웹크롤링을 할 경우 웹페이지 노출에 대한 여부

 

실제 프로젝트를 진행한다면 false값에

아래와 같이 적용해야 할 것입니다

그럼 env의 상태로 개발, 운영을 구분하여 웹페이지를 노출하게 될 것입니다

 

환경설정활용-웹브라우저노출여부
환경설정 값을 활용해서 웹브라우저 노출여부를 지정한다

 

 

그리고 페이지를 이동하고 나서

잠시 대기하는 시간을 갖습니다

예전에 puppeteer.waitFor을 사용하였습니다

하지만 버전이 올라가면서 해당 함수가 삭제되어서

아래와 같이 구현할 수 있습니다

 

태그에 접근할 정보가 있다면

evaluate()함수는 tagvalue값에 접근이 가능합니다

하지만 콜백함수를 활용해서 비동기로 setTimeout함수를 사용해서

함수 실행을 딜레이 합니다

 

그리고 잠시 대기하는 시간이 왜 필요한지도 궁금하실 수 있습니다

그 이유는 자동으로 웹크롤링을 진행하게 되면

속도가 엄청 빠르다는 것입니다

그럼 웹페이지를 운영하는 입장에서는

비정상적인 동작이라고 생각하기 때문에

우리에게 제재를 줄 수 있습니다

 

비정상적인 방법이라는 인식을 피하기 위함입니다

 

웹크롤링-잠시대기
웹크롤링 잠시 대기

 

 

이렇게 간단한 함수들을 정리해보았습니다

하지만 지금 이 방법은 순차적으로 진행하다 보니

소 시간이 소요됩니다

만약 더 많은 작업을 한다면 소요되는 시간은 더 클 것이라고 생각합니다

 

그래서 이제 프로미스를 적용해서 한방에 요청하고

순차적으로 가 아닌 먼저 도착한 순서대로 진행하여

작업 시간을 단축해보도록 하겠습니다


 

반응형

 


 

Promise.all을 활용해서 웹크롤링 시간 단축 하기

 

아래와 같이 코드를 변경합니다

Promise.all을 사용한 것이 보이시나요?

 

함수마다 Promise.all을 적용해서 동시 작업을 진행합니다

이때 비동기이지만 한 번에 작업할 때마다 동시에 한다는 점입니다

그래서 전체적으로 볼 때는 비동기라는 것을 인지해 주세요 

 

부가적인 설명은 필요가 없을 것 같습니다

 

그럼 이제는 CSV 파일을 읽어서 그 정보를 바탕으로 웹 크롤링을 해보겠습니다

 

const puppeteer = require('puppeteer');

const crawler = async () => {
  const browser = await puppeteer.launch({headless: process.env.NODE_ENV === 'production'});
  const [page1, page2, page3] = await Promise.all([
    browser.newPage(),
    browser.newPage(),
    browser.newPage()
  ])
  await Promise.all([
    page1.goto('https://naver.com'),
    page2.goto('https://google.com'),
    page3.goto('https://www.daum.net')
  ]);
  await Promise.all([
    page1.evaluate(async() => {
      await new Promise(r => setTimeout(r, 3000))
    }),
    page2.evaluate(async() => {
      await new Promise(r => setTimeout(r, 3000))
    }),
    page3.evaluate(async() => {
      await new Promise(r => setTimeout(r, 3000))
    })
  ]);
  await Promise.all([
    page1.goto('https://n.news.naver.com/mnews/ranking/article/437/0000310583?ntype=RANKING&sid=001'),
    page2.goto('https://newslibrary.naver.com/search/searchByDate.naver'),
    page3.goto('https://google.com')
  ]);
  await Promise.all([
    page1.evaluate(async() => {
      await new Promise(r => setTimeout(r, 3000))
    }),
    page2.evaluate(async() => {
      await new Promise(r => setTimeout(r, 3000))
    }),
    page3.evaluate(async() => {
      await new Promise(r => setTimeout(r, 3000))
    })
  ]);
  await Promise.all([
    page1.close(),
    page2.close(),
    page3.close()
  ]);
  await browser.close();
}

crawler();

 


 

CSV 파일을 읽어서 웹크롤링 시작하기

 

CSV 파일을 읽기 위해서는 라이브러리가 필요합니다

그리고 초기 세팅이 필요하겠지요

그것은 위에 링크를 걸어 두었던 것 기억하시지요?

그것을 참고하시길 바랍니다

 

아래와 같이 코드를 작성합니다

 

 

const parse = require('csv-parse/lib/sync');
const stringify = require('csv-stringify/lib/sync') 
const fs = require('fs');
const puppeteer = require('puppeteer');

const csv = fs.readFileSync('csv/data.csv');
const records = parse(csv.toString('utf-8'))

const crawler = async () => {
  const resultScore = []
  try {
    const browser = await puppeteer.launch({headless: false});
    console.log('첫번째: records', records)
    // Promise.all을 활용해서 csv에서 받아온 records를 모든 배열의 정보를 동시에작업을 진행합니다
    await Promise.all(records.map(async (r, i) => {
        const page = await browser.newPage();
        await page.goto(r[1]);
        const scoreEl = await page.$('.score.score_left .star_score');
        if (scoreEl) {
          const text = await page.evaluate(tag => tag.textContent, scoreEl);
          if (text) {
            console.log('두번째: ', r[0], '평점', text.trim());
            resultScore.push([r[0], r[1], text.trim()])
            // resultScore[i] = [r[0], r[1], text.trim()]
          }
        }
        await page.evaluate(async() => {
          await new Promise(r => setTimeout(r, 3000))
        })
        await page.close();
    }));
      await browser.close();
      const str = stringify(resultScore);
      console.log('3번째: ', str)
      fs.writeFileSync('csv/result.csv', str);
  } catch (e) {
    console.error(e)
  }
}


crawler();

 

 

위의 코드에서

Promise.all을 사용해서 recodes.map을 매개변수 사용해서

한 번에 작업을 진행합니다

그리고 콘솔 창에는 아래와 같이 확인되었습니다

그래서 링크 주소를 활용해서 웹 크롤링을 시작합니다

 

콘솔데이터정보
콘솔 데이터 정보 노출 상황

 

 

 

개발자 모드에서 아래와 같이 클래스명을 확인해서

데이터 값을 가져올 수 있는 위치를 확인합니다

 

 

개발자모드-선택자찾기
개발자모드 선택자찾기

 

그래서 아래와 같이 클래스명을 기입해서

그 해당하는 textContent를 가져옵니다

이때 필요한 함수는 page.evaluate를 활용합니다

 tag.textContentscoreElreturn 받아

 

text 변수에 값이 있으면 push를 해서 최종 값을 저장합니다 

 

 

데이터-가져오기
데이터 가져오기

 

 

 

그리고 웹 크롤링해온 평점을 다시 CSV 파일에 넣어보도록 하겠습니다

먼저 라이브러리를 설치합니다

 

 

npm i csv-stringify

 

 

이전에 웹 스크롤링을 해서 저장된 데이터를

stringify함수를 사용해서 간단히 String으로 만듭니다

그래서 CSV 파일 형식으로 다른 이름으로 저장합니다 

 

 

CSV파일-웹크롤링정보쓰기
웹크롤링 정보 CVS파일 쓰기

 

그 파일은 아래와 같습니다

하지만 문제가 있습니다

첫 번째 콘솔에 정렬된 순서와 저장되고 나서의 순서가

다르다는 것입니다

 

협업을 하다 보면 파일의 정보 순서가

정말 중요할 경우가 있습니다

 

저장된 CSV파일
저장된 CSV파일

 

위의 코드에서 주석 부분을 변경해서 i를 사용합니다

i는 인덱스 넘버가 저장되어있습니다

초기 배열의 저장된 순서가 i에 저장되어있어서

정렬순서가 보존될 수 있습니다

 

정렬순서보존
map의 인덱스 넘버를 활용해서 순서보존하기

 

 

 

 

아래는 웹 크롤링할 경우에 노출되는 웹페이지입니다

10개가 동시에 열려있는 것이 보이시나요?

 

 

웹크롤링-브라우저모습
웹크롤링 브라우저 모습

 

 


 

이렇게 웹 크롤링을 알아보았습니다

아직 여러 함수들을 사용하지 않고 정말 기본적인 것을 활용해서

웹크롤링을 해보았습니다

 

다음 포스팅에서는 더욱 심도 있는 기능으로 찾아뵙겠습니다

우리 IT의 대모험은 끝나지 않았습니다

 

끝나는(?) 그날까지 파이팅 하세요

 

 

728x90
반응형
728x90
반응형

 

 

이전 포스팅에서는 엑셀과  CSV 파일에 저장되어 있는 데이터를 불러오는 것을 알아보았습니다

다른 타 부서나 프로젝트진행에 필요한 파일들이 저장된 데이터를 가져오기 위해

엑셀 및 CSV 파일 파싱을 해서 가져와서 데이터를 핸들링하는 방법을 알아보았습니다

 

더 상세히 알고 싶으시면 아래의 링크를 클릭해서 확인해보시길 바랍니다

 

2022.08.17 - [IT_Web/Nodejs] - node.js로 엑셀 및 CSV 파일 데이터 간단하게 가져오기

 

node.js로 엑셀 및 CSV 파일 데이터 간단하게 가져오기

node.js 관련은 처음 소개하는 것 같습니다 다음에 node.js로 서버 구축하는 것도 알아볼 테니 많은 관심 부탁드립니다 우선 node 다운로드 링크 주소는 사이드바에 생성해 놓을 테니 확인해 보시길

tantangerine.tistory.com

 

 

웹 크롤링을 간단하게 해 보자

 

 

그럼 바로 라이브러리부터 설치해보겠습니다

 

 

npm i axios cheerio

 

 

아래의 코드도 추가합니다

파일 구조나 시작점은 위의 링크로 들어가셔서 처음에 어떻게 시작했는지 확인해 보시길 바랍니다

이전 포스팅과 연결되는 부분이 조금 있어서 꼭 한번 들어가서 확인하시고

다시 돌아오세요~

 

 

const xlsx = require('xlsx');

const workbook = xlsx.readFile('xlsx/data.xlsx');
//console.log('readFile: ', Object.keys(workbook.Sheets))

const ws = workbook.Sheets.Sheet1; // 시트 명을 
//console.log('resultSheet: ', ws)

const wsRecods = xlsx.utils.sheet_to_json(ws); // 시트에 담긴 데이터를 json파일로 변경해준다
//console.log('resultWsRecods: ', wsRecods)


const axios =  require('axios');
const cheerio = require('cheerio');

const crawler = async () => {
  for (const [i, r] of wsRecods.entries()) {
    console.log('링크', r.링크)
    const response = await axios.get(r.링크);
    if(response.status === 200){
      const htmlStr = response.data;
      console.log('htmlStr: ', htmlStr)
      const $ = cheerio.load(htmlStr) // cherrio는 태그를 접근할 수 있습니다
      const text = $('.score.score_left .star_score').text(); // 선택자로 직접적인 class명에 접근이 가능합니다
      console.log(r.제목, '평점: ', text.trim());
    }
  }
}

 

데이터링크
링크가 들어있는 모습

첫 번째 콘솔에 링크를 보시면

현재 엑셀 파일에 받아온 데이터가 링크 주소인 것을 확인할 수 있습니다

그 주소를 불러와서 axios.get()의 함수를 사용해서

링크를 매개변수로 할당하여 데이터 값을 받아옵니다

그렇게 되면 responsehtml이 문자열로 들어오게 됩니다

 

html문자열을 콘솔 창에서 확인을 하면,

아래와 같이 html문자열을 확인할 수 있습니다

이 문자열을 활용하여 정보를 가지고 옵니다

 

html문자열
html 문자열을 확인할 수 있다

 

 

반응형

 

 

 

아래의 두줄의 코드를 보시면 cheerio를 활용하여 태그에 접근 가능하며,

이때 jQery를 사용해서 선택자의 클래스명에 접근이 가능합니다

이때 선택자는 어떻게 지정하느냐가 중요합니다

 

두 번째 줄에 보시면. score.score_left. star_score 클래스 명을 사용해서

 jQery함수인. text()를 사용해서 값을 받아오고 있습니다

 

 

 

F12로 개발자 모드 창을 띄워서 

아래의 빨간색 테두리에 있는 것을 클릭해서 웹페이지의 태그를 알아볼 수 있습니다

 

 

개발자모드창
개발자 모드 창

 

 

 

그리고 아래의 이미지처럼

보고 싶은 웹사이트의 부분을 클릭하면 됩니다

 

개발자모드
개발자모드

 

 

 

아래의 이미지를 개발자 모드 창에서 확인할 수 있습니다

 

 

개발자태그코드
개발자 모드의  html

 

그럼 text()를 사용하여 그 선택자로 접근한 태그의 textContent를 가져오게 됩니다

그렇게 되면 아래와 같이 결과가 출력됩니다

 

 

 

평점출력결과
평점출력결과

 

 

지금은 for of문과 await 사용하여 순차적으로 요청하는 모습을 볼 수 있습니다

하지만 이것은 한 개씩 요청하고 받기 때문에 시간이 다소 걸릴 수 있습니다

 

시간이 걸리는 것을 해결하기 위해서는 Promise를 사용하면

모든 요청을 한 번에 다 보내게되고 한번에 받게 됩니다

하지만 그렇게 되면 순서를 보장할 수 없습니다

 

그래서 이 두 가지를 생각하고 순서를 보장해야 할 경우에는 for of문 await을 같이 사용하고

그럴 필요가 없다면 Promise를 사용하는 것이 좋을 것입니다

 

 

Promise의 방법은 아래와 같습니다

 

const crawler = async () => {
  await Promise.all(wsRecods.map(async (r) => {
    //console.log('링크', r.링크)
    const response = await axios.get(r.링크);
    if(response.status === 200){
      const htmlStr = response.data;
      //console.log('htmlStr: ', htmlStr)
      const $ = cheerio.load(htmlStr) // cherrio는 태그를 접근할 수 있습니다
      const text = $('.score.score_left .star_score').text(); // 선택자로 직접적인 class명에 접근이 가능합니다
      console.log(r.제목, '평점: ', text.trim());
    }
  }))
}

 

 

지금까지 간단한 axioscheerio 활용하면,

간단한 웹 크롤링을 할 수 있습니다

그래서 다른 중요한 웹 페이지들은 cheerio 활용하면 웹 크롤링을 할 수 없습니다

이제 오늘은 간단한 웹크롤링을 배워 보았습니다

 

cheerio 말고 더 강력한 라이브러리를 사용해서 웹 크롤링해보도록 하겠습니다

그럼 오늘도 열심히 하시고

 

다음 포스팅도 기대해주십시오!

 

 

 

 

 

728x90
반응형
728x90
반응형

node.js 관련은 처음 소개하는 것 같습니다

다음에 node.js로 서버 구축하는 것도 알아볼 테니 많은 관심 부탁드립니다

 

우선 node 다운로드 링크 주소는 사이드바에 생성해 놓을 테니 확인해 보시길 바랍니다

그리고 설치방법은 구글 검색으로 간단히 해결할 수 있습니다

그러니 그것은 그냥 스킵하도록 하겠습니다

 

그럼  아래와 같이 폴더를 생성해주시고

 

파일구조
파일구조

아래와 같이 lecture 폴더 안에서 

npm init을 하시길 바랍니다

npm설정
npm 설정하기

입력하게 되면 몇 가지 질문사항을 확인하실 수 있습니다

 

package name 패키지 명은 폴더 이름이 기본값입니다.

version는 패키지 버전입니다. (1.0.0)는 기본 값입니다.

description는 패키지에 대한 설명입니다. 

entry point는 시작 파일 명입니다. (index.js)는 기본 값입니다.

test commandnpm test를 호출할 때마다 실행되는 명령입니다.

git repository는 패키지가 저장되어 있는 Git 저장소의 URL입니다.

keywords는 패키지의 키워드입니다.

author는 원작자의 이름입니다. 여러분의 이름이나 아이디를 입력하면 됩니다.

license는 패키지 사용에 대한 라이선스입니다. (ISC)는 기본 값입니다.

 

모든 질문사항을 기본값으로 설정해도 무관합니다

 

그럼 위와 같이 package.json파일이 생성됩니다

그리고 package.json파일 내부의 코드를 변경합니다

 

package.json 구조
package.json 구조

 

npm start를 할 경우에 index를 실행시킨다는 의미입니다

그리고 추후에 라이브러리를 설치하지만 csv-parse, xlsx의 버전을 확인해주시길 바랍니다

버전이 다르면 현재 제가 하는 것과 다르게 사용해야 하니 주의해 주세요

 

 


 

csv파일 파싱 해서 데이터 가져오기

CSV 파일이란 무엇일까요?

Comma Separated Value라고 합니다

한마디로 콤마로 이루어진 파일이라고 할 수 있습니다

 

아래와 같이 파일이 구성되어있지요

정확히 말하면 콤마와 줄 바꿈 정도라고 생각하실 수 있습니다

타이타닉,https://movie.naver.com/movie/bi/mi/basic/nhn?code=18847
아바타,https://movie.naver.com/movie/bi/mi/basic/nhn?code=62266
메트릭스,https://movie.naver.com/movie/bi/mi/basic/nhn?code=24453
반지의 제왕,https://movie.naver.com/movie/bi/mi/basic/nhn?code=31794
어벤져스,https://movie.naver.com/movie/bi/mi/basic/nhn?code=72363
겨울왕국,https://movie.naver.com/movie/bi/mi/basic/nhn?code=100931
트랜스포머,https://movie.naver.com/movie/bi/mi/basic/nhn?code=61521
해리 포터,https://movie.naver.com/movie/bi/mi/basic/nhn?code=30688
다크나이트,https://movie.naver.com/movie/bi/mi/basic/nhn?code=62586
캐리비안해적,https://movie.naver.com/movie/bi/mi/basic/nhn?code=37148

 

그런데 여기서 잠깐 의문이 생길 수 있겠지요

왜 이런 작업이 필요할까?

 

우리가 프로젝트를 진행하다 보면 현업 즉 고객사와 협업을 많이 진행합니다

아니면 다른 타 부서 사람과도 협업을 하겠지요

그러다 보면 대량의 정보를 데이터 베이스에 저장을 한다거나,

이 정보에 해당하는 어떤 값을 저장해 달라고 하거나

여러 정보가 필요하게 됩니다

이때 몇 천 건의 데이터를 수기로 하기는 어려움이 있습니다

그래서 이러한 프로그램적으로 구현하여 손쉽게 하기 위함입니다

 

그럼 다시 본론으로 넘어와

csv 파싱 라이브러리를 설치합니다

우리는 현존에 개발해놓은 좋은 라이브러리를 잘 사용하기만 하면 됩니다

이거 가져다 쓴다고 흉이 아니니 잘 쓰고 빨리 개발하면 되겠습니다

 

npm i csv-parse

 

 

 

index.js에서 아래와 같이 코드를 작성합니다

CSV 파일을 파싱 할 때 주의해야 할 점은 버퍼 파일이기 때문에 인코딩을 해야 하는 것을 잊지 말아야 합니다

 

// npm 패키지 불러올때 사용하는 require함수입니다
// 또한 node_modules파일에서 경로를 그대로 따라가서 그 함수를 들고온다고 생각하면된다
// 노드서버를 만들어서 배포하는 것이 좋습니다
const parse = require('csv-parse/lib/index.js'); 
const fs = require('fs'); // 파일시스템 모듈을 불러온다
const csv = fs.readFileSync('csv/data.csv'); // 파일을 불러오는 함수

// 이때 불러오게된 csv에는 버퍼(Buffer)형식이라서 문자열로 변경해주어야한다
// 버퍼는 0,1로 이루어진 컴퓨터 친화적인 데이터이다
csv.toString('utf-8') // toString을 하면서 인코딩을 하는 것은 잊지말자

console.log('result: ', csv.toString('utf-8'));

const records = parse(csv.toString('utf-8'))
records.forEach((movieInfo, i) => {
  console.log('영화제목: ', movieInfo[0], '*** 영화링크: ', movieInfo[1]);
});

 

 

첫 번째 콘솔result에 해당하는 값을 확인해보면 아래와 같이 확인할  수 있습니다

아래는 단지 파일을 읽어온 결과 값입니다

 

csv파일읽은결과
csv파일읽은결과

 

두 번째 CSV 파일파싱 한 결과

영화 제목과 링크로 나누어 보이는 결과 값입니다

아래와 같습니다 

 

csv파일파싱결과
csv파일 파싱 결과

이제 데이터를 어떻게 사용해야 할지 감이 잡이시나요?

그럼 다음은 엑셀입니다


 

 

 

반응형

 

 


 

엑셀 파싱 해서 데이터 가져오기

 

엑셀 파일에 가져올 데이터를 채워서 파일로 준비합니다

 

엑셀파싱데이터
엑셀파싱데이터

 

 

엑셀 파싱 관련 라이브러리를 추가합니다

엑셀 파싱은 정말 혼자서 만들기 힘드니 라이브러리를 이용하시길 바랍니다

 

 

npm i xlsx

 

 

그리고 아래의 코드를 index.js에 작성하여 줍니다

그리고 npm start를 실행하게 합니다

 

const xlsx = require('xlsx');

const workbook = xlsx.readFile('xlsx/data.xlsx');
console.log('readFile: ', Object.keys(workbook.Sheets))

const ws = workbook.Sheets.Sheet1; // 시트 명(Sheet1)을 뒤에 붙여준다
console.log('resultSheet: ', ws)

// .sheet_to_json()함수로 시트에 담긴 데이터를 json파일로 변경해준다
const wsRecods = xlsx.utils.sheet_to_json(ws); 
console.log('resultWsRecods: ', wsRecods)

for (const [i, r] of wsRecods.entries()) {
  console.log(`${i}번 ${r.제목} ** ${r.링크}`)
}

 

첫 번째 콘솔readFile을 보시면 ['Sheet1']이 들어가 있는 것을 확인할 수 있습니다

그래서 workbooks.Sheet.Sheet1으로 파일을 받아올 수 있게 되는 것입니다

 

그리고 두 번째 콘솔resultSheet를 보시면 엑셀 파일의 데이터가 저장되어있는 것을 확인할 수 있습니다

엑셀의 데이터를 ws라는 변수에 저장해서  xlsx라이브러리를 사용해서 파싱 해서 데이터를 가져옵니다

 

 

 

 

세 번째 콘솔에 보시면 resultWsRecodswsRecods값을 보시면

아래와 같이 json형태로 데이터가 들어오는 것을 확인할 수 있습니다

그 이유는 xlsx.utils.sheet_to_json() 함수를 사용해서

편하게 json형태 데이터를 파싱 할 수 있게 된 것입니다

 

 

 

네 번째 콘솔에 for문을 활용해서 데이터 값을 가져올 수 있습니다

그 결과 값은 아래와 같이 노출됩니다

이렇게 간단하게 라이브러리를 사용해서 파싱을 하여

외부 데이터를 가져와 핸들링하는 방법을 알아보았습니다

 

 

 

가져온 데이터를 데이터베이스에 저장을 하거나,

다른 작업을 할 수 있겠지요?

 

그리고 중요한 것이 있습니다

웹 크롤링을 위해서 for문, forEach문을 익혀두셔야 합니다

자바스크립트는 싱글 스레드이기 때문에 크롤링할 때도 똑같이 적용됩니다

그래서 비동기로부터 벗어날 수 없어서 비동기를 조심하면서 크롤링을 해야 합니다

이때 forEach문을 사용할 때와 for문을 사용할 때의 차이가 극명하게 다르기 때문

비동기를 자유자재로 사용해야 크롤링된 데이터를 모아서 한방에 저장할 수가 있습니다

 

오늘도 유익한 정보들을 알아보았습니다

이렇게 공부를 하다 보면 언젠가는 눈감고도 할 수 있는 날이 오겠죠?

ㅎㅎ 그럼 오늘도 수고하셨고

우리의 대모험이 끝나는 날까지 힘내시고 파이팅하시길 바랍니다

그럼 다음 포스팅도 많은 관심 부탁드립니다

 

 

 

 

728x90
반응형
728x90
반응형

이전 포스팅에서 데이터 모델링의 기본적인 스키마 구조와

모델을 보는 3가지 관점이 왜 중요한지 알아보았습니다

 

각 각의 스키마 구조에 해당하는 중요한 작업들이 있었습니다

사용자가 바라보는 관점과 외부 스키마,

그 관점을 데이터 관점으로 통합한 뷰의 개념 스키마,

데이터 DBMS에 저장되는 논리적 구조의 내부 스키마

 

그 스키마들이 행하는 근본적인 기준을 제대로 알고 있어야

외부 스키마의 데이터를 개념 스키마를 무시한 채 내부 스키마까지 적용하는 일이 없어야 할 것입니다

그런 기준을 보다 넓은 사고력을 키워 줄 수 있는 시간였던 것 같습니다

궁금하시다면 아래의 포스팅을 클릭해 주시길 바랍니다

 

 

2022.08.14 - [Data_Modeling/Methodology] - 데이터 모델링의 기본적인 스키마 구조, 모델을 보는 3가지 관점이 왜 중요한 걸까?

 

데이터 모델링의 기본적인 스키마 구조, 모델을 보는 3가지 관점이 왜 중요한걸까?

이전 포스팅에서 데이터 모델링의 범주화와 추상화에 대해서 알아보았습니다 너무나도 범주화가 중요했지요 범주화는 본질을 정확하게 파악해서 성격이 유사한 개체로 묶는 과정이었습니다

tantangerine.tistory.com

 

 

 

이제 데이터 모델링의 마음가짐이란 무엇일까요?

 

데이터 베이스가 최상의 성능을 낼 수 있는 구조를 도출해야 합니다

단지 모델링을 단순히 데이터의 저장 구조를 그려내는 것이라고 생각해서는 안된다는 것입니다

그 궁극의 기반 이론이 정규화라는 것도 기억해두시길 바랍니다

 

주문서
주문서

 

정규화를 무시 한채 단순히 데이터의 저장구조를 그리게 된다면 큰 손실을 보게 될 것입니다

 

단적인 예로 이야기하도록 하겠습니다

주문서의 데이터를 사용자가 필요로 하는 형태로 조직화된 하나의 집합체로 보이지만

그 내부의 데이터는 연관성을 고려하여 여러 테이블의 구조로 이루어져 있습니다

 

하지만 외부 스키마에서 바라보는 정보들로 내부, 개념 스키마를 구성했다면 어떤 일이 벌어질까요?

 

그것은 주문서라는 하나의 테이블에 모든 속성을 모아 설게 한 경우가 될 것입니다

이렇게 설계가 된다면 중대한 문제점이 여럿 존재하게 됩니다

 

그중 세 가지를 살펴보겠습니다.

 

첫째, 나중에 주문서에 포함된 상품의 이름이 바뀌면

해당 상품과 관련된 주문을 모두 찾아서 상품명을 수정해야 합니다

 

둘째, 주문을 단 한 번도 하지 않은 고객은 관리 자체가 불가능합니다.

주문을 해야 주문서를 통해 주문 고객의 정보가 들어올 수 있는 구조 이기 때문입니다.

 

셋째, 주문한 상품을 배송하려면 주문 배송 주소 데이터를 참조해야 합니다

그러나 불필요한 다른 속성들 때문에 대량 주문 처리할 경우 시스템 성능에 악영향을 줄 수 있습니다.

 

이렇듯 우리가 데이터 모델링을 해야 한다면

먼저 테이블의 데이터를 어떻게 수정할 것인가?

저장된 데이터는 어떻게 관리하며 어떻게 재사용할 수 있을까?

지금 테이블의 구조로 시스템의 성능은 최상으로 유지할 수 있을까?

라는 의문점을 가지고 데이터 모델링을 진행해야 합니다.

 

프로젝트를 진행하다 보면 정말 많은 테이블 구조와 설계를 보게 됩니다

정말 한숨이 절로 나오는 설계도 있으며

정말 노력의 흔적이 보이는 모델링도 보게 됩니다.

이제 저 또하 데이터 모델링을 하게 된다면

누구에게 어떠한 시선으로 보일지 그것을 생각하며

제 스스로 떳떳할 수 있게 책임감을 가지고 나아가겠습니다.

 

그럼 공부하시는 IT 개발자 여러분 앞으로도 같이 파이팅 하시길 바랍니다.

그럼 다음 포스팅에서 뵙겠습니다.

 

오늘도 보람찬 하루가 되었길 바라겠습니다.

 

 

 

 

 

 

728x90
반응형
728x90
반응형

이전 포스팅에서는 구글 파이어 베이스의 Cloud FireStore에

데이터를 추가하고 Storagedp 파일도 같이 저장하는 방법을 알아보았습니다 

 

더 자세한 내용은 아래의 포스팅을 참고하세요

 

 

2022.07.31 - [Server/Firebase] - React Native X Expo X 구글 파이어 베이스 Cloud Firestore 데이터 추가 및 이미지 추가 기능 구현

 

React Native X Expo X 구글 파이어 베이스 Cloud Firestore 데이터 추가 및 이미지 추가 기능 구현

이번에는 구글 파이어 베이스의 Cloud Firestore 데이터 추가 기능을 구현해보겠습니다  추가할 때 이미지도 같이 저장해보도록 하겠습니다 아래의 목차와 같이 진행하겠습니다 Storage 설정 및 폴더

tantangerine.tistory.com

 

무한 스크롤이란?

스크롤을 내릴 때 마지막 하단까지 내려오게 되면 정보를 다시 불러와서

그 정보를 마지막 하단 밑에 붙여주는 효과입니다.

그렇게 되면 사용자로 하여금 스크롤이 무한으로 내려가는 것처럼 느껴지게 되는 것입니다.

 

 

무한 스크롤의 장점은?

기존은 페이징 처리가 되어있어 다음 페이지를 보려면 계속 클릭을 해주어야 합니다.

그렇게 되면 흥미가 떨어질 경우 클릭을 하지 않아 사용자가 이탈하는 경우가 발생합니다.

하지만 스크롤을 내리는 것은 사용자 입장에서는 간단하게 정보를 불러올 수 있으며

스크롤을 내리는 행위로 끊임없이 정보를 노출하면서 사용자의 이탈을 막을 수 있습니다.

 

 

리액트 네이티브 FlatList 및 Firestore Database 무한 스크롤 구현 방법

 

아래의 공식문서를 확인하시면 flatList의 활용하는 방법을 확인하실 수 있습니다

더 상세한 정보를 알고 싶다면 링크를 클릭하시면 됩니다.

 

https://docs.expo.dev/versions/latest/react-native/flatlist/

 

FlatList - Expo Documentation

Expo is an open-source platform for making universal native apps for Android, iOS, and the web with JavaScript and React.

docs.expo.dev

 

ScrollView와 비슷하면서 다른 FlatList

 

 

아래의 코드를 구현해보았습니다

이제 그 속성에 대해서 하나하나 알아보도록 합시다

 

import React, { useState, useEffect } from 'react';
import {
  ActivityIndicator,
  StyleSheet,
  Image,
  View,
  FlatList,
  SafeAreaView,
  Alert,
} from 'react-native';
import { Col, Row, Grid } from 'react-native-easy-grid';
import {
  Container,
  Header,
  Content,
  Left,
  Icon,
  Right,
  Text,
  Button,
} from 'native-base';
import CardComponent from '../components/CardComponent';
import HeaderComponent from '../components/HeaderComponent';
import * as Animatable from 'react-native-animatable';
import { getData, getNextData } from '../config/firebaseApi';
const data = require('../data.json');
export default function MainPage({ navigation }) {
  const [data, setData] = useState([]);
  const [next, setNext] = useState(0);

  useEffect(() => {
    navigation.addListener('beforeRemove', (e) => {
      e.preventDefault();
    });
    readyData();
  }, []);
  const readyData = async () => {
    const data = await getData(setNext);
    setData(data);
  };
  console.log('다음:' + next);
  return (
    <Container>
      <HeaderComponent />
      {data.length == 0 ? (
        <ActivityIndicator size="large" />
      ) : (
        <FlatList
          data={data}
          ListHeaderComponent={() => {
            return (
              <Content style={{ marginTop: 30 }}>
                <Animatable.View
                  animation="pulse"
                  easing="ease-out"
                  iterationCount={3}
                  direction="alternate"
                >
                  <Grid style={styles.banner}>
                    <Col size={1} style={{ padding: 20 }}>
                      <Icon name="paper-plane" style={{ color: 'deeppink' }} />
                    </Col>
                    <Col size={6} style={{ padding: 15 }}>
                      <Text>이야기 하고 싶은 친구들에게</Text>
                      <Text style={{ fontWeight: '700' }}>
                        wegram을 전하세요
                      </Text>
                    </Col>
                  </Grid>
                </Animatable.View>

                <Grid style={{ padding: 20 }}>
                  <Text style={{ color: 'grey' }}>FROM THE DIARY</Text>
                </Grid>
              </Content>
            );
          }}
          onEndReachedThreshold={0.8}
          onEndReached={async () => {
            console.log('바닥 가까이 감: 리프레시');
            if(next > 0){
              let nextData = await getNextData(next, setNext);
              if (nextData == 0) {
                Alert.alert('더이상 글이 없어요!');
              } else {
                let newData = [...data, ...nextData];
                console.log(newData);
                await setData(newData);
              }
            }
          }}
          renderItem={(data) => {
            // console.log(data);
            return (
              <CardComponent navigation={navigation} content={data.item} />
            );
          }}
          numColumns={1}
          keyExtractor={(item) => item.date.toString()}
        />
      )}
    </Container>
  );
}

const styles = StyleSheet.create({
  banner: {
    backgroundColor: '#F6F6F6',
    height: 70,
    borderRadius: 10,
    width: '90%',
    alignSelf: 'center',
  },
});

 

ListHeaderComponent 속에는 컴포넌트를 삽입해서 

아래와 같은 부분을 정의할 수 있습니다

 

 

ListHeaderComponent
헤더 컴포넌트 정의하기

 

 

onEndReachedThreshold={0}은 제일 하단을 언제부터 감지할 것이냐라는 것입니다

0을 준다면 하단 끝까지 맞닿았을 때 감지하게 됩니다.

이 부분은 조금씩 변경해서 한번 테스트를 해보시길 바랍니다.

그리고 감지될 경우  onEndReached에 정의된 함수를 실행하게 됩니다.

keyExtractor속성은 데이터의 키값에 해당하는 값을 넘겨주시면 됩니다.

 

 

속성설명
속성설명

 

하단이 감지된다면 

onEndReached의 함수가 실행돼서

다음 데이터를 호출하여 무한 스크롤 효과를 적용합니다

 

그리고 그 getNextData는 구글 파이어 베이스의 cloud Storage의 기능을 사용합니다

 

export async function getNextData(nextDate, setNext) {
  try {
    console.log("불러올 다음 date: " + nextDate)
    let data = []
    const db = firebase.firestore();
    const next = db.collection('diary')
      .orderBy('date', 'desc')
      .startAfter(nextDate)
      .limit(5);
    const snapshot = await next.get();
    snapshot.docs.map((doc) => {
      console.log("[페이지네이션 Next]")
      doc.data()
      data.push(doc.data());
    });
    console.log(snapshot.docs.length)
    let last;
    if (snapshot.docs.length !== 0) {
      last = snapshot.docs[snapshot.docs.length - 1];
      setNext(last.data().date)
      return data
    } else {
      return 0
    }

  } catch (err) {
    console.log(err);
    return false;
  }
}

 

 

위의 코드를 본다면 .startAfter은 매개변수의 값의 기준으로 그다음의 정보를 불러옵니다.

그리고 last함수에 마지막 데이터를 저장해서 값을 setNext에 넣어줍니다.

그래서 함수를 다시 호출할 때 next값을 사용하여 계속 정보를 가져오게 됩니다.

 

React Native를 공부하기 위해 책으로 공부할까 아니면

강의를 들을까 하다 귀찮기도 하고 강의를 듣기로 하였습니다

책은 값이 싸기는 하지만 책을 보면서 하면 시간이 많이 걸리고

강의를 보면 강의료가 비싸긴 해도 1~2주에 끝날 수 있으니 선호하는 편입니다

 

그러던 중 스파르타 코딩 클럽을 찾았고

후기가 안 좋은 편이긴 하였으나

처음이라 어떤지 궁금하기도 했고

올해 초 이벤트를 하길래 결제를 했었습니다

이벤트가 아니면 너무 비싼 가격이라 하지 않으려고 했었죠

하지만 코드를 보면 리팩토링 해야 할게 너무나도 많았습니다

처음에는 고치면서 하려다가 새로운 코드를 강의마다 적용해야 해서

엎는 과정에서 시간 소요가 상당했습니다

 

그래서 중간에 포기하고 그냥 포스팅하면서 기능 위주로 보려고 하는데

조금 심한 경우가 많네요

이걸 초보자분들이 본다고 생각하니 조금 아찔하기도 합니다

 

if문에 두 개의 return이 있는 거 하며

onEndReached를 실행할 때마다 next값이 0인데도 계속 조회해오는 이상한 구조입니다

처음부터 setNext에 0을 할당해서 getNextData를 호출하지 않게 해야 하는 것이 더 중요한데 말이지요

 

강사분도 그렇게 경력이 많아 보이지도 않고요

다음에는 정말 좋은 강의를 소개하는 포스팅도 쓰도록 해보아야겠네요

 

아무튼 무한 스크롤 구현은 끝났습니다

다음에 이 앱 개발 플러스 프로젝트를 마치고

전체적으로 리팩토링을 하는 포스팅을 올리도록 하겠습니다

 

아무튼 오늘도 앱에 대한 이해도를 넓히기 위해  공부한 것은 보람이 있었다고 할 수 있겠네요

그럼 모든 분들도 보람 있는 하루가 되셨기를 바랍니다

그럼 다음 포스팅에서 뵙겠습니다

 

파이팅!

 

 

 

 

 

 

 

 

 

728x90
반응형
728x90
반응형

이전 포스팅에서 데이터 모델링의 범주화와 추상화에 대해서 알아보았습니다

너무나도 범주화가 중요했지요

범주화는 본질을 정확하게 파악해서 성격이 유사한 개체로 묶는 과정이었습니다

추상화는 가장 핵심적인 특성만 추리하는 과정입니다

 

이렇게 과정을 거치면서 본질과 현상을 구분하여 본질적인 개체를 무시하게 되면

큰 문제점이 드러나는 것도 알아보았습니다

 

더 자세한 사항을 알고 싶다면 아래의 포스팅을 확인해보세요

 

 

 

2022.08.12 - [Data_Modeling/Methodology] - 데이터 모델링 개제의 본질적인 정보와 역할 정보를 구분해야 하는 이유는? 범주화와 추상화의 중요성은?

 

데이터 모델링 개제의 본질적인 정보와 역할 정보를 구분해야하는 이유는? 범주화와 추상화의

이전 포스팅에서는 데이터 관점에 대해서 알아보았습니다 그 관점이라는 것은 우리가 필요로 하는 데이터 값을 결정할 때 여러 가지 정보가 기준이 되어 데이터 값에 영향을 주었다면 그 여러

tantangerine.tistory.com

 


 

 

3단계 스키마 구조가 중요한 이유

 

데이터 모델링을 공부한다고 하면

정말 자주 나오는 것이 3단계 스키마 구조입니다

 

외부 스키마, 개념 스키마, 내부 스키마

이 3개의 스키마 구조인 이 단어들을 많이 들어보셨을 것입니다

 

하지만

이 스키마 구조가 왜 중요한지에 대해서는

생각해보지 못했을 수도 있다고 생각합니다

 

이론적으로 접근해서 

외부 스키마는 사용자 관점의 정보 단위 뷰

개념 스키마는 사용자 관점을 데이터 관점으로 통합한 뷰

내부 스키마 데이터가 DBMS에 저장되는 논리적 구조

 

위의 사항으로만 인지하고 실제 모델링할 경우

생가지 못한 상황이 발생할 수 있습니다

 

이제 이야기를 한번 해봅시다

 

개념 스키마는 외부 스키마와 독립된 계층이라는 것입니다.

하지만 프로젝트를 하다 보면 이상한 구조의 스키마를 보게 됩니다

외부 스키마를 다루듯 테이블을 화면별, 업무 프로세스별로 만드는 경우가 그런 상황입니다

이렇게 되면 스키마가 독립성이 줄어들고 결합도만 커지는 상황이 생깁니다

 

이런 상황이 발생하면 좋은 스키마 구조라고는 할 수 없습니다

외부 스키마에서 정의된 것들이 개념과 내부 스키마를 무시한 채 구조가 정립된 거라고 할 수 있습니다

화면이 변경되면 테이블을 수정하거나 새로운 테이블을 만드는 상황이 올 수도 있다는 것입니다.

그럼 새 프로그램을 개발하는 상황이 되고 그것을 운영하려면 테스트도 새롭게 해야 합니다.

결론적으로 새로운 공수가 발생하게 됩니다.

 

그래서 데이터의 독립성을 정확하게 분석하고 이해해서

데이터 독립성을 해치지 않도록 개념 스키마에 근거해서 모델링해야 합니다.

개념 스키마는 외부 스키마보다 데이터의 본질에 가깝습니다.

그 본질을 무시한 체 외부 스키마의 사용자 관점으로 모든 것을 개발하게 되면

개발자들의 공수들이 늘어나게 되며 이것은 운영에서 지속적인 스트레스가 발생할 수 있습니다 

 

그 데이터들의 형태들을 생각해서 유사한 것을

묶을 수 있는 범주화가 필요할 것입니다

범주화는 이전 포스팅에서 알아보았습니다

 

우리가 결국 만드는 것은 저장 구조입니다

그 구조의 형태는 스프레드 시트와 같은 2차원 표며,

그 표에 어떤 데이터를 어떻게 관리할 것인지를 고민하는 것이 데이터 모델링입니다

 

 

이렇게 우리는 이론적으로 간단하게 생각할 수도 있는 스키마 구조를

본능적으로 이러한 것들을 생각하면서 모델링할 수도 있습니다

하지만 이러한 것들을 생각을 하고 관점을 높일 수 있는 것은 좋은 것 같습니다

 


반응형

 

개념 모델, 논리 모델, 물리 모델, 그리고 현실적인 논리 모델

 

데이터 모델을 보는 3가지 관점으로

개념 모델, 논리 모델, 물리 모델이 있습니다.

 

논리 모델은 물리적인 요소를 고려하지 않고 업무 관점의 모델이며

물리 모델은 물리적인 요소를 고려한 현실적인 모델입니다.

 

이렇게 관점에 대해 듣다 보면 의문점이 생기기 마련입니다.

 

논리 모델에서 현실적인 항목을 반영하면 잘못된 것인가?

논리 모델과 물리 모델 간의 차이가 심하면 모델링은 잘못된 것인가?

그리고 개념 모델은 어느 정도의 수준의 모델인가?

 

이런 의문점을 해결하기 위해서는 

우선 개념 모델링에 대해 정확하게 이해하는 것이 좋습니다.

 

개념 모델주식 별자는 물론이며

엔터티 간의 관계와 주요 속성이 모두 그려진 구체적인 모델입니다.

개념 모델은 가장 특징적인 주요 속성 및 식별까지만 도출해서 전체적인 그림을 그리는 것입니다

품질 좋은 논리 모델을 만들기 위해 반드시 거쳐야 합니다.

 

논리 모델은 주요 엔터티뿐 아니라

모든 엔터티와 개별 엔터티의 속성모두 도출된 구체적이고 정규화된 모델입니다.

논리 모델은 업무 데이터를 테이블 수준이 아닌,

정보 요구사항을 인간이 이해하기 가장 적합한 수준으로

통합하거나 분리한 형태의 모델입니다.

 

이렇게 개념 모델은 논리 모델을 정확하게 그리기 위해 구체적인 뼈대를 형성하여 만들고

논리 모델은 그 뼈대를 바탕으로 살을 붙이는 과정입니다.

 

그렇다면 개념 모델은 어느 정도 수준으로 모델링해야 할지 알 수 있을 것입니다. 

 

이제 남은 것은 물리 모델과 논리 모델의 차이점과 역할을 분명히 할 필요가 있겠습니다.

 

논리 모델에서 성능 이슈는 아니지만

전체 구조에 영향을 미치는 요소들은 좀 더 일찍 결정하는 것이 중요합니다.

그래서 물리 모델에서는 무결성이 조금 손상되더라도

성능과 같은 현실적인 이슈를 최대한 해결하기 위한 작업이라고 할 수 있습니다

 

그러니 논리에서는 최대한 사람이 이해하기 가장 쉬우면서

가능한 한 테이블에 유사한 형태의 모델이어야 합니다.

그렇게 작업이 된 상태에서 물리 모델을 적용해서 성능 이슈를 해결해야 할 것입니다

 


 

프로젝트 성패를 결정짓는 데이터 모델링 이야기 中 by김상래

 

 

 

오늘의 포스팅은 

이전에 많이 배웠던 기본적인 것들이었습니다

3가지 관점과 3단계 스키마를 단 3줄로 정리된 이론적이 것이었지요

하지만 이렇게 관점을 어떻게 보고 어떻게 생각하는지에 대한

사고력을 높이는 것은 상당히 좋은 경험이었던 것 같습니다

 

오늘도 이렇게 좋은 한 걸음을 나아간 것 같습니다

그럼 다음 포스팅도 기대해주세요

 

728x90
반응형
728x90
반응형

이전 포스팅에서는 웹팩에 바벨을 적용해보았습니다.

그리고 바벨이 왜 중요한지에 대한 이유도 알아보았고요.

바벨이 필요한 이유는 자바스크립트의 버전이 점점 올라가면서

다양한 함수들을 구현하여 코드를 더 간편하게 작성할 수 있게 되었습니다.

하지만 브라우저 간에 호환이 안 되는 문제점이 발생하면서

바벨의 중요성은 더욱 부각되었죠

 

더 상세한 이야기가 궁금하시다면 아래의 이전 포스팅을 보시면 됩니다.

 

 

2022.08.11 - [IT_Web/Webpack] - 웹팩 바벨 설치 및 활용 webpack에서 babel이 필요한 이유는?

 

웹팩 바벨 설치 및 활용 webpack에서 babel이 필요한 이유는?

이전 포스팅에서는 HTML, Javascript, css 파일을 웹팩에서 배포 시 압축하는 방법을 알아보았습니다 혹시 못보신분이 계시다면 아래의 링크를 통해 보실 수 있습니다 2022.08.02 - [IT_Web/Webpack] - 웹팩 CSS

tantangerine.tistory.com

 

 

이전 포스팅부터 따라오셨다면

기존 코드들이 있으실 겁니다 

그 코드를 리액트로 만들어보겠습니다

 

 

리액트로 바벨을 적용해야 하기 때문에 리액트 관련 바벨을 추가하며,

리액트 관련 라이브러리를 추가하겠습니다

 

 

npm i --save-dev @babel/preset-react react react-dom

 

 

.babelrc 파일에

@babel/preset-react를 코드 추가하여 줍니다.

 

 

.babelrc

{
 "presets": ["@babel/preset-env", "@babel/preset-react"]
}

 

 

web.common.js 파일에서

jsx문법을 추가해 주시면 됩니다.

 

web.common.js 

module.exports = {
    entry: "./src/index.js",
    module: {
        rules: [
            {......},
 			{......},
            {
                test: /\.(js|jsx)$/, // 추가
                exclude: /node_modules/,
                use: {
                    loader: "babel-loader"
                }
            }
        ]
    }
};

 

 

그리고 이제 기존에 html 파일에

React로 변경된 파일들을 적용해야 합니다

 

우선 app.js를 만들어서 index.js와 html 파일에 적용을 해줍니다

 

import React from 'react';
import ReactDOM from "react-dom";

const App = () => {
    return (
        <div>
       		<h1>React Title</h1>
        </div>
    );
};

export default App;

const wrapper = document.getElementById("app");
wrapper ? ReactDOM.render(<App />, wrapper) : false;

 

 

위의 파일을 만들어서 index.js에

컴포넌트로 전달합니다

 

 

import { btnChange } from "./app/btn";
import { changeColor } from "./app/color";
import "./css/main.css";
import App from "./js/App"; // 추가

btnChange.addEventListener("click", function() {
 changeColor("pink");
})

 

 

 

html 파일에도 코드를 추가하여줍니다

이렇게 하면 리액트가 추가됩니다

그다음은 app.js에서 리액트 라이브러리를 추가하여 코드를 작성하여

리액트 관련 개발을 하면 되겠습니다 

 

 

<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport"
              content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
    </head>
    <body>
        <h1 class="card-title">Card Title</h1>
        <button class="btn-change">버튼 변경</button>
        <img src="./assets/funnelback.png">
        <div id="app"></div> /* 코드 추가 */
    </body>
</html>

 

 

 

 

오늘은 정말 간단한 시간을 가졌습니다

바벨 적용도 그냥 한 줄의 라이브러리 설치와 

한 개의 파일만 만들면 간단하게 적용이 되며 설치가 됩니다

 

오늘은 이렇게 설치가 간단하게 된다는 걸 알면 조금 허무하긴 하네요

 

아무튼 그래도 이런 방법을 아는 것도 중요한 공부라고 생각합니다

그럼 다음 포스팅도 기대해주시고

 

대모험이 끝나는 그날까지 파이팅합시다!!

 

 

 

 

 

728x90
반응형

+ Recent posts

Powered by Tistory, Designed by wallel