카테고리 없음

15주차 (4)

jaeoun0238 2025. 2. 6. 23:01

스크래핑

스크래핑을 처음 접하면서 찾아본 결과, 스크래핑은 프로그램을 이용해 웹사이트에서 데이터를 자동으로 수집하는 기술이라고 합니다. 그렇다면 왜 스크래핑이 필요할까요? 조사해보니 데이터 분석, 마케팅 및 시장 조사, 정보 수집 등의 분야에서 많이 활용된다고 합니다. 직접 스크래핑을 해보면서 느낀 점은, 원하는 정보를 자동으로 가져올 수 있어 매우 편리하지만, 무분별하게 데이터를 수집하는 경우도 발생할 수 있다는 것입니다. 이를 방지하기 위해 웹사이트에는 보안 장치가 마련되어 다음과 같은 방법이 사용됩니다.

 

1. robots.txt 파일

대부분의 웹사이트는 robots.txt 파일을 사용하여 크롤러(스크래핑 봇)가 접근할 수 있는 영역과 금지된 영역을 지정합니다. 이 파일을 확인하고 정책을 준수하는 것이 중요합니다.

2. CAPTCHA 및 Bot Detection

일부 웹사이트는 로그인 과정이나 특정 요청 시 CAPTCHA를 사용하여 봇의 접근을 차단합니다. 또한, 브라우저 행동 패턴 분석을 통해 봇을 탐지하는 보안 시스템도 존재합니다.

3. IP 차단 및 Rate Limiting

일정 시간 내 너무 많은 요청을 보내는 경우, 서버가 IP를 차단하거나 속도를 제한하는 Rate Limiting을 적용할 수 있습니다. 이를 방지하려면 요청 간격을 조정하고, API가 제공된다면 공식 API를 이용하는 것이 좋습니다.

 

이러한 방법으로 무분별한 스크래핑을 방지할 수 있다고 합니다. 스크래핑을 진행할 때는 반드시 대상 사이트의 이용 약관을 확인하고, 윤리적인 방법으로 데이터를 수집해야 한다는 것을 알게 되었습니다. 비록 이번 Trello 프로젝트에서는 스크래핑을 활용하지 못했지만, 다음 최종 프로젝트에서는 꼭 적용해보고 싶습니다!

 

scraperService.ts

import { Injectable } from '@nestjs/common'; // NestJS의 Injectable 데코레이터 (의존성 주입을 위해 사용)
import * as puppeteer from 'puppeteer'; // Puppeteer 라이브러리 가져오기 (헤드리스 브라우저 실행)

@Injectable() // NestJS의 서비스 클래스임을 나타내는 데코레이터
export class ScraperService {

  // Puppeteer를 사용하여 동적으로 웹사이트를 스크래핑하는 메서드
  // @param url - 크롤링할 웹사이트 주소
  // @returns 페이지 제목(title), h1~h3 태그 내용, 모든 링크 목록을 포함한 객체

  async scrapeWebsite(
    url: string
  ): Promise<{ title: string; headings: string[]; links: string[] }> {
    let browser: puppeteer.Browser | null = null; // Puppeteer 브라우저 객체 (초기값 null)

    try {
      // Puppeteer 브라우저 실행 (백그라운드에서 동작하는 headless 모드)
      browser = await puppeteer.launch({ headless: true });
      const page: puppeteer.Page = await browser.newPage(); // 새로운 페이지(탭) 생성

      // 지정된 URL로 이동 (네트워크 요청이 거의 완료될 때까지 대기)
      await page.goto(url, { waitUntil: 'networkidle2' });

      // 현재 페이지의 제목(title) 가져오기
      const title: string = await page.title();

      // 1, h2, h3 태그 내의 텍스트를 가져오기
      const headings: string[] = await page.$$eval('h1, h2, h3', (elements: Element[]) =>
        elements.map((el: Element) => el.textContent?.trim() || '') // textContent가 null이면 빈 문자열 반환
      );

      // 모든 a 태그에서 링크(href 속성) 가져오기
      const links: string[] = await page.$$eval('a', (elements: Element[]) =>
        elements
          .map((el: Element) => el.getAttribute('href')) // 각 링크의 href 속성 가져오기
          .filter((href): href is string => !!href && href.startsWith('http')) // 빈 값, null 제외 & http로 시작하는 URL만 필터링
      );

      // Puppeteer 브라우저 종료 후 크롤링 결과 반환
      await browser.close();
      return { title, headings, links };
    } catch (error) {
      if (browser) await browser.close(); // 오류 발생 시 브라우저 종료
      throw new Error(`Puppeteer 스크래핑 실패: ${error.message}`); // 에러 메시지 반환
    }
  }
}

scraper.controller

import { Controller, Post, Body } from '@nestjs/common'; // POST 요청을 처리하기 위해 @Post, @Body 추가
import { ScraperService } from './scraper.service'; // ScraperService 가져오기 (서비스 계층)

@Controller('scraper') // 'scraper' 엔드포인트 설정 (예: http://localhost:3000/scraper)
export class ScraperController {
  constructor(private readonly scraperService: ScraperService) { } // ScraperService를 의존성 주입하여 사용

  // @POST 요청을 처리하는 엔드포인트
  // 요청의 body에서 'url' 값을 받아 웹사이트 데이터를 스크래핑한다.
  // @param body 요청 바디 (JSON 데이터)
  // @returns 크롤링된 데이터를 JSON 형식으로 반환

  @Post()
  async scrape(@Body() body: { url: string }) {
    // 요청 바디에서 URL 값이 없는 경우 오류 반환
    if (!body.url) {
      return { error: 'URL을 입력하세요.' };
    }

    try {
      // ScraperService의 scrapeWebsite() 메서드를 호출하여 크롤링 수행
      const data = await this.scraperService.scrapeWebsite(body.url);

      // 크롤링 결과를 JSON 형태로 반환
      return { url: body.url, data };
    } catch (error) {
      // 오류 발생 시 사용자에게 오류 메시지를 반환
      return { error: `스크래핑 실패: ${error.message}` };
    }
  }
}

 

스크래핑 완료!