imhamburger 님의 블로그
데이터엔지니어 부트캠프 - 영화데이터 수집 프로그램 만들기 (6주차) 본문
영화진흥위원회에서 영화데이터를 불러와 영화데이터를 수집하는 프로그램을 만들었다.
영화진흥위원회에서 제공하는 영화데이터가 아래와 같이 다양하게 있다.
이중에서 영화목록 ~ 영화인 상세정보를 연도별로 데이터를 저장하는 프로그램을 만들도록 하자.
프로그램을 실행했을 때 아래와 같이 나타나도록 할 것이다.
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 요청 때 필요한 키값이 하루치를 다 써서 호출이 되지 않았었다.. 항상 여러 상황을 고려하여 코드를 짜는걸로!!
앞으로 나의 방향
파이썬 코드를 작성하면서 정말 어렵다는 것을 느꼈다. 이전까지는 간단한? 코드만 짜고 그랬어서 잘 헤쳐나갈 수 있었지만 이번에는 코드가 더 길어지니 뭐부터 해야할지..막막했던 것 같다.. 코드의 길이가 길어지면서 복잡해지고, 단순히 기능을 추가하는 것만으로는 문제가 해결되지 않았다. 코드가 길어지면 각 부분이 서로 어떻게 연결되는지 파악하는 것이 더 어려워지고, 이로 인해 전체적인 구조를 이해하고, 수정할 부분을 찾는 데 어려움을 겪었다.
길어진 코드를 효과적으로 다루기 위해서는 체계적인 계획이 필요하다는 것을 느꼈다. 코드 작성 전에 명확한 설계와 계획이 없으면, 구현 과정에서 많은 혼란을 겪을 수 있을 것 같다. 기능별로 나누어 작업하고, 각 단계별로 목표를 설정하는 것이 중요하다는 것을 깨달았다. 앞으로는 코드의 구조를 체계적으로 설계하는 방향으로 진행하는걸루...
'데이터엔지니어 부트캠프' 카테고리의 다른 글
데이터엔지니어 부트캠프 - 두번째 팀프로젝트 (8/26~8/28) (8주차) (12) | 2024.09.01 |
---|---|
데이터엔지니어 부트캠프 - 카프카(Kafka) 이해하기 (7주차) (1) | 2024.08.25 |
데이터엔지니어 부트캠프 - 아파치 스파크(Apache Spark) 이해하기, 에어플로우에 적용시키기 (5주차) (1) | 2024.08.11 |
데이터엔지니어 부트캠프 - 첫번째 팀프로젝트 (8/2~8/6) 2-3일차 (0) | 2024.08.05 |
데이터엔지니어 부트캠프 - 첫번째 팀프로젝트 (8/2~8/6) 1일차 (1) | 2024.08.04 |