웹에는 무한한 정보가 있지만, 필요한 데이터를 직접 수집하려면 많은 시간이 필요합니다. 이럴 때 파이썬의 웹 스크래핑 라이브러리를 활용하면 데이터 수집을 자동화할 수 있어 작업 효율이 크게 향상됩니다. 오늘은 파이썬의 대표적인 웹 스크래핑 도구인 BeautifulSoup과 Scrapy를 자세히 살펴보겠습니다. 각 라이브러리의 장단점과 실 예제를 통해 어떤 상황에서 어떤 도구를 사용하는 것이 적합한지 판단해 보시기 바랍니다. 🙂

 

1. 웹 스크래핑이란?

웹 스크래핑(Web Scraping)은 웹사이트에서 데이터를 자동으로 추출하는 기술입니다. 웹페이지의 HTML 구조를 분석하여 원하는 정보를 수집하고, 이를 구조화된 형태로 저장하는 과정을 말합니다. 이 기술은 데이터 과학, 시장 조사, 가격 모니터링, 콘텐츠 집계 등 다양한 분야에서 활용됩니다.

웹 스크래핑이 필요한 이유

  • 데이터 수집 자동화: 수작업으로 데이터를 수집하는 것은 시간이 많이 소요되고 오류가 발생하기 쉽습니다
  • 실시간 데이터 모니터링: 가격 변동, 재고 상태, 뉴스 등 지속적으로 변하는 정보를 실시간으로 추적할 수 있습니다
  • 데이터 분석을 위한 대량의 데이터 확보: 데이터 분석에 필요한 충분한 양의 데이터를 빠르게 확보할 수 있습니다
  • 공개 API가 없는 웹사이트의 데이터 활용: 모든 웹사이트가 데이터 접근을 위한 API를 제공하지는 않습니다

 

2. 파이썬 웹 스크래핑의 기본 개념

파이썬은 간결한 문법과 풍부한 라이브러리 생태계 덕분에 웹 스크래핑에 가장 많이 사용되는 언어 중 하나입니다. 기본적인 웹 스크래핑 과정은 다음과 같습니다:

  1. 웹페이지 요청 (Request)
  2. HTML 응답 받기 (Response)
  3. HTML 구문 분석 (Parsing)
  4. 데이터 추출 (Extraction)
  5. 데이터 저장/분석 (Storage/Analysis)

Python-Web-Scrapping-Process

파이썬에서는 주로 두 가지 도구를 사용합니다:

  • BeautifulSoup: HTML 파서로, 웹페이지의 내용을 파싱하고 탐색하는 데 특화되어 있습니다
  • Scrapy: 웹 크롤링과 스크래핑을 위한 완전한 프레임워크로, 대규모 프로젝트에 적합합니다

 

3. BeautifulSoup 기초: 간단하고 직관적인 HTML 파싱 도구

BeautifulSoup은 HTML과 XML 파일에서 데이터를 추출하기 위한 파이썬 라이브러리입니다. 설치가 간단하고 사용법이 직관적이라 초보자도 쉽게 배울 수 있습니다.

BeautifulSoup 설치하기

# pip을 이용한 설치
pip install beautifulsoup4

# requests 라이브러리도 함께 설치
pip install requests

BeautifulSoup 기본 사용법

아래는 BeautifulSoup의 기본적인 사용 예시입니다:

import requests
from bs4 import BeautifulSoup

# 웹페이지 가져오기
url = 'https://example.com'
response = requests.get(url)

# BeautifulSoup 객체 생성
soup = BeautifulSoup(response.text, 'html.parser')

# 제목 태그 찾기
title = soup.title.text
print('페이지 제목:', title)

# 모든 링크 찾기
links = soup.find_all('a')
for link in links:
    print('링크:', link.get('href'))
    
# 특정 클래스를 가진 요소 찾기
content = soup.find('div', class_='content')
if content:
    print('콘텐츠:', content.text)

BeautifulSoup 선택자 사용하기

BeautifulSoup은 다양한 방법으로 HTML 요소를 선택할 수 있습니다:

