[SK플래닛] ASAC 빅데이터전문가 11기 | 11일차

오늘은 약간 “데이터를 가져온다”는 말 안에 뭐가 이렇게 많이 들어있지? 싶었던 날이었다.

전날까지는 판다스로 표 만들고, 컬럼 추가하고, 정리하는 느낌이 더 강했는데

오늘은 그 전 단계, 그러니까

  • 어디서 가져올 건지
  • 그게 JSON인지 XML인지 HTML인지
  • 어떤 패키지로 받아야 하는지
  • 받은 뒤에는 뭘 기준으로 접근해야 하는지

이런 쪽이 더 크게 남았다.

특히 오늘 제일 크게 남은 건

같은 API라도 JSON으로 받는 거랑 XML로 받는 건 느낌이 꽤 다르고,

거기서 더 나아가 DART처럼 일반 사이트는 또 완전히 다르다는 점이었다.

즉 오늘은 “수집”이라는 말 안에 공식 API 요청이랑 일반 웹사이트 HTML 접근이 다 들어있다는 걸 좀 더 제대로 본 날 같았다.


1. 오늘은 API랑 일반 사이트 접근이 다르다는 게 먼저 들어왔다

오늘 앞부분에서 다시 정리된 게 이거였다.

API는

명시적으로 데이터를 주겠다고 해놓은 입구다.

그러니까 내가

  • 어떤 주소로
  • 어떤 key를 붙이고
  • 어떤 파라미터를 보낼지

이걸 맞춰서 요청하면

상대가 정해둔 포맷으로 데이터를 주는 구조였다.

그래서 비용 문제만 없다면 제일 깔끔한 방식이라고 정리돼 있었고, 실제로 KOBIS 예시가 딱 그런 느낌이었다.

근데 일반 사이트는 다르다.

DART처럼 사이트는 정보는 보여주지만

“이거 가져가세요~” 하고 API를 딱 준 건 아닌 경우가 있다.

그럴 때는 개발자도구 Network 탭을 켜서

숨겨진 요청 주소나 파라미터를 직접 찾아야 한다는 흐름이 나왔다. DART 예시 페이지 1~2에도 개발자도구 Network와 search.ax, selectDate, currentPage를 찾는 흐름이 그림이랑 같이 정리돼 있었어.

이 부분이 꽤 크게 남았다.

전에는 그냥

“수집 = 크롤링”처럼 생각한 적도 있었는데,

오늘은 확실히

  • 공식 API
  • 일반 HTML 페이지

이건 접근 방식부터 다르구나 싶었다.


2. JSON은 확실히 Python 쪽이랑 잘 맞아서 편했다

KOBIS API에서 JSON으로 받는 쪽은

전날 했던 거랑 연결돼서 그래도 비교적 익숙했다.

받고 나면 결국

  • dict
  • list

이런 Python 자료형처럼 쓸 수 있으니까

키값이랑 정수 인덱스로 들어가면 됐다.

자료에서도 JSON은 파이썬 리스트/딕셔너리 자료형으로 바꾸고, 정수 인덱스와 키값 중심으로 순차 접근한다고 설명돼 있었어.

예를 들면 이런 흐름.

import pandas as pd
import urllib.request
import json

url_p1 = '<http://www.kobis.or.kr/kobisopenapi/webservice/rest/movie/searchMovieList.json>'
key = "내 키"
url_p2 = str(50)

url = url_p1 + "?key=" + key + "&itemPerPage=" + url_p2

movie_page = urllib.request.urlopen(url)
movie_data = json.loads(movie_page.read().decode("utf-8"))

그리고 나면

  • movie_data는 dict
  • movie_data["movieListResult"]도 dict
  • movie_data["movieListResult"]["movieList"]는 list

이렇게 구조가 딱 보였다.

이 부분은 진짜 편했다.

왜냐면

JSON은 일단 Python에서 다루는 방식이 너무 자연스럽다.

그냥 딕셔너리/리스트 보듯이 쭉 들어가면 되니까.


