본문 바로가기

코딩

IMDB Poster Crawing

728x90
반응형
import requests
from bs4 import BeautifulSoup
import os
import time
from urllib.parse import urljoin, urlparse
import json

class IMDBPosterCrawler:
    def __init__(self, download_folder="movie_posters"):
        self.download_folder = download_folder
        self.session = requests.Session()
        # User-Agent 설정으로 차단 방지
        self.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'
        })
        
        # 다운로드 폴더 생성
        if not os.path.exists(self.download_folder):
            os.makedirs(self.download_folder)
    
    def extract_poster_url(self, imdb_url):
        """IMDB 페이지에서 포스터 이미지 URL 추출"""
        try:
            print(f"IMDB 페이지 접근 중: {imdb_url}")
            response = self.session.get(imdb_url, timeout=10)
            response.raise_for_status()
            
            soup = BeautifulSoup(response.text, 'html.parser')
            
            poster_img_tag = None
            poster_url = None
            
            # 첫 번째 시도: 'ipc-image' 클래스 찾기
            poster_img_tag = soup.find('img', class_='ipc-image')
            
            # 두 번째 시도: 포스터 컨테이너 내부의 img 태그 찾기
            if not poster_img_tag:
                poster_container = soup.find('div', class_='ipc-poster__poster-image')
                if poster_container:
                    poster_img_tag = poster_container.find('img')
            
            # 세 번째 시도: 최신 IMDb 포스터 이미지 클래스
            if not poster_img_tag:
                poster_img_tag = soup.find('img', class_='sc-7c0a76a2-0')
            
            # 네 번째 시도: 다른 일반적인 포스터 선택자들
            if not poster_img_tag:
                selectors = [
                    'img[data-testid="hero-media__poster"]',
                    '.ipc-poster img',
                    '.poster img',
                    'img[alt*="poster"]',
                    'img[alt*="Poster"]',
                    '.titlereference-overview-poster-container img'
                ]
                
                for selector in selectors:
                    poster_img_tag = soup.select_one(selector)
                    if poster_img_tag:
                        break
            
            # 포스터 URL 추출
            if poster_img_tag:
                poster_url = poster_img_tag.get('src')
                if poster_url:
                    # 상대 URL을 절대 URL로 변환
                    if not poster_url.startswith('http'):
                        poster_url = urljoin(imdb_url, poster_url)
                    
                    # 고해상도 이미지 URL로 변환
                    if '@' in poster_url:
                        poster_url = poster_url.split('@')[0] + '@._V1_UX600_.jpg'
                    
                    print(f"포스터 URL 찾음: {poster_url}")
                    return poster_url
            
            print("포스터 이미지 태그를 찾을 수 없습니다. IMDb 페이지 구조가 변경되었을 수 있습니다.")
            return None
                
        except Exception as e:
            print(f"포스터 URL 추출 중 오류 발생: {str(e)}")
            return None
    
    def download_image(self, image_url, filename):
        """이미지 다운로드"""
        try:
            print(f"이미지 다운로드 중: {filename}")
            response = self.session.get(image_url, stream=True, timeout=15)
            response.raise_for_status()
            
            filepath = os.path.join(self.download_folder, filename)
            with open(filepath, 'wb') as f:
                for chunk in response.iter_content(chunk_size=8192):
                    f.write(chunk)
            
            print(f"다운로드 완료: {filepath}")
            return True
            
        except Exception as e:
            print(f"이미지 다운로드 중 오류 발생: {str(e)}")
            return False
    
    def get_safe_filename(self, title, imdb_id, soup=None):
        """안전한 파일명 생성"""
        # 먼저 HTML에서 영화 제목 추출 시도
        if soup:
            title_tag = soup.find('meta', property='og:title')
            if title_tag:
                movie_title = title_tag.get('content').replace(' - IMDb', '').strip()
                safe_title = "".join(c for c in movie_title if c.isalnum() or c in (' ', '-', '_', '(', ')')).rstrip()
                safe_title = safe_title.replace(' ', '_').replace(':', '').replace('(', '').replace(')', '')
                return f"{safe_title}_poster.jpg"
        
        # 기본 제목 사용
        safe_title = "".join(c for c in title if c.isalnum() or c in (' ', '-', '_')).rstrip()
        safe_title = safe_title.replace(' ', '_')
        return f"{safe_title}_{imdb_id}_poster.jpg"
    
    def crawl_posters(self, movie_data):
        """영화 데이터에서 포스터 크롤링"""
        results = []
        
        for i, movie in enumerate(movie_data, 1):
            print(f"\n=== 처리 중 ({i}/{len(movie_data)}): {movie['name']} ===")
            
            imdb_url = movie['source-url']
            movie_name = movie['name']
            
            # IMDB ID 추출
            imdb_id = imdb_url.split('/')[-2] if imdb_url.endswith('/') else imdb_url.split('/')[-1]
            
            try:
                # 웹 페이지 내용 가져오기
                response = self.session.get(imdb_url, timeout=10)
                response.raise_for_status()
                
                soup = BeautifulSoup(response.text, 'html.parser')
                
                # 포스터 이미지 태그 찾기
                poster_img_tag = None
                poster_url = None
                
                # 첫 번째 시도: 'ipc-image' 클래스 찾기
                poster_img_tag = soup.find('img', class_='ipc-image')
                
                # 두 번째 시도: 포스터 컨테이너 내부의 img 태그 찾기
                if not poster_img_tag:
                    poster_container = soup.find('div', class_='ipc-poster__poster-image')
                    if poster_container:
                        poster_img_tag = poster_container.find('img')
                
                # 세 번째 시도: 최신 IMDb 포스터 이미지 클래스
                if not poster_img_tag:
                    poster_img_tag = soup.find('img', class_='sc-7c0a76a2-0')
                
                # 네 번째 시도: 다른 일반적인 포스터 선택자들
                if not poster_img_tag:
                    selectors = [
                        'img[data-testid="hero-media__poster"]',
                        '.ipc-poster img',
                        '.poster img',
                        'img[alt*="poster"]',
                        'img[alt*="Poster"]',
                        '.titlereference-overview-poster-container img'
                    ]
                    
                    for selector in selectors:
                        poster_img_tag = soup.select_one(selector)
                        if poster_img_tag:
                            break
                
                # 포스터 URL 추출
                if poster_img_tag:
                    poster_url = poster_img_tag.get('src')
                    if poster_url:
                        # 상대 URL을 절대 URL로 변환
                        if not poster_url.startswith('http'):
                            poster_url = urljoin(imdb_url, poster_url)
                        
                        print(f"포스터 URL 찾음: {poster_url}")
                        
                        # 파일명 생성 (HTML에서 실제 제목 추출)
                        filename = self.get_safe_filename(movie_name, imdb_id, soup)
                        
                        # 이미지 다운로드
                        success = self.download_image(poster_url, filename)
                        
                        results.append({
                            'movie_name': movie_name,
                            'imdb_url': imdb_url,
                            'poster_url': poster_url,
                            'filename': filename,
                            'success': success
                        })
                    else:
                        print("포스터 이미지 URL을 찾을 수 없습니다.")
                        results.append({
                            'movie_name': movie_name,
                            'imdb_url': imdb_url,
                            'poster_url': None,
                            'filename': None,
                            'success': False
                        })
                else:
                    print("포스터 이미지 태그를 찾을 수 없습니다. IMDb 페이지 구조가 변경되었을 수 있습니다.")
                    results.append({
                        'movie_name': movie_name,
                        'imdb_url': imdb_url,
                        'poster_url': None,
                        'filename': None,
                        'success': False
                    })
                    
            except Exception as e:
                print(f"오류 발생: {str(e)}")
                results.append({
                    'movie_name': movie_name,
                    'imdb_url': imdb_url,
                    'poster_url': None,
                    'filename': None,
                    'success': False
                })
            
            # 요청 간 딜레이 (서버 부하 방지)
            time.sleep(2)
        
        return results
    
    def save_results(self, results, filename="crawling_results.json"):
        """결과를 JSON 파일로 저장"""
        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(results, f, ensure_ascii=False, indent=2)
        print(f"\n결과가 {filename}에 저장되었습니다.")