# 태그명으로 찾기
paragraphs = soup.find_all('p')

# CSS 선택자로 찾기
result = soup.select('div.article h2')

# ID로 찾기
header = soup.find(id='header')

# 속성으로 찾기
images = soup.find_all('img', alt=True)  # alt 속성이 있는 모든 이미지

# 텍스트 내용으로 찾기
search_results = soup.find_all('a', string=lambda text: 'python' in text.lower())

 

4. BeautifulSoup 실 예제: 뉴스 기사 스크래핑

실제 뉴스 사이트에서 기사를 스크래핑하는 예제를 살펴보겠습니다. 이 예제에서는 가상의 뉴스 사이트에서 최신 기사 제목, 요약, 링크를 수집합니다.

import requests
from bs4 import BeautifulSoup
import csv
from datetime import datetime

# 뉴스 사이트 URL (예시)
url = 'https://example-news-site.com'

def scrape_news():
    # 웹페이지 요청
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
    }
    response = requests.get(url, headers=headers)
    
    # 응답 확인
    if response.status_code != 200:
        print(f"오류 발생: 상태 코드 {response.status_code}")
        return None
    
    # HTML 파싱
    soup = BeautifulSoup(response.text, 'html.parser')
    
    # 뉴스 기사 컨테이너 찾기 (가정: 각 기사는 article 태그 내에 있음)
    articles = soup.find_all('article', class_='news-item')
    
    # 결과 저장할 리스트
    news_data = []
    
    # 각 기사에서 데이터 추출
    for article in articles:
        # 제목 추출
        title_element = article.find('h2', class_='title')
        title = title_element.text.strip() if title_element else 'No Title'
        
        # 요약 추출
        summary_element = article.find('p', class_='summary')
        summary = summary_element.text.strip() if summary_element else 'No Summary'
        
        # 링크 추출
        link_element = article.find('a', class_='read-more')
        link = link_element['href'] if link_element and 'href' in link_element.attrs else ''
        
        # 날짜 추출
        date_element = article.find('span', class_='date')
        date = date_element.text.strip() if date_element else 'No Date'
        
        # 데이터 저장
        news_data.append({
            'title': title,
            'summary': summary,
            'link': link,
            'date': date
        })
    
    return news_data

# 데이터 CSV로 저장
def save_to_csv(data, filename=None):
    if not filename:
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        filename = f'news_data_{timestamp}.csv'
    
    with open(filename, 'w', newline='', encoding='utf-8') as csvfile:
        fieldnames = ['title', 'summary', 'link', 'date']
        writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
        
        writer.writeheader()
        for article in data:
            writer.writerow(article)
    
    print(f"데이터가 {filename}에 저장되었습니다.")

# 메인 함수
if __name__ == '__main__':
    print("뉴스 데이터 스크래핑 시작...")
    news_data = scrape_news()
    
    if news_data:
        print(f"{len(news_data)}개의 기사를 찾았습니다.")
        save_to_csv(news_data)
    else:
        print("데이터를 추출할 수 없습니다.")

이 예제는 다음과 같은 기능을 포함합니다:

  1. User-Agent 헤더를 설정하여 실제 브라우저처럼 요청
  2. 응답 상태 코드 확인
  3. 기사의 제목, 요약, 링크, 날짜 추출
  4. CSV 파일로 데이터 저장

 

5. Scrapy: 강력한 웹 크롤링 프레임워크

Scrapy는 웹 크롤링과 스크래핑을 위한 강력한 파이썬 프레임워크입니다. BeautifulSoup과 달리 Scrapy는 단순한 파서가 아닌 완전한 웹 크롤링 애플리케이션을 구축할 수 있는 도구입니다.

Scrapy의 특징

  • 비동기 네트워킹: 동시에 여러 요청을 처리하여 성능 향상
  • 내장 선택자: XPath 및 CSS 선택자를 통한 데이터 추출
  • 미들웨어 지원: 요청 및 응답 처리를 위한 커스텀 미들웨어
  • 파이프라인: 추출된 데이터 처리, 검증, 저장을 위한 구조
  • 확장성: 다양한 플러그인과 확장 가능한 아키텍처
  • 내장 익스포터: JSON, CSV, XML 등 다양한 형식으로 데이터 저장

