Nest.js puppeteer

이건선·2023년 6월 11일
0

해결

목록 보기
43/48
post-custom-banner

주식 데이터를 크롤링 해보자

-60%까지 떨어졌던 주식이 부활하고 쏠쏠한 수익을 냈다. 그래서 다시 주식을 시작했고, 효율적으로 관리하기 위해서 개인적인 웹 페이지를 만들고 싶다는 욕심이 생겼다. 그 첫단계로 주식 데이터를 크롤링 해야겠다.

라이브러리 설치

npm i puppeteer

조건

  1. 현재 SOXL, TNA, TQQQ 총 3가지 종목을 크롤링 해야한다.
  2. 종목의 개수는 시장 현황에 따라서 유동적으로 변화할 수 있다.

Controller

...

  @Post('/')
  async currentStock(@Body('url') url: string[]): Promise<BoardsData> {
    return await this.boardsService.name(url);
  }
  
...

Service

...

 for (let urls of url) {
      const page: puppeteer.Page = await browser.newPage();
      await page.goto(urls, { waitUntil: 'networkidle2' });

      const result: BoardData = await page.evaluate(() => {
        const nameElement: HTMLElement | null = document.querySelector(
          'div.instrument-price_instrument-price__xfgbB > div.text-xl > span.instrument-price_change-percent__bT4yt',
        );

        const newElement: HTMLElement | null = document.querySelector(
          'div.instrument-header_instrument-name__VxZ1O > h1',
        );

        let change_percent: string = nameElement
          ? nameElement.textContent
          : 'No data found';
        let name: string = newElement
          ? newElement.textContent
          : 'No data found';
        return { change_percent, name };
      });

...

알게 된 것

1. ({headless: false,})

headless 프로퍼티가 ture라면 실제 브라우저의 실행없이도 데이터를 크롤링 할 수 있다. 그런데 ture옵션을 주었을 때 정상적으로 작동하지 않는 것 같다. 일단 false 옵션을 사용하면 '헤드풀(headful)' 모드에서 브라우저를 시작한다. 즉, 실제 브라우저 창이 열리고 사용자가 볼 수 있게 된다는데 이 때는 정상작동 한다.

...

 const browser: puppeteer.Browser = await puppeteer.launch({
      headless: false,
    });
    
...

2. await page.goto(urls, { waitUntil: 'networkidle2' })

Puppeteer의 page.goto 메소드의 waitUntil 옵션은 특정 이벤트가 발생할 때까지 페이지 네비게이션을 대기하는데 사용됩니다. 사용 가능한 값은 다음과 같습니다:

'load': window.load 이벤트가 발생할 때까지 대기. 이 이벤트는 모든 이미지, 스타일 시트 등이 로드된 후에 발생합니다.

'domcontentloaded': DOMContentLoaded 이벤트가 발생할 때까지 대기. 이 이벤트는 HTML이 완전히 로드되고 파싱되었지만 스타일 시트, 이미지, 서브프레임 등이 아직 로드되지 않았을 때 발생합니다.

'networkidle0': 최소한 500ms 동안 네트워크에 최대 0개의 네트워크 연결이 발생할 때까지 대기. 이는 페이지가 거의 완전히 로드되었음을 나타내는 좋은 지표가 될 수 있습니다.

'networkidle2': 최소한 500ms 동안 네트워크에 최대 2개의 네트워크 연결이 발생할 때까지 대기. 이는 페이지 로드가 거의 끝났지만 일부 요소가 아직 로드 중일 수 있음을 나타냅니다.

이러한 옵션은 웹 사이트의 로드 상태를 더 세밀하게 제어하고 싶을 때 유용합니다. 예를 들어, 네트워크 연결이 완전히 끝나기를 기다리는 대신 DOMContentLoaded 이벤트가 발생하자마자 크롤링을 시작하고 싶을 수 있습니다.

3. document.querySelector HTML 문서에서 선택자를 사용

  • >: 이 기호는 "직접 자식(direct child)" 선택자를 의미합니다. 즉, parent > childparent 요소의 직접적인 하위 요소 중 child를 선택합니다. parent 아래에 더 깊게 중첩된 child는 선택하지 않습니다.

  • .: 이 기호는 클래스 선택자를 의미합니다. 따라서 .class-name은 HTML 요소에서 class-name이라는 클래스를 가진 모든 요소를 선택합니다.


전문

import { Controller, Get, Injectable } from '@nestjs/common';
import * as puppeteer from 'puppeteer';
import { createStockDTO } from './dto/boards.dto';
import { BoardsData, BoardData } from './interface/boards.interface';

@Injectable()
export class BoardsService {
  async name(url: string[]): Promise<BoardsData> {
    const browser: puppeteer.Browser = await puppeteer.launch({
      headless: false,
    });
    let arr: BoardData[] = [];

    for (let urls of url) {
      const page: puppeteer.Page = await browser.newPage();
      await page.goto(urls, { waitUntil: 'networkidle2' });

      const result: BoardData = await page.evaluate(() => {
        const nameElement: HTMLElement | null = document.querySelector(
          'div.instrument-price_instrument-price__xfgbB > div.text-xl > span.instrument-price_change-percent__bT4yt',
        );

        const newElement: HTMLElement | null = document.querySelector(
          'div.instrument-header_instrument-name__VxZ1O > h1',
        );

        let change_percent: string = nameElement
          ? nameElement.textContent
          : 'No data found';
        let name: string = newElement
          ? newElement.textContent
          : 'No data found';
        return { change_percent, name };
      });

      const arrName: BoardData = {
        change_percent: result.change_percent,
        name: result.name,
      };

      arr.push(arrName);
      await page.close();
    }

    await browser.close();
    return { currentPercent: arr };
  }

  async createStock(createStockDTO: createStockDTO) {
    return;
  }
}
profile
멋지게 기록하자
post-custom-banner

0개의 댓글