3. 근데 JSON도 값이 비어 있으면 또 얘기가 달랐다

편하긴 했는데

여기서도 또 현실적인 문제가 나왔다.

예를 들어 감독 정보인 directors.

어떤 영화는 감독 정보가 있는데

어떤 영화는 그냥 빈 리스트 []였다.

실제 예시에서도 두 번째 영화에서 감독이 없어서 IndexError가 났고, directors == [] 체크를 따로 넣는 흐름이 있었다.

그래서 결국 이렇게 처리했다.

if movie_data["movieListResult"]["movieList"][i]["directors"] == []:
    print("감독이름없음")
else:
    print(movie_data["movieListResult"]["movieList"][i]["directors"][0]["peopleNm"])

이 부분에서 느낀 건

JSON은 구조는 편한데,

데이터가 비어 있는 건 또 따로 내가 봐야 한다는 점이었다.

즉,

  • JSON이라 편하다 = 구조 얘기
  • 값이 다 있다 = 전혀 아님

이거.

이게 오늘 꽤 남았다.


4. 그래서 영화 하나를 한 줄 샘플로 보고 쌓는 게 더 안전하다는 말이 이해됐다

오늘 좋았던 포인트 중 하나가

영화 한 개를 샘플 하나로 보고, 필요한 정보를 묶어서 쌓는 방식이었다.

처음엔 자꾸

  • 코드만 쭉 뽑고
  • 제목만 쭉 뽑고
  • 감독만 쭉 뽑고

이런 식으로 생각하기 쉬운데,

그렇게 하면 중간에 값이 없는 애가 나올 때 줄이 밀릴 수 있다.

그래서 결국 더 안전한 건

tot_data = []

for idx, data in enumerate(movie_data["movieListResult"]["movieList"]):
    i_code = data["movieCd"]
    i_name = data["movieNm"]
    i_name_e = data["movieNmEn"]
    i_day = data["openDt"]

    if data["directors"] == []:
        i_dir = ""
    else:
        i_dir = data["directors"][0]["peopleNm"]

    tot_data.append([i_code, i_name, i_name_e, i_day, i_dir])

이런 식으로

영화 한 개에서 필요한 값 다 뽑고 → 한 줄로 append

하는 구조였다.

자료에서도 “세로줄 컬럼 베이스보다, 가로줄 샘플 베이스로 처리하는 게 맞다”는 식으로 설명이 나왔어.

이건 진짜 이후에도 계속 중요할 것 같았다.


5. dict로 쌓는 방식도 생각보다 괜찮았다

리스트로 쌓는 것 말고

dict로 쌓는 방식도 나왔다.

tot_data.append({
    "movieCd": i_code,
    "movieTitle": i_name,
    "movieETitle": i_name_e,
    "openDay": i_day,
    "dirName": i_dir
})

이건 딱 보기에

아예 컬럼명이 같이 보이니까

읽는 건 더 편했다.

특히 나처럼 아직 헷갈리는 입장에서는

리스트는 순서를 계속 머릿속에 들고 있어야 하고,

dict는 이름이 바로 보여서 좀 덜 꼬이는 느낌이었다.

강의 녹취에서도 dict 방식은 어차피 DataFrame으로 바꿀 때 컬럼명이 올라와서 편하다는 얘기가 있었어.

이건 나중에 내가 혼자 정리할 땐

dict 쪽을 더 많이 쓸 수도 있겠다 싶었다.


6. XML은 진짜 JSON이랑 느낌이 많이 달랐다

오늘 새로웠던 건 XML이었다.

같은 KOBIS API인데도

주소 끝을 .json이 아니라 .xml로 바꾸면

정보는 비슷한데 포장지가 XML로 온다.

XML은 자료에서 설명한 것처럼

태그 중심 언어였다.

즉 <tag>값</tag> 구조로 감싸고, 값이 없으면 <tag/>처럼 닫기도 한다.

처음엔

“JSON이나 XML이나 비슷하지 않나?”

싶었는데, 막상 만져보니까 다뤄지는 방식이 아예 달랐다.

  • JSON → dict/list 접근
  • XML → 태그 접근