Scrapy 설치하기

pip install scrapy

Scrapy 프로젝트 구조

Scrapy는 프로젝트 기반으로 작동합니다. 새 프로젝트를 생성하면 다음과 같은 구조가 만들어집니다:

myproject/
    scrapy.cfg            # 프로젝트 설정 파일
    myproject/            # 프로젝트 파이썬 모듈
        __init__.py
        items.py          # 아이템 정의 (추출할 데이터 구조)
        middlewares.py    # 미들웨어 정의
        pipelines.py      # 아이템 파이프라인 정의
        settings.py       # 프로젝트 설정
        spiders/          # 스파이더 디렉토리
            __init__.py
            spider1.py    # 스파이더 정의
            spider2.py

 

6. Scrapy 실 예제: 온라인 서점 데이터 수집

아래 예제는 Scrapy를 사용하여 가상의 온라인 서점에서 책 정보를 수집하는 방법을 보여줍니다.

1. Scrapy 프로젝트 생성

scrapy startproject bookstore
cd bookstore

2. 아이템 정의 (items.py)

import scrapy

class BookItem(scrapy.Item):
    title = scrapy.Field()         # 책 제목
    author = scrapy.Field()        # 저자
    price = scrapy.Field()         # 가격
    rating = scrapy.Field()        # 평점
    description = scrapy.Field()   # 설명
    category = scrapy.Field()      # 카테고리
    url = scrapy.Field()           # 상세 페이지 URL

3. 스파이더 생성 (spiders/books_spider.py)

import scrapy
from bookstore.items import BookItem

class BooksSpider(scrapy.Spider):
    name = 'books'
    allowed_domains = ['example-bookstore.com']
    start_urls = ['https://example-bookstore.com/books/category/programming']
    
    def parse(self, response):
        # 책 목록 페이지에서 각 책의 상세 페이지 링크 추출
        book_links = response.css('div.book-item a.title::attr(href)').getall()
        
        # 각 책의 상세 페이지 방문
        for link in book_links:
            yield response.follow(link, self.parse_book)
        
        # 다음 페이지가 있으면 이동
        next_page = response.css('a.next-page::attr(href)').get()
        if next_page:
            yield response.follow(next_page, self.parse)
    
    def parse_book(self, response):
        # 책 상세 정보 추출
        book = BookItem()
        
        book['title'] = response.css('h1.book-title::text').get().strip()
        book['author'] = response.css('div.author-info span.name::text').get().strip()
        book['price'] = response.css('span.price::text').get().strip()
        book['rating'] = response.css('div.rating span.score::text').get()
        book['description'] = response.css('div.description::text').get().strip()
        book['category'] = response.css('div.breadcrumbs span.category::text').getall()[-1].strip()
        book['url'] = response.url
        
        yield book

4. 파이프라인 설정 (pipelines.py)

import json
from itemadapter import ItemAdapter

class PricePipeline:
    """가격 정보를 정수로 변환"""
    def process_item(self, item, spider):
        adapter = ItemAdapter(item)
        # '₩30,000' 형태의 가격을 정수로 변환
        if adapter.get('price'):
            price_text = adapter['price']
            # 통화 기호와 쉼표 제거 후 정수로 변환
            try:
                adapter['price'] = int(price_text.replace('₩', '').replace(',', ''))
            except ValueError:
                spider.logger.warning(f"가격 변환 실패: {price_text}")
        return item

class JsonWriterPipeline:
    """아이템을 JSON 파일로 저장"""
    def open_spider(self, spider):
        self.file = open('books.json', 'w', encoding='utf-8')
        self.file.write('[')
        self.first_item = True

    def close_spider(self, spider):
        self.file.write(']')
        self.file.close()

    def process_item(self, item, spider):
        line = json.dumps(dict(item), ensure_ascii=False) + ',\n'
        if self.first_item:
            self.file.write(line[:-2])  # 첫 번째 항목은 쉼표 없음
            self.first_item = False
        else:
            self.file.write(line)
        return item

