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()
주요 기능:
- 자동 포스터 추출: IMDB 페이지에서 포스터 이미지 URL을 자동으로 찾습니다
- 고해상도 이미지: 가능한 경우 고해상도 포스터를 다운로드합니다
- 안전한 파일명: 특수문자를 제거하고 안전한 파일명을 생성합니다
- 오류 처리: 네트워크 오류나 이미지 찾기 실패 시 적절히 처리합니다
- 결과 저장: 크롤링 결과를 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. 에러 처리 강화
- 각 단계별 상세한 에러 메시지
- 실패 시에도 프로그램 계속 실행
테스트된 선택자들:
- img.ipc-image (가장 일반적)
- div.ipc-poster__poster-image > img
- img.sc-7c0a76a2-0 (최신 클래스)
- img[data-testid="hero-media__poster"]
- img[alt*="poster"] 등
이제 프로그램이 제공해주신 코드와 같은 방식으로 포스터를 찾을 수 있을 것입니다. 특히 ipc-image 클래스를 우선적으로 찾도록 하여 성공률을 높였습니다.
728x90
반응형
'코딩' 카테고리의 다른 글
| 무한 반복 영화로 영어 듣기 공부 - 완전 무료 (0) | 2025.07.05 |
|---|---|
| 프로그램 백업 및 배포 : github + pythonanywhere (0) | 2025.07.05 |
| OOP in Python - Classes, Objects, class methods, monkey patching & more! (0) | 2025.07.02 |
| 파이썬 클래스 & 오브젝트 (한글 요약) (0) | 2025.06.29 |
| django, HTMX, Alpine, Tailwind (0) | 2025.06.26 |