이 차이가 생각보다 컸다.


7. XML은 BeautifulSoup으로 태그를 찾아가야 했다

XML 쪽에서는 BeautifulSoup을 썼다.

import pandas as pd
import urllib.request
from bs4 import BeautifulSoup

url_p1 = '<http://www.kobis.or.kr/kobisopenapi/webservice/rest/movie/searchMovieList.xml>'
key = "내 키"
url_p2 = str(50)
url = url_p1 + "?key=" + key + "&itemPerPage=" + url_p2

res = urllib.request.urlopen(url)
soup = BeautifulSoup(res, "xml")

여기서 중요한 건

이제는 더 이상 json.loads()처럼 자료형 변환이 아니라,

태그를 찾으러 다닌다는 점이었다.

자료에서도 XML은 BeautifulSoup으로 태그 중심 접근을 하게 되고, JSON처럼 자료형을 바꾸는 방식과는 다르다고 정리돼 있어.

이건 확실히 다르게 느껴졌다.

JSON은 코드가

키값 따라 들어가는 느낌이라면,

XML은

트리 구조에서 태그 이름을 찾는 느낌이었다.


8. find랑 find_all 차이가 생각보다 중요했다

오늘 XML에서 제일 남은 건 이거였다.

  • find() → 하나만
  • find_all() → 전부 리스트로

근데 중요한 건 개수보다

없는 태그를 찾았을 때 반응이었다.

find()는 없으면 좀 애매하게 반응해서

예외 처리하기가 불편하고,

find_all()은 없으면 그냥 []를 줘서

오히려 처리하기가 쉬웠다. 이 부분은 자료랑 녹취 둘 다 꽤 강조했어.

이건 JSON 때 빈 리스트 체크하던 거랑 좀 비슷하게 느껴졌다.

결국 XML에서도

없을 수 있다를 염두에 두고 find_all()로 가는 쪽이 더 안전했다.


9. XML도 태그 하나씩 따로 긁는 건 위험할 수 있다는 게 인상적이었다

처음엔 XML도 그냥

[i.text for i in soup.find_all("movieCd")]

이런 식으로 태그를 종류별로 한 번에 모으면 편해 보였다.

근데 오늘 설명에서 제일 와닿았던 건

그렇게 하면 감독 정보처럼 없는 태그가 있을 때 줄이 밀릴 수 있다는 점이었다.

녹취에서도 peopleNm 같은 태그를 그냥 싹 긁어오면 없는 영화는 생략돼서 “이빨이 안 맞는다”고 말했어. 그래서 샘플 단위 처리로 가야 한다고.

그래서 결국 XML도 안전한 건 이런 구조였다.

movie_list = []
for idx, data in enumerate(soup.find_all("movie")):
    i_dict = {"code":"", "title":"", "e-title":"", "openD":"", "dirName":""}
    i_dict["code"] = data.find_all("movieCd")[0].text
    i_dict["title"] = data.find("movieNm").text
    i_dict["e-title"] = data.find("movieNmEn").text
    i_dict["openD"] = data.find("openDt").text

    if data.find_all("peopleNm") != []:
        i_dict["dirName"] = data.find("peopleNm").text
    else:
        i_dict["dirName"] = "X"

    movie_list.append(i_dict)

결국 JSON이든 XML이든

한 영화씩 끊어서 필요한 걸 뽑는 게 제일 안 꼬인다

이게 오늘 공통 핵심으로 남았다.


10. requests는 urllib보다 확실히 좀 더 깔끔해 보였다

후반에는 requests도 나왔다.

솔직히 이건 보자마자

“아 이게 더 낫다”

싶었다 ㅋㅋ

예전에는 URL을 내가 직접 문자열로 이어붙였는데,

requests는 params를 dict로 넘겨주면 알아서 붙여준다.

import pandas as pd
import requests

base_url = '<http://www.kobis.or.kr/kobisopenapi/webservice/rest/movie/searchMovieList.json>'