5. 설정 업데이트 (settings.py)

# 파이프라인 활성화
ITEM_PIPELINES = {
    'bookstore.pipelines.PricePipeline': 300,
    'bookstore.pipelines.JsonWriterPipeline': 800,
}

# 봇처럼 보이지 않도록 User-Agent 설정
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'

# 요청 간 딜레이 설정 (초 단위)
DOWNLOAD_DELAY = 1.5

# 동시 요청 수 제한
CONCURRENT_REQUESTS = 16

# 로깅 설정
LOG_LEVEL = 'INFO'

6. 스파이더 실행

scrapy crawl books -o books.csv

이 명령은 스파이더를 실행하고 결과를 CSV 파일로 저장합니다.

 

7. BeautifulSoup vs Scrapy: 특징 비교

두 라이브러리는 서로 다른 특성과 장단점이 있습니다. 다음 표는 BeautifulSoup과 Scrapy의 주요 특징을 비교한 것입니다:

비교표

특징 BeautifulSoup Scrapy
유형 파서 라이브러리 웹 크롤링 프레임워크
설치 및 설정 매우 간단 보다 복잡한 구조
학습 곡선 낮음 (초보자 친화적) 중간~높음
속도 단일 요청 처리 비동기식 다중 요청 처리
확장성 제한적 매우 높음
프로젝트 구조 자유로움 정형화된 구조
내장 기능 HTML 파싱에 집중 크롤링, 데이터 저장, 미들웨어 등 다양한 기능
사용 적합성 단순한 스크래핑, 소규모 프로젝트 대규모 스크래핑, 복잡한 크롤링 작업
유지보수 코드가 간결하고 이해하기 쉬움 구조화되어 있어 대규모 프로젝트에 적합

 

8. 웹 스크래핑 팁과 개발 스탠다드

웹 스크래핑을 할 때 고려해야 할 중요한 팁과 관련 스탠다드를 살펴보겠습니다.

법적/윤리적 고려사항

  • robots.txt 파일 준수: 웹사이트의 robots.txt 파일을 확인하여 크롤링이 허용된 영역을 파악
  • 이용약관 확인: 대상 웹사이트의 이용약관에서 자동화된 액세스에 대한 정책을 확인
  • 과도한 요청 피하기: 서버에 부담을 주지 않도록 요청 간 적절한 지연 시간을 설정
  • 개인정보 존중: 개인식별정보(PII)를 수집할 때는 관련 개인정보보호법을 준수

기술적 모범 사례

# requests 라이브러리 사용 시 모범 사례
import requests
from time import sleep
import random

def get_page(url):
    # User-Agent 헤더 설정
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
        'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
        'Referer': 'https://www.google.com/'
    }
    
    try:
        # 타임아웃 설정 및 요청
        response = requests.get(url, headers=headers, timeout=10)
        response.raise_for_status()  # 4XX, 5XX 오류 확인
        
        # 요청 간 랜덤 지연 (1~3초)
        sleep(random.uniform(1, 3))
        
        return response.text
    except requests.exceptions.RequestException as e:
        print(f"요청 실패: {e}")
        return None

# 세션 사용하기 (쿠키 유지, 연결 재사용)
def use_session():
    session = requests.Session()
    session.headers.update({
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
    })
    
    # 첫 번째 페이지 방문 (로그인 페이지 등)
    session.get('https://example.com/login')
    
    # 이후 요청에서 세션 유지
    response = session.get('https://example.com/protected-page')
    
    return response.text

자주 발생하는 문제와 해결 방법

문제 해결 방법
차단 또는 CAPTCHA • User-Agent 헤더 사용<br>• 요청 간 적절한 지연 설정<br>• 요청 패턴 랜덤화<br>• 프록시 서버 사용
동적 콘텐츠 (JavaScript) • Selenium이나 Playwright 같은 헤드리스 브라우저 사용<br>• API 요청 찾아 직접 호출<br>• JavaScript 렌더링 서비스 사용
로그인 필요 • requests.Session 사용<br>• 쿠키 관리<br>• 토큰 인증 처리
구조 변경 • 선택자를 최대한 구체적으로 사용<br>• 다양한 선택자 조합 사용<br>• 정기적인 유지보수 계획
대용량 데이터 • 병렬 처리 구현<br>• 데이터베이스 활용<br>• 증분 크롤링 전략

 