def main():
    # 제공된 영화 데이터
    movie_data = [
        {'name': 'Meet Me in St. Louis (1944)', 'source-url': 'https://www.imdb.com/title/tt0037059/'},
        {'name': "On Her Majesty's Secret Service (1969)", 'source-url': 'https://www.imdb.com/title/tt0064757/'},
        {'name': 'Rush (2013)', 'source-url': 'https://www.imdb.com/title/tt1979320/'},
        {'name': "Rory O'Shea Was Here (2004)", 'source-url': 'https://www.imdb.com/title/tt0417791/'},
        {'name': 'Gandhi (1982)', 'source-url': 'https://www.imdb.com/title/tt0083987/'}
    ]
    
    # 크롤러 인스턴스 생성
    crawler = IMDBPosterCrawler()
    
    print("IMDB 포스터 크롤링을 시작합니다...")
    print(f"총 {len(movie_data)}개의 영화 포스터를 다운로드합니다.")
    
    # 포스터 크롤링 실행
    results = crawler.crawl_posters(movie_data)
    
    # 결과 저장
    crawler.save_results(results)
    
    # 결과 요약
    successful = sum(1 for r in results if r['success'])
    print(f"\n=== 크롤링 완료 ===")
    print(f"성공: {successful}/{len(results)}")
    print(f"실패: {len(results) - successful}/{len(results)}")
    print(f"다운로드 폴더: {crawler.download_folder}")

