imhamburger 님의 블로그

데이터엔지니어 부트캠프 - 영화데이터 수집 프로그램 만들기 (6주차) 본문

데이터엔지니어 부트캠프

데이터엔지니어 부트캠프 - 영화데이터 수집 프로그램 만들기 (6주차)

imhamburger 2024. 8. 18. 01:31

영화진흥위원회에서 영화데이터를 불러와 영화데이터를 수집하는 프로그램을 만들었다.

영화진흥위원회에서 제공하는 영화데이터가 아래와 같이 다양하게 있다.

 

이중에서 영화목록 ~ 영화인 상세정보를 연도별로 데이터를 저장하는 프로그램을 만들도록 하자.

프로그램을 실행했을 때 아래와 같이 나타나도록 할 것이다.

tests/test_movie.py 데이터가 이미 존재합니다: data/movies/year=2015/data.json
데이터가 이미 존재합니다: data/movies/year=2016/data.json
데이터가 이미 존재합니다: data/movies/year=2017/data.json
데이터가 이미 존재합니다: data/movies/year=2018/data.json
데이터가 이미 존재합니다: data/movies/year=2019/data.json
데이터가 이미 존재합니다: data/movies/year=2020/data.json
 52%|██████████████████████████▎                        | 96/186 [00:40<00:36,  2.45it/s]

 

json 파일형태로 저장할 것이며, 데이터가 이미 존재한다면 skip, 존재하지 않다면 저장을 진행할 것이다.

해야할 목록은 다음과 같다.

  • v0.1 - 영화목록 데이터를 저장하는 코드 완성 
  • v0.2 - 위 v0.1 코드 중 이미 다운 받은 data 는 skip 하도록 코드 완성
  • v0.3 - 위 v0.2 에서 다운받아 저장한 영화목록(data.json) 을 연도별로 읽어서 movieCd(영화코드) 를 추출하고 LOOP 돌면서 "영화 상세정보" API 를 조회하여 저장
  • v0.4 - 영화사목록 위와 같은 방식으로 받아 저장
  • v0.5 - 영화사 상세정보 위와 같은 방식으로 받아 저장
  • v0.6 - 영화인목록 위와 같은 방식으로 받아 저장
  • v0.7 - 영화인 상세정보 위와 같은 방식으로 받아 저장

 

v0.1 - 영화목록 데이터를 저장하는 코드 완성

import requests
import os
import json
import time
from tqdm import tqdm

API_KEY = os.getenv('MOVIE_API_KEY')

#파일저장 경로를 생성하고 json파일로 저장
def save_json(data, file_path):
    #파일저장 경로 mkdir
    os.makedirs(os.path.dirname(file_path), exist_ok=True)

    with open(file_path, 'w', encoding='utf-8') as f:
        json.dump(data, f, indent=4, ensure_ascii=False)
    pass

def req(url):
    r = requests.get(url)
    j = r.json()
    return j

 

1. `with open(file_path, 'w', encoding='utf=8') as f`:

  • 파일을 열기 위한 open() 함수 사용
  • file_path는 파일의 경로와 이름을 나타내는 변수
  • 'w'는 파일을 쓰기 모드로 열겠다는 의미. 파일이 이미 존재하면 그 내용을 덮어쓰고, 파일이 존재하지 않으면 새로 생성
  • encoding='utf-8'은 파일을 UTF-8 인코딩으로 열겠다는 의미. UTF-8은 대부분의 언어를 지원하는 범용적인 인코딩 방식
  • with 키워드는 파일을 열고 작업이 끝난 후 자동으로 파일을 닫도록 해줌. 이를 통해 파일을 명시적으로 닫지 않아도 파일 리소스를 안전하게 관리할 수 있다.

2. `json.dump(data, f, indent=4, ensure_ascii=False)`:

  • json.dump() 함수는 Python 객체를 JSON 형식으로 파일에 기록한다.
  • data는 JSON으로 저장할 Python 객체 (내가 지정한 python 객체명이다.)
  • f는 파일 객체로, 앞에서 open()을 통해 생성한 파일 "as f"
  • indent=4는 JSON 파일을 저장할 때 들여쓰기를 4칸으로 하여 가독성을 높이겠다는 의미. 이렇게 하면 JSON 데이터가 계층적으로 잘 정렬되어 사람이 읽기 쉽게 저장된다.
  • ensure_ascii=False는 JSON을 UTF-8 인코딩으로 저장하도록 설정한다. 이 옵션이 없으면 기본적으로 비 ASCII 문자는 이스케이프된 ASCII 형식(예: 유니코드)으로 저장된다.