9. 웹 스크래핑 프로젝트 예제: 구인 사이트 데이터 분석

아래 예제는 구인 정보 사이트에서 데이터를 수집하고 분석하는 통합 프로젝트입니다. BeautifulSoup과 Pandas를 조합하여 구현했습니다.

import requests
from bs4 import BeautifulSoup
import pandas as pd
import matplotlib.pyplot as plt
import time
import random
import re
from datetime import datetime

# 구인 사이트 데이터 스크래핑 클래스
class JobScraper:
    def __init__(self, base_url, max_pages=5):
        self.base_url = base_url
        self.max_pages = max_pages
        self.jobs = []
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
        }
    
    def scrape_all_pages(self):
        """모든 페이지에서 채용 정보 스크래핑"""
        for page in range(1, self.max_pages + 1):
            print(f"페이지 {page} 스크래핑 중...")
            url = f"{self.base_url}/jobs?page={page}"
            self._scrape_page(url)
            
            # 요청 간 랜덤 지연
            time.sleep(random.uniform(1, 3))
    
    def _scrape_page(self, url):
        """한 페이지에서 채용 정보 스크래핑"""
        try:
            response = requests.get(url, headers=self.headers, timeout=10)
            response.raise_for_status()
            
            soup = BeautifulSoup(response.text, 'html.parser')
            job_listings = soup.find_all('div', class_='job-card')
            
            for job in job_listings:
                job_data = self._extract_job_data(job)
                if job_data:
                    self.jobs.append(job_data)
                    
        except requests.exceptions.RequestException as e:
            print(f"페이지 스크래핑 실패: {e}")
    
    def _extract_job_data(self, job_element):
        """채용 카드에서 직무 데이터 추출"""
        try:
            # 직무명 추출
            title_element = job_element.find('h2', class_='job-title')
            title = title_element.text.strip() if title_element else "N/A"
            
            # 회사명 추출
            company_element = job_element.find('div', class_='company-name')
            company = company_element.text.strip() if company_element else "N/A"
            
            # 지역 추출
            location_element = job_element.find('div', class_='location')
            location = location_element.text.strip() if location_element else "N/A"
            
            # 급여 추출
            salary_element = job_element.find('div', class_='salary')
            salary_text = salary_element.text.strip() if salary_element else "N/A"
            
            # 급여 정규화 (예: "연봉 5,000만원 ~ 7,000만원" -> [5000, 7000])
            if salary_text != "N/A":
                salary_range = re.findall(r'(\d[\d,]+)만원', salary_text)
                if len(salary_range) >= 2:
                    min_salary = int(salary_range[0].replace(',', ''))
                    max_salary = int(salary_range[1].replace(',', ''))
                else:
                    min_salary = max_salary = "N/A"
            else:
                min_salary = max_salary = "N/A"
            
            # 게시 날짜 추출
            date_element = job_element.find('div', class_='post-date')
            posted_date = date_element.text.strip() if date_element else "N/A"
            
            # 요구 기술 추출
            skills_elements = job_element.find_all('span', class_='skill-tag')
            skills = [skill.text.strip() for skill in skills_elements] if skills_elements else []
            
            # 직무 URL 추출
            url_element = job_element.find('a', class_='job-link')
            job_url = url_element['href'] if url_element and 'href' in url_element.attrs else "N/A"
            if job_url.startswith('/'):
                job_url = self.base_url + job_url
            
            return {
                'title': title,
                'company': company,
                'location': location,
                'salary_text': salary_text,
                'min_salary': min_salary,
                'max_salary': max_salary,
                'posted_date': posted_date,
                'skills': skills,
                'url': job_url
            }
        except Exception as e:
            print(f"직무 데이터 추출 실패: {e}")
            return None
    
    def save_to_csv(self, filename=None):
        """스크래핑한 데이터를 CSV로 저장"""
        if not filename:
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            filename = f'job_data_{timestamp}.csv'
        
        df = pd.DataFrame(self.jobs)
        df.to_csv(filename, index=False, encoding='utf-8-sig')
        print(f"데이터가 {filename}에 저장되었습니다.")
        return df