my_params = {
    "key": "내 키",
    "itemPerPage": 50
}

res = requests.get(base_url, params=my_params)

if res.ok:
    result = res.json()
else:
    print("HTTP 통신 체크~~~")

이거 보면서 좋았던 건

  • URL 조립을 덜 해도 되고
  • 파라미터 구조가 눈에 잘 들어오고
  • res.json()으로 바로 파싱도 되고

이런 점이었다.

자료에서도 requests.get(base_url, params=...)와 res.ok, res.json() 흐름이 별도로 잘 정리돼 있었어.

즉,

urllib는 되는 걸 보여줬고,

requests는 더 깔끔한 방식을 보여준 느낌이었다.


11. DART 쪽은 “숨겨진 요청 규칙 찾기”가 핵심이라 느낌이 또 달랐다

오늘 후반 DART는 또 결이 달랐다.

KOBIS처럼 공식 API를 바로 쓰는 게 아니라,

사이트 내부에서 어떤 요청이 날아가는지 개발자도구 Network에서 눈치껏 찾아야 했다.

DART 코랩 1~2페이지 그림에서도 Network 탭, search.ax, Payload의 currentPage, selectDate 같은 값이 직접 표시돼 있었어.

이게 되게 재밌었음.

예를 들어 날짜랑 페이지를 넣어서 이런 URL을 만들었다.

import requests
from bs4 import BeautifulSoup
import pandas as pd
import re
import time

date = "2026.04.16"
page = "1"
url = f"<https://dart.fss.or.kr/dsac001/mainAll.do?selectDate={date}¤tPage={page}>"

그리고 requests.get(url)로 받으니까

이번엔 JSON이 아니라 그냥 HTML 코드 문자열이 왔다.

자료에서 res.text 예시도 HTML 소스가 그대로 내려오는 걸 보여줘.

즉 DART는

API를 직접 주는 느낌보다는, 사이트가 내부적으로 쓰는 요청 규칙을 찾아서 접근하는 느낌이 강했다.


12. HTML은 XML처럼 태그 중심이긴 한데, 속성까지 같이 봐야 해서 더 복잡했다

DART HTML 파싱에서는 BeautifulSoup(res.text, "html.parser")를 썼다.

soup = BeautifulSoup(res.text, "html.parser")

여기서 또 느낀 건

HTML도 태그 중심이긴 한데,

XML보다 훨씬 중복 태그가 많고, class 같은 속성까지 같이 봐야 원하는 걸 찾기 쉽다는 점이었다.

자료에서도 div가 너무 많으니 class="headTitle" 같은 속성값으로 타겟팅해야 한다고 정리돼 있었어.

예를 들면:

soup.find_all("div", {"class":"headTitle"})
soup.find_all("div", class_="headTitle")

이 부분 보면서

아 HTML은 그냥 태그만 찾으면 안 되고,

속성까지 같이 보는 습관이 필요하겠다 싶었다.


13. DART에서는 정규식도 같이 써야 하는 게 좀 인상적이었다

오늘 DART에서 의외로 중요했던 게 정규식이었다.

예를 들어 페이지 수 정보가

[1/4] [총 376건]

이렇게 들어오면,

여기서 총 건수나 총 페이지 수를 뽑으려면 태그 접근만으로는 안 되고 결국 문자열 처리까지 가야 했다.

자료에서 re.findall(r'\\d+건', temp), re.sub(r'건', "", t) 예시가 그대로 있었고, 페이지 수를 /4]에서 추출하는 정규식도 나왔어.

이 부분이 되게 현실적이었다.

결국 웹 수집은

  • 태그 접근
  • 속성 접근
  • 문자열 처리
  • 정규식

이런 게 다 같이 섞인다.


14. 공백 처리도 생각보다 중요했다

DART에서 시간값 같은 걸 뽑으면

눈에 안 보이는 공백이 엄청 끼어 있었다.

예를 들면 18:22도 그냥 바로 예쁘게 안 나오는 경우가 있었고,