3. r = requests.get(url):

  • requests 를 이용하여 url을 호출
  • r.json(): 호출한 url을 JSON으로 변환

이 코드의 핵심은 json.dump()를 사용하여 Python 데이터를 JSON 파일로 저장하는 것이다.

 

def save_movies(year=2015, per_page=10, sleep_time=1):
    home_path = os.path.expanduser("~")
    file_path = f"{home_path}/data/movies/year={year}/data.json"

    #토탈카운트 가져오고 total_pages 계산
    url_base = f"https://kobis.or.kr/kobisopenapi/webservice/rest/movie/searchMovieList.json?key={API_KEY}&openStartDt={year}&openEndDt={year}"
    r = req(url_base + f"&curPage=1")
    tot_cnt = r['movieListResult']['totCnt']
    total_pages = (tot_cnt // per_page) + 1

    #total_pages 만큼 loop 돌면서 API 호출
    all_data = []

    for page in tqdm(range(1, total_pages + 1)):
        time.sleep(sleep_time)
        r = req(url_base + f"&curPage={page}")
        d = r['movieListResult']['movieList']
        all_data.extend(d)

    save_json(all_data, file_path)
    
    return True

 

1. 함수 매개변수 

  • year=2015: 데이터를 가져올 연도를 나타낸다. 기본값은 2015년으로 설정되어 있다.
  • per_page=10: 한 페이지에 가져올 영화 데이터의 개수. 기본값은 10
  • sleep_time=1: API 요청 간의 지연 시간을 초 단위로 설정. 기본값은 1초.

2. 파일 경로 설정

  • home_path는 사용자의 홈 디렉토리 경로
  • file_path는 영화 데이터를 저장할 파일의 경로 설정. 예를 들어, 2015년의 데이터는 ~/data/movies/year=2015/data.json에 저장. 위의 with open(file_path, 'w', encoding='utf=8') 의 file_path가 이 file_path 이다.

3. 호출할 영화데이터를 페이지 수를 계산하여 수집

페이지 수를 계산해야하는 이유는 영화진흥위원회에서 제공하는 default 페이지가 10페이지이기 때문에 모든 페이지를 불러와 저장하기 위함이다.

  • url_base는 API 호출의 기본 URL을 설정. 여기서 API_KEY는 API 접근을 위한 키이다.
  • req(url_base + f"&curPage=1")를 통해 첫 번째 페이지의 데이터를 가져온다.
  • tot_cnt는 해당 연도의 총 영화 개수이다. (아래는 영화진흥위원회에서 제공하는 JSON파일 이미지캡쳐본)

  • total_pages는 모든 데이터를 가져오기 위해 필요한 페이지 수를 계산. tot_cnt // per_page는 정수 나눗셈으로 페이지 수를 구하고, 여기에 1을 더해 총 페이지 수를 계산.
  • all_data는 모든 페이지에서 가져온 영화 데이터를 저장할 리스트
  • tqdm(range(1, total_pages + 1))는 현재 페이지 진행 상황을 시각적으로 표현 (아래는 tqdm을 사용한 기능)
52%|██████████████████████████▎                        | 96/186 [00:40<00:36,  2.45it/s]
  • time.sleep(sleep_time)은 API 호출 사이의 지연 시간을 설정하여 서버에 부담을 줄인다.
  • all_data.extend(d): 각 페이지의 데이터(movieList)를 가져와 all_data 리스트에 추가 (extend 메서드 사용)
  • return True: 함수가 정상적으로 완료되었음을 나타내기 위해 True를 반환

지정된 연도에 해당하는 모든 영화 데이터를 API를 통해 가져와 로컬 디스크에 JSON 파일로 저장하고, 각 단계에서 데이터를 안전하게 가져오기 위해 지연 시간(sleep_time)을 설정해 서버 과부하를 방지한다. 또한, tqdm을 사용하여 진행 상태를 시각적으로 확인할 수 있다.

 

 

v0.2 - 위 v0.1 코드 중 이미 다운 받은 data 는 skip 하도록 코드 완성

def save_movies(year=2015, per_page=10, sleep_time=1):
	home_path = os.path.expanduser("~")
    file_path = f"{home_path}/data/movies/year={year}/data.json"

        #위 경로가 있으면 API 호출을 멈추고 프로그램 종료
        if os.path.exists(file_path):
            print(f"파일이 이미 존재합니다. (연도: {year})")
            continue
        else:
            print(f"데이터를 저장합니다. (연도: {year})")

    #토탈카운트 가져오고 total_pages 계산
    url_base = f"https://kobis.or.kr/kobisopenapi/webservice/rest/movie/searchMovieList.json?key={API_KEY}&openStartDt={year}&openEndDt={year}"
...
...
...

 

1. if문 추가

  • if os.path.exists(file_path):는 지정한 경로에 파일이 이미 존재하는지 확인
  • 파일이 존재하면, 이미 데이터가 저장되어 있다는 메시지를 출력하고, continue를 사용하여 다음 연도로 넘어간다. 이로 인해 API 호출을 하지 않고 다음 연도로 넘어가게 된다.
  • 파일이 존재하지 않는 경우, 데이터 저장을 시작하겠다는 메시지를 출력

 

v0.3 - 위 v0.2 에서 다운받아 저장한 영화목록(data.json) 을 연도별로 읽어서 movieCd(영화코드) 를 추출하고 LOOP 돌면서 "영화 상세정보" API 를 조회하여 저장

 

이 부분은 더 세분화해서 진행하였다. 먼저,  영화목록(data.json) 을 연도별로 저장하는 코드를 추가하였다.

 

v0.3 - 1 영화목록(data.json) 을 연도별로 저장하는 코드 추가

def save_movies(start_year=2014, end_year=2021, per_page=10, sleep_time=1):

    #연도별 저장
    for year in range(start_year, end_year + 1):
        home_path = os.path.expanduser("~")
        file_path = f"{home_path}/data/movies/year={year}/data.json"

        #위 경로가 있으면 API 호출을 멈추고 프로그램 종료
        if os.path.exists(file_path):
            print(f"파일이 이미 존재합니다. (연도: {year})")
            continue
        else:
            print(f"데이터를 저장합니다. (연도: {year})")

 

1. 함수 매개변수 start_year와 end_year 추가

  • start_year=2014: 데이터 수집을 시작할 연도
  • end_year=2021: 데이터 수집을 종료할 연도

2. 연도별 저장을 위해 for문으로 감싸기

  • for year in range(start_year, end_year + 1): 루프를 사용하여 start_year부터 end_year까지 각 연도에 대해 반복. range() 함수는 시작 연도부터 종료 연도까지의 범위를 생성

결과

 

 

v0.3 - 2 연도별로 읽어서 movieCd(영화코드) 를 추출하고 LOOP 돌면서 "영화 상세정보" API 를 조회하여 저장

 

파이썬 파일 하나에 한 기능만 넣고 기능별로 분리하고 싶었다. 따라서 새로운 파이썬 파일을 만들어 진행하였다.

import requests
import os
import json
import time
from tqdm import tqdm

API_KEY = os.getenv('MOVIE_API_KEY')

def extract_movie_list_json(movieCd):

    home_path = os.path.expanduser("~")
    start_year = 2015
    end_year = 2021

    #모든 연도의 movieCd를 저장할 리스트
    all_moviecd = []

    for year in range(start_year, end_year + 1):
        movie_list_path=f"{home_path}/data/movies/year={year}/data.json"


    #저장하였던 JSON 파일 열기
    if os.path.exists(movie_list_path):
        with open(movie_list_path, 'r', encoding='utf-8') as f:
            data = json.load(f)

        #JSON파일에서 MovieCd 추출하기
        for key in data:
            if movieCd in key:
                all_moviecd.append({"year": year, "movieCd": key[movieCd]})
    else:
        print(f"{movie_list_path} 파일이 존재하지 않습니다.")

    return all_moviecd

 

1. 홈 경로 및 연도 범위 설정

  • home_path는 사용자의 홈 디렉토리 경로로 os.path.expanduser("~")를 사용하여 홈 디렉토리의 절대 경로로 사용할 수 있다.
  • start_year와 end_year는 데이터를 가져올 연도 범위를 설정

2. movieCd 저장

  • all_moviecd는 모든 연도에서 추출한 movieCd를 저장할 빈 리스트

3. 연도별로 JSON 파일 열기

  • for year in range(start_year, end_year + 1): 루프를 사용하여 start_year부터 end_year까지의 각 연도에 대해 반복
  • movie_list_path는 연도별 JSON 파일의 경로를 설정. 예를 들어, 2015년의 파일 경로는 ~/data/movies/year=2015/data.json

4. 파일 존재 확인 및 JSON 파일 열기

  • if os.path.exists(movie_list_path):는 지정된 경로에 파일이 존재하는지 확인
  • 파일이 존재하면, 파일을 열고 JSON 데이터를 읽어 data 변수에 저장
  • with open(movie_list_path, 'r', encoding='utf-8') as f:를 사용하여 파일을 읽기 모드로 열고, json.load(f)로 JSON 데이터를 파이썬 객체로 변환
  • 파일이 존재하지 않을 경우, '파일이 존재하지 않습니다.' 출력

5. JSON 파일에서 movieCd 추출하기

  • all_moviecd.append({"year": year, "movieCd": key[movieCd]})는 movieCd가 존재할 경우, 연도와 movieCd를 포함한 딕셔너리를 all_moviecd 리스트에 추가

각 연도의 JSON 파일에서 특정 movieCd를 추출하여 리스트로 반환한다. 주요 작업은 각 연도의 파일을 열고, 파일에서 원하는 movieCd를 찾는 것이다. 반환된 리스트는 연도와 movieCd를 포함한 딕셔너리 형태로 저장된다.

 

#영화 상세정보 url
def req(url):
    r = requests.get(url)
    j = r.json()
    return j


#영화 상세정보 저장
def save_movies_info():
    movie_code_key = 'movieCd'
    extract_movie_code = extract_movie_list_json(movie_code_key)

    movie_info_by_year = {}

    for key in tqdm(extract_movie_code):
        year = key['year']
        code = key['movieCd']
        home_path = os.path.expanduser("~")
        file_path = f"{home_path}/data/movies/year={year}/movie_info.json"

        #영화 정보가 이미 연도의 movie_info.json에 저장되어 있는지 확인
        if year not in movie_info_by_year:
            movie_info_by_year[year] = []

            #기존 movie_info.json이 존재하면 로드
            if os.path.exists(file_path):
                with open(file_path, 'r', encoding='utf-8') as f:
                    movie_info_by_year[year] = json.load(f)

       #중복 확인(이미 저장된 movieCd인지 확인)
        if any(movie.get('movieCd') == code for movie in movie_info_by_year[year]):
           print(f"영화 정보가 이미 존재합니다: {year}년 {code}")
           continue

        #API 호출
        url_base = f"http://www.kobis.or.kr/kobisopenapi/webservice/rest/movie/searchMovieInfo.json?key={API_KEY}&movieCd={code}"
        movie_info = req(url_base).get('movieInfoResult', {}).get('movieInfo', {})

        #연도별로 영화상세정보 리스트에 다 저장
        movie_info_by_year[year].append(movie_info)

     #데이터를 연도별로  json 파일로 저장
     for year, movie_info_list in movie_info_by_year.items():
        file_path = f"{home_path}/data/movies/year={year}/movie_info.json"
        save_json(movie_info_list, file_path)
        print(f"영화 정보를 저장했습니다: {year}년 {code}")
            
return True

 

1. API 요청

  • req(url) 함수는 주어진 URL로 HTTP GET 요청을 보내고, 응답을 JSON 형식으로 반환

2. 영화코드 추출 및 데이터 구조 초기화

  • movie_code_key는 JSON에서 영화 코드를 찾기 위한 키
  • extract_movie_code는 이전에 작성된 extract_movie_list_json() 함수를 호출하여 모든 영화 코드와 연도 추출
  • movie_info_by_year는 연도별로 영화 정보를 저장할 빈 딕셔너리. 

3. 영화 정보 수집 및 저장

  • for key in tqdm(extract_movie_code):는 추출된 영화 코드와 연도를 반복
  • year와 code는 현재 항목의 연도와 영화 코드를 가져온다.
  • file_path는 해당 연도의 영화 정보가 저장될 JSON 파일의 경로를 설정

4. 연도별 저장될 영화데이터 초기값 설정

  • if year not in movie_info_by_year:는 현재 연도가 movie_info_by_year 딕셔너리에 존재하지 않으면 빈 리스트를 생성
  • 기존에 영화 정보 JSON 파일이 존재하면 파일을 열어 기존 데이터를 로드하여 movie_info_by_year[year]에 저장

5. 중복 확인

  • if any(movie.get('movieCd') == code for movie in movie_info_by_year[year]):는 현재 영화 코드가 이미 저장된 영화 정보에 존재하는지 확인
  • 중복된 영화 정보가 있을 경우, 해당 연도와 영화 코드에 대한 메시지를 출력하고 다음 항목으로 넘어간다.

6. API로 요청할 url 설정

  • url_base는 API 요청 URL을 설정. 영화 코드(code)를 포함하여 영화 상세 정보를 가져온다.
  • movie_info는 API 호출을 통해 가져온 영화 상세 정보. req(url_base)를 호출하여 JSON 응답에서 영화 정보를 추출 (get 메서드 이용)
  • movie_info_by_year[year].append(movie_info)는 가져온 영화 정보를 해당 연도의 리스트에 추가

7. 연도별 JSON 파일로 저장

  • for year, movie_info_list in movie_info_by_year.items():는 movie_info_by_year 딕셔너리에서 연도와 영화 정보 리스트를 가져온다.
  • file_path는 해당 연도의 JSON 파일 경로 
  • save_json(movie_info_list, file_path)는 연도별로 영화 정보를 JSON 파일로 저장

이 함수는 주어진 연도 범위의 영화 정보를 API를 통해 가져와 연도별로 JSON 파일에 저장합니다. 중복된 영화 정보를 방지하고, 이미 존재하는 데이터는 로드하여 덮어쓰지 않으며, 최종적으로 각 연도별로 업데이트된 영화 정보를 JSON 파일에 저장한다.

 

 

추가해야할 코드...

 

API 요청 오류 처리 추가

  • req(url) 함수에서 response.raise_for_status()를 사용하여 API 요청이 실패했을 때 예외 발생

 

일주일을 보내면서...

코드를 작성하면서 명확한 구조와 일관성 있는 변수 명명의 중요성을 다시 한 번 깨달았다. 잘 정의된 함수와 일관된 변수 이름은 코드의 가독성을 높인다는 것... 예를 들어, 함수와 변수 이름을 명확하게 작성하면 코드의 의도를 쉽게 파악할 수 있고 나도 기억하기 쉽다.. 그리고 API 호출 시 효율적인 데이터 처리와 오류 처리는 매우 중요한 것 같다. 실제 환경에서는 네트워크 오류나 API 서버의 문제로 인해 요청이 실패할 수 있는데, requests 라이브러리의 raise_for_status()를 사용하여 요청이 실패했을 때 오류 메시지를 출력하고, 프로그램이 중단되지 않도록 하는 것이 중요한 것 같다. 나는 추가하지 않아서 계속 오류가 나길래 헤맸는데 알고보니 API 요청 때 필요한 키값이 하루치를 다 써서 호출이 되지 않았었다.. 항상 여러 상황을 고려하여 코드를 짜는걸로!!

 

앞으로 나의 방향

파이썬 코드를 작성하면서 정말 어렵다는 것을 느꼈다. 이전까지는 간단한? 코드만 짜고 그랬어서 잘 헤쳐나갈 수 있었지만 이번에는 코드가 더 길어지니 뭐부터 해야할지..막막했던 것 같다.. 코드의 길이가 길어지면서 복잡해지고, 단순히 기능을 추가하는 것만으로는 문제가 해결되지 않았다. 코드가 길어지면 각 부분이 서로 어떻게 연결되는지 파악하는 것이 더 어려워지고, 이로 인해 전체적인 구조를 이해하고, 수정할 부분을 찾는 데 어려움을 겪었다.

길어진 코드를 효과적으로 다루기 위해서는 체계적인 계획이 필요하다는 것을 느꼈다. 코드 작성 전에 명확한 설계와 계획이 없으면, 구현 과정에서 많은 혼란을 겪을 수 있을 것 같다. 기능별로 나누어 작업하고, 각 단계별로 목표를 설정하는 것이 중요하다는 것을 깨달았다. 앞으로는 코드의 구조를 체계적으로 설계하는 방향으로 진행하는걸루...