# 데이터 분석 클래스
class JobDataAnalyzer:
    def __init__(self, df):
        self.df = df
    
    def clean_data(self):
        """데이터 정제"""
        # 결측치 처리
        self.df = self.df.fillna('N/A')
        
        # 급여 데이터가 숫자인 행만 필터링 (분석용)
        self.salary_df = self.df[self.df['min_salary'] != 'N/A'].copy()
        if not self.salary_df.empty:
            self.salary_df['min_salary'] = self.salary_df['min_salary'].astype(int)
            self.salary_df['max_salary'] = self.salary_df['max_salary'].astype(int)
            self.salary_df['avg_salary'] = (self.salary_df['min_salary'] + self.salary_df['max_salary']) / 2
        
        return self.df
    
    def get_basic_stats(self):
        """기본 통계 정보 추출"""
        stats = {
            'total_jobs': len(self.df),
            'companies': len(self.df['company'].unique()),
            'locations': self.df['location'].value_counts().to_dict(),
            'top_skills': self._get_top_skills(10)
        }
        
        if hasattr(self, 'salary_df') and not self.salary_df.empty:
            stats['avg_min_salary'] = self.salary_df['min_salary'].mean()
            stats['avg_max_salary'] = self.salary_df['max_salary'].mean()
            stats['highest_salary'] = self.salary_df['max_salary'].max()
        
        return stats
    
    def _get_top_skills(self, n=10):
        """가장 많이 요구되는 기술 상위 n개 추출"""
        all_skills = []
        for skills_list in self.df['skills']:
            if isinstance(skills_list, list):
                all_skills.extend(skills_list)
        
        skill_counts = pd.Series(all_skills).value_counts()
        return skill_counts.head(n).to_dict()
    
    def plot_location_distribution(self, save_path=None):
        """지역별 채용 분포 시각화"""
        plt.figure(figsize=(12, 6))
        location_counts = self.df['location'].value_counts().head(10)
        location_counts.plot(kind='bar', color='skyblue')
        plt.title('지역별 채용 공고 수')
        plt.xlabel('지역')
        plt.ylabel('공고 수')
        plt.xticks(rotation=45)
        plt.tight_layout()
        
        if save_path:
            plt.savefig(save_path)
            print(f"그래프가 {save_path}에 저장되었습니다.")
        
        plt.show()
    
    def plot_salary_distribution(self, save_path=None):
        """급여 분포 시각화"""
        if not hasattr(self, 'salary_df') or self.salary_df.empty:
            print("급여 데이터가 충분하지 않습니다.")
            return
        
        plt.figure(figsize=(12, 6))
        plt.hist(self.salary_df['avg_salary'], bins=20, color='lightgreen', edgecolor='black')
        plt.title('평균 급여 분포')
        plt.xlabel('평균 급여 (만원)')
        plt.ylabel('빈도')
        plt.grid(axis='y', linestyle='--', alpha=0.7)
        plt.tight_layout()
        
        if save_path:
            plt.savefig(save_path)
            print(f"그래프가 {save_path}에 저장되었습니다.")
        
        plt.show()
    
    def plot_skills_wordcloud(self, save_path=None):
        """기술 키워드 워드클라우드 시각화"""
        try:
            from wordcloud import WordCloud
            import matplotlib.pyplot as plt
            
            all_skills = []
            for skills_list in self.df['skills']:
                if isinstance(skills_list, list):
                    all_skills.extend(skills_list)
            
            skill_text = ' '.join(all_skills)
            
            if not skill_text:
                print("기술 데이터가 충분하지 않습니다.")
                return
            
            plt.figure(figsize=(12, 8))
            wordcloud = WordCloud(
                width=800, 
                height=500,
                background_color='white',
                max_words=100
            ).generate(skill_text)
            
            plt.imshow(wordcloud, interpolation='bilinear')
            plt.axis('off')
            plt.title('가장 많이 요구되는 기술')
            plt.tight_layout()
            
            if save_path:
                plt.savefig(save_path)
                print(f"워드클라우드가 {save_path}에 저장되었습니다.")
            
            plt.show()
            
        except ImportError:
            print("워드클라우드 생성을 위해 wordcloud 패키지가 필요합니다. pip install wordcloud로 설치하세요.")