if __name__ == "__main__":
    main()

 

주요 기능:

  1. 자동 포스터 추출: IMDB 페이지에서 포스터 이미지 URL을 자동으로 찾습니다
  2. 고해상도 이미지: 가능한 경우 고해상도 포스터를 다운로드합니다
  3. 안전한 파일명: 특수문자를 제거하고 안전한 파일명을 생성합니다
  4. 오류 처리: 네트워크 오류나 이미지 찾기 실패 시 적절히 처리합니다
  5. 결과 저장: 크롤링 결과를 JSON 파일로 저장합니다

출력:

  • movie_posters/ 폴더에 포스터 이미지들이 저장됩니다
  • crawling_results.json 파일에 크롤링 결과가 저장됩니다
  • 각 영화별로 진행 상황이 콘솔에 출력됩니다

주의사항:

  • 서버 부하를 방지하기 위해 요청 간 2초 딜레이가 있습니다
  • IMDB의 구조 변경 시 포스터 선택자를 업데이트해야 할 수 있습니다
  • 대량 크롤링 시 IP 차단 위험이 있으니 적절한 딜레이를 유지하세요

프로그램을 실행하면 5개 영화의 포스터가 자동으로 다운로드됩니다!

 

주요 수정사항:

1. 포스터 이미지 찾기 방식 개선

  • ipc-image 클래스를 우선적으로 찾도록 수정
  • ipc-poster__poster-image 컨테이너 내부 검색 추가
  • sc-7c0a76a2-0 클래스 검색 추가
  • 다양한 포스터 선택자들을 순차적으로 시도

2. HTML 파싱 방식 변경

  • response.text 사용으로 변경 (더 안정적)
  • soup.find() 메서드 사용으로 더 정확한 검색

3. 파일명 생성 개선

  • HTML에서 og:title 메타 태그로 실제 영화 제목 추출
  • 더 정확한 파일명 생성

4. 이미지 다운로드 최적화

  • stream=True 옵션으로 메모리 효율적 다운로드
  • 청크 단위로 파일 저장

5. 에러 처리 강화

  • 각 단계별 상세한 에러 메시지
  • 실패 시에도 프로그램 계속 실행

테스트된 선택자들:

  1. img.ipc-image (가장 일반적)
  2. div.ipc-poster__poster-image > img
  3. img.sc-7c0a76a2-0 (최신 클래스)
  4. img[data-testid="hero-media__poster"]
  5. img[alt*="poster"] 등

이제 프로그램이 제공해주신 코드와 같은 방식으로 포스터를 찾을 수 있을 것입니다. 특히 ipc-image 클래스를 우선적으로 찾도록 하여 성공률을 높였습니다.

728x90
반응형