.strip()이나 정규식으로 \\n, \\t, \\r, \\s 같은 걸 지워야 했다.

자료에서도 .strip()과 re.sub(r'\\n|\\t|\\r|\\s', "", t_str1) 예시가 나와 있어.

이 부분은 좀 웃기면서도 중요했다.

눈에 보기엔 18:22인데

실제로는 안쪽에 개행이랑 탭이 섞여 있다는 거.

이런 건 진짜

해보지 않으면 모르겠구나 싶었다.


15. 그리고 정보는 태그 사이에만 있는 게 아니라 속성 안에도 있었다

DART에서 회사명, 시장 구분, 회사 코드값 뽑는 부분도 재밌었다.

처음엔 그냥 태그 텍스트만 보면 되겠지 싶었는데,

실제로는 속성값 안에 중요한 정보가 들어 있는 경우도 있었다.

자료에서도 “내가 원하는 정보가 tag 사이에 있을 수도 있고, 속성값으로 있을 수도 있고, 속성값 일부에 있을 수도 있다”고 딱 적혀 있었어. 그리고 get("title") 같은 접근이 예시로 나왔고.

즉,

  • 태그 텍스트만 볼 게 아니라
  • 속성도 같이 봐야 한다

이게 오늘 DART 쪽에서 남은 핵심이었다.


16. 저장은 그냥 저장이 아니라 구분자/인코딩/엑셀 차이까지 봐야 했다

마지막엔 저장도 했다.

movie_df.to_csv('/content/kobis_api.csv', sep='@', encoding='cp949')
movie_df.to_excel('/content/kobis_api.xlsx')

csv는 가볍지만

구분자랑 인코딩을 생각해야 했다.

  • 콤마가 영화 제목 안에도 들어갈 수 있으니 @ 같은 구분자를 쓰는 예시
  • 윈도우는 cp949
  • 리눅스/유니코드는 utf-8

이런 식으로 운영체제/환경 따라 생각할 게 있었다.

녹취에서도 csv는 값 중심의 라이트한 포맷이고, 엑셀은 양식/그래프까지 들어가는 무거운 포맷이라고 비교했고, 한글 인코딩 충돌도 얘기했어.

이건 진짜

데이터를 다 모았다고 끝이 아니라,

내가 어디서 열 건지까지 생각해야 한다는 점이 남았다.


17. 오늘 전체적으로 남은 것

오늘 하루를 내 식으로 적으면 이거 같다.

같은 “데이터 수집”이라도,공식 API인지 / 일반 사이트인지 / JSON인지 / XML인지 / HTML인지에 따라 접근 방식이 다 달라진다.

오늘 남은 포인트 정리하면:

  • API는 공식적으로 주는 데이터 입구
  • JSON은 Python이랑 잘 맞아서 편함
  • XML은 태그 중심이라 BeautifulSoup이 필요함
  • HTML은 태그 + 속성 + 정규식까지 같이 봐야 함
  • JSON이든 XML이든 결국 샘플 단위로 정리하는 게 안전함
  • 저장할 때도 csv/xlsx, 구분자, 인코딩까지 생각해야 함

즉 오늘은

그냥 “데이터를 받아봤다”가 아니라,

어디서 어떻게 받고, 어떻게 안 꼬이게 정리할지를 본 날이었다.


마무리

11일차는

KOBIS API로

  • JSON 요청
  • XML 요청
  • urllib, requests
  • json, BeautifulSoup
  • DataFrame 변환
  • csv/xlsx 저장

이 흐름을 봤고,

DART 쪽에서는

  • 개발자도구 Network
  • 숨겨진 요청 규칙 찾기
  • HTML 태그/속성 접근
  • 정규식/공백 처리

이 흐름까지 이어졌다.

개인적으로는 오늘이

“수집”이라는 말이 생각보다 훨씬 넓다는 걸 본 날이었다.

그리고 제일 크게 남은 건 이거.

데이터를 가져오는 것보다,그걸 안 꼬이게 정리해서 내가 쓸 수 있는 구조로 만드는 게 더 중요하다.