# 실행 예시
if __name__ == '__main__':
    # 스크래핑 실행
    scraper = JobScraper('https://example-job-site.com', max_pages=3)
    scraper.scrape_all_pages()
    
    # CSV로 저장
    job_df = scraper.save_to_csv('job_data.csv')
    
    # 데이터 분석
    analyzer = JobDataAnalyzer(job_df)
    analyzer.clean_data()
    
    # 기본 통계 출력
    stats = analyzer.get_basic_stats()
    print(f"총 공고 수: {stats['total_jobs']}")
    print(f"총 회사 수: {stats['companies']}")
    
    # 가장 많은 공고가 있는 상위 5개 지역
    print("\n채용이 많은 지역:")
    for location, count in list(stats['locations'].items())[:5]:
        print(f"  - {location}: {count}개")
    
    # 가장 많이 요구되는 기술
    print("\n가장 많이 요구되는 기술:")
    for skill, count in stats['top_skills'].items():
        print(f"  - {skill}: {count}회")
    
    # 시각화
    analyzer.plot_location_distribution('location_distribution.png')
    analyzer.plot_salary_distribution('salary_distribution.png')
    analyzer.plot_skills_wordcloud('skills_wordcloud.png')

이 프로젝트 예제는 다음과 같은 기능을 제공합니다:

  1. 여러 페이지에서 구인 정보 수집
  2. 복잡한 데이터 구조 (급여 범위, 기술 리스트 등) 처리
  3. 데이터 정제 및 기본 통계 분석
  4. 다양한 시각화 (막대 그래프, 히스토그램, 워드클라우드)

 

10. 웹 스크래핑 고급: 동적 콘텐츠 처리하기

최신 웹사이트는 JavaScript를 사용해 동적으로 콘텐츠를 로드하기 때문에 BeautifulSoup이나 Scrapy만으로는 접근하기 어려울 수 있습니다. 이런 사이트를 스크래핑하기 위한 고급 기법을 알아보겠습니다.

Selenium을 이용한 동적 웹페이지 스크래핑

Selenium은 브라우저를 자동화하는 도구로, JavaScript가 실행된 후의 페이지 상태에 접근할 수 있습니다.

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager
import time
import pandas as pd

# Selenium 설정
def setup_driver():
    options = Options()
    options.add_argument('--headless')  # 헤드리스 모드 (화면 표시 없음)
    options.add_argument('--no-sandbox')
    options.add_argument('--disable-dev-shm-usage')
    options.add_argument('--disable-gpu')
    options.add_argument('--window-size=1920,1080')
    options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36')
    
    # 최신 ChromeDriver 자동 설치
    service = Service(ChromeDriverManager().install())
    driver = webdriver.Chrome(service=service, options=options)
    return driver

# 동적 페이지 스크래핑
def scrape_dynamic_page(url):
    driver = setup_driver()
    results = []
    
    try:
        driver.get(url)
        
        # 페이지 로딩 대기
        WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.CLASS_NAME, 'product-item'))
        )
        
        # "더 보기" 버튼이 있는 경우 더 많은 콘텐츠 로드
        try:
            while True:
                load_more_button = driver.find_element(By.CLASS_NAME, 'load-more-button')
                if load_more_button.is_displayed():
                    load_more_button.click()
                    # 추가 콘텐츠 로드 대기
                    time.sleep(2)
                else:
                    break
        except:
            # 더 보기 버튼이 더 이상 없음
            pass
        
        # 모든 제품 항목 가져오기
        product_elements = driver.find_elements(By.CLASS_NAME, 'product-item')
        
        for product in product_elements:
            try:
                name = product.find_element(By.CLASS_NAME, 'product-name').text
                price = product.find_element(By.CLASS_NAME, 'product-price').text
                rating = product.find_element(By.CLASS_NAME, 'product-rating').text
                
                # 제품 상세 정보를 위한 클릭 (필요시)
                # product.click()
                # time.sleep(1)
                # description = driver.find_element(By.CLASS_NAME, 'product-description').text
                # driver.back()
                
                results.append({
                    'name': name,
                    'price': price,
                    'rating': rating
                })
            except Exception as e:
                print(f"제품 데이터 추출 오류: {e}")
        
    except Exception as e:
        print(f"스크래핑 오류: {e}")
    
    finally:
        driver.quit()
    
    return results

# API 요청을 통한

requestsHTML을 활용한 간단한 자바스크립트 렌더링

requests-HTML은 Python requests 라이브러리의 확장으로, 기본적인 JavaScript 렌더링 기능을 제공합니다.

from requests_html import HTMLSession
import pandas as pd

def scrape_with_requests_html(url):
    session = HTMLSession()
    r = session.get(url)
    
    # JavaScript 렌더링 (Chromium 사용)
    r.html.render(sleep=2, timeout=20)
    
    # 데이터 추출
    products = []
    product_elements = r.html.find('.product-item')
    
    for item in product_elements:
        name_elem = item.find('.product-name', first=True)
        price_elem = item.find('.product-price', first=True)
        
        name = name_elem.text if name_elem else 'N/A'
        price = price_elem.text if price_elem else 'N/A'
        
        products.append({
            'name': name,
            'price': price
        })
    
    return products

Playwright를 이용한 최신 방식의 브라우저 자동화

Playwright는 Microsoft에서 개발한 최신 브라우저 자동화 도구로, 크로스 브라우저 지원과 강력한 기능을 제공합니다.

from playwright.sync_api import sync_playwright
import pandas as pd

def scrape_with_playwright(url):
    results = []
    
    with sync_playwright() as p:
        # 브라우저 시작 (Chrome, Firefox, WebKit 선택 가능)
        browser = p.chromium.launch(headless=True)
        page = browser.new_page(user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36')
        
        try:
            # 페이지 로드
            page.goto(url)
            
            # 콘텐츠 로드 대기
            page.wait_for_selector('.product-item')
            
            # 무한 스크롤 처리 (필요시)
            for _ in range(5):  # 5번 스크롤 예시
                page.evaluate('window.scrollTo(0, document.body.scrollHeight)')
                page.wait_for_timeout(1000)  # 1초 대기
            
            # 데이터 추출
            product_elements = page.query_selector_all('.product-item')
            
            for product in product_elements:
                name = product.query_selector('.product-name').inner_text()
                price = product.query_selector('.product-price').inner_text()
                
                results.append({
                    'name': name,
                    'price': price
                })
            
        except Exception as e:
            print(f"스크래핑 오류: {e}")
        
        finally:
            browser.close()
    
    return results

 

이상으로 이번 포스트를 마무리 하려고 합니다. 파이썬을 이용한 웹 스크래핑은 데이터 수집 자동화를 위한 강력한 도구입니다. BeautifulSoup은 간단한 프로젝트에 적합한 직관적인 파서이며, Scrapy는 대규모 크롤링 프로젝트에 적합한 완전한 프레임워크입니다. 두 라이브러리의 장단점을 이해하고 프로젝트의 요구 사항에 맞게 선택하는 것이 중요합니다.

이 글에서는 기본 개념부터 실전 예제, 그리고 동적 콘텐츠 처리와 같은 고급 기법까지 다양한 내용을 다루었습니다. 웹 스크래핑을 수행할 때는 항상 법적, 윤리적 고려사항을 염두에 두고, 대상 웹사이트에 과도한 부하를 주지 않도록 주의해야 합니다. 이상으로 포스팅을 마치겠습니다. 감사합니다. 🙂

 

댓글 남기기