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

오늘은 두가지를 알게된 날이었습니다.

  1. 데이터 수집이 생각보다 단순하지 않다는 것,
  2. 코테에서 내가 아직 파이썬 자료구조를 자유롭게 쓰지 못한다는 것이었습니다.

앞부분에서는 DART와 다음 금융을 가지고 일반 사이트 데이터를 수집하는 방법을 봤습니다. 이전까지는 API로 JSON/XML을 받아서 처리했다면, 이번에는 일반 웹사이트 안에서 실제 요청 주소를 찾아내고, HTML 태그와 속성을 따라가고, 정규식으로 필요한 값만 뽑아내는 흐름이었습니다.

오후에는 코테 시험을 보면서 문자열, 중첩 리스트/튜플, 딕셔너리, 반복문 문제를 풀었는데, 특히 2번 중첩 구조 문제와 4번 딕셔너리 반전 문제에서 많이 막혔습니다.

1. 데이터 수집 방식 정리

이번에 가장 먼저 정리한 건 데이터 수집 방식의 단계였습니다.

# 웹 사이트에 있는 데이터를 수집할 때
# 1) API : best
# 2) 일반적인 사이트
#    2-0) 아주 투명한 친구들
#    2-1) DART : 주소가 숨겨져 있는 친구
#    2-2) daum 금융 : 숨겨진 정보를 찾아도 코드 요청은 막힐 수 있음
#          user-agent, referer, cookie 등을 봐야 함
#    2-3) 브라우저 기반 코드 제어 : selenium
#    2-4) AI 브라우저 활용 : 반자동

이 구분이 꽤 중요하게 느껴졌습니다. 이전에는 웹에서 데이터를 가져오는 걸 그냥 “크롤링”이라고 뭉뚱그려 생각했는데, 실제로는 사이트마다 난이도가 완전히 달랐습니다. API처럼 정해진 주소와 포맷을 제공하는 경우가 가장 깔끔하고, DART처럼 Network에서 요청 규칙을 찾아야 하는 경우도 있고, 다음 금융처럼 주소를 찾아도 403이 뜨는 경우도 있었습니다. 자료에서도 일반 사이트 수집은 API보다 훨씬 경우가 다양하고, 상황에 따라 user-agent, referer, 쿠키, 셀레니움까지 고려해야 한다고 정리되어 있었습니다.

즉, 이번 파트에서 남은 건 “크롤링 코드를 외운다”가 아니라, 사이트가 데이터를 어떤 방식으로 내려주는지 먼저 확인해야 한다는 점이었습니다.

2. DART 크롤링: 숨겨진 요청 주소 찾기

DART는 그냥 눈에 보이는 주소만 보면 원하는 데이터가 바로 나오지 않았습니다. 개발자도구의 Network 탭을 보고, 실제로 데이터가 요청되는 주소와 파라미터를 찾아야 했습니다.

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}>"
res = requests.get(url)
soup = BeautifulSoup(res.text, "html.parser")

여기서 핵심은 selectDate와 currentPage였습니다. 날짜와 페이지 번호를 바꾸면 다른 공시 목록을 가져올 수 있기 때문에, 이 두 값을 변수로 잡아두면 나중에 여러 날짜/여러 페이지로 확장할 수 있습니다. 자료에서도 DART는 selectDate, currentPage 같은 파라미터를 찾아 요청 주소를 조립하는 흐름으로 정리되어 있었습니다.

이 부분은 이전 API보다 조금 더 “눈치껏 찾아야 하는” 느낌이 강했습니다. API는 문서에 주소와 파라미터가 정리되어 있지만, DART 같은 일반 사이트는 개발자도구를 보면서 실제 요청을 찾아내야 했습니다.

3. HTML은 태그만으로는 부족하고, 속성까지 봐야 했습니다

DART 응답은 JSON이 아니라 HTML이었습니다. 그래서 BeautifulSoup으로 파싱한 뒤 원하는 태그를 찾아가야 했습니다.

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

처음에는 find() / find_all()로 태그만 잘 찾으면 될 줄 알았는데, HTML에서는 같은 태그가 너무 많았습니다. div, span, td, a 같은 태그가 여러 번 반복되니까, 단순히 태그 이름만으로 접근하면 범위가 너무 넓었습니다. 그래서 class, title, href 같은 속성까지 같이 봐야 했습니다.

자료에서도 일반 사이트는 태그가 표준화되어 있어서 중복 태그명이 많고, 원하는 정보를 정확히 찾으려면 태그와 속성을 조합해서 타이트하게 타겟팅해야 한다고 설명되어 있었습니다.

4. 공시 건수와 페이지 수는 정규식으로 추출했습니다

DART 페이지에는 공시 건수와 페이지 정보가 이런 식으로 들어 있었습니다.

[1/4] [총 376건]

사람이 보면 바로 “총 376건이고 4페이지구나”라고 알 수 있지만, 코드에서는 문자열에서 숫자를 뽑아야 했습니다.

page_info = soup.find_all("div", class_="pageInfo")[0].text
page_info
re.findall(r"\\d+", page_info)
re.findall(r"\\d+건", page_info)

예를 들어 376건을 찾은 뒤 건을 제거하면 총 건수만 남길 수 있습니다.

total_count_text = re.findall(r"\\d+건", page_info)[0]
total_count = int(re.sub(r"건", "", total_count_text))

if total_count % 100 == 0:
    total_page = total_count // 100
else:
    total_page = total_count // 100 + 1

total_count, total_page

이 부분에서 정규식이 왜 필요한지 조금 더 느꼈습니다. HTML 태그를 찾는 것만으로는 끝이 아니고, 태그 안에 들어 있는 문자열에서 다시 필요한 패턴을 추출해야 했습니다. re.findall()은 원하는 패턴을 찾는 용도이고, re.sub()는 찾은 패턴을 다른 문자열로 바꾸거나 제거할 때 쓰였습니다. 자료에서도 \\d+건, re.sub()를 이용해 공시 건수를 뽑는 흐름이 정리되어 있었습니다.

5. 공백 처리: .strip()과 정규식의 차이

DART에서 첫 번째 공시 시간을 가져오면 그냥 18:22만 나올 것 같았는데, 실제로는 개행, 탭, 공백이 잔뜩 섞여 있었습니다.

temp = soup.find("tbody").find_all("tr")[0]

raw_time = temp.find_all("td")[0].text
raw_time

결과가 이런 느낌이었습니다.

'\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t\\r\\n\\t\\t\\t\\t\\t\\t\\t\\t18:22\\r\\n\\t\\t\\t\\t\\t\\t\\t'

양쪽 공백만 제거할 때는 .strip()을 쓸 수 있습니다.

time_text = temp.find_all("td")[0].text.strip()
time_text

하지만 중간에 섞인 공백까지 처리하려면 정규식이 더 확실했습니다.

time_text = re.sub(r"\\r|\\n|\\t|\\s", "", temp.find_all("td")[0].text)
time_text

이번에 느낀 건, 웹에서 가져온 텍스트는 눈에 보이는 값과 실제 문자열이 다를 수 있다는 점입니다. .strip()은 앞뒤 공백 제거에는 좋지만, 중간에 들어간 공백 문자까지 처리해야 하면 re.sub()가 필요했습니다. 자료에서도 .strip()은 양쪽 공백만 제거하고, 중간 공백은 정규식으로 처리해야 한다고 설명되어 있었습니다.

6. DART 공시 한 건에서 필요한 값 추출하기

첫 번째 공시를 하나의 샘플로 잡고, 그 안에서 필요한 값을 하나씩 뽑았습니다.

temp = soup.find("tbody").find_all("tr")[0]
tds = temp.find_all("td")

시간

time_text = tds[0].text.strip()
time_text

회사명

회사명은 두 번째 td 안의 a 태그 텍스트에 있었습니다.

company_name = tds[1].find("a").text.strip()
company_name

시장 구분

시장 구분은 텍스트가 아니라 span 태그의 title 속성 안에 있었습니다.

market = tds[1].find_all("span")[1].get("title")
market

여기서 get("title")을 쓴 게 중요했습니다. 지금까지는 주로 .text로 태그 사이의 값을 가져왔는데, 이번에는 태그의 속성값 안에 필요한 정보가 있었습니다.

회사 코드번호

회사 코드번호는 a 태그의 href 속성 안에 숨어 있었습니다. 그래서 href 문자열을 가져온 뒤, 8자리 숫자만 정규식으로 뽑았습니다.

href = tds[1].find("a").get("href")
corp_code = re.findall(r"\\d{8}", href)[0]
corp_code

여기서 정리된 건 이거였습니다.

원하는 정보는 항상 태그 사이에만 있지 않다.
태그 텍스트에 있을 수도 있고,
속성값에 있을 수도 있고,
속성값의 일부에 있을 수도 있다.

DART 자료에서도 원하는 정보가 태그 사이, 속성값, 속성값 일부에 흩어져 있을 수 있다고 설명되어 있었습니다.

7. 보고서명과 보고서번호 추출

공시 제목은 세 번째 td에 있었고, 보고서번호는 그 안의 a 태그 href 속성에 있었습니다.

report_name = re.sub(r"\\r|\\n|\\t", "", tds[2].text.strip())
report_name

보고서번호는 14자리 숫자 패턴으로 뽑았습니다.

report_href = tds[2].find("a").get("href")
report_no = re.findall(r"\\d{14}", report_href)[0]
report_no

접수일자는 다섯 번째 td에서 가져왔습니다.

submit_date = tds[4].text
submit_date

이렇게 하니까 첫 번째 공시 한 건에서 필요한 정보가 정리되었습니다.

data = {
    "시간": time_text,
    "회사명": company_name,
    "구분자": market,
    "코드번호": corp_code,
    "보고서명": report_name,
    "보고서번호": report_no,
    "접수일자": submit_date
}

data

이 부분이 중요했던 이유는, 크롤링을 할 때 바로 전체 반복문부터 짜는 게 아니라 한 개 샘플을 먼저 완성하고, 그 다음 반복문으로 확장하는 방식이 훨씬 안정적이기 때문입니다.

8. 한 페이지 전체 공시를 DataFrame으로 정리

첫 번째 공시에서 필요한 값을 뽑는 구조가 잡히면, 그 다음은 전체 tr에 반복문을 돌리면 됩니다.

rows = []

for idx, temp in enumerate(soup.find("tbody").find_all("tr")):
    data = {
        "시간": temp.find_all("td")[0].text.strip(),
        "회사명": temp.find_all("td")[1].find("a").text.strip(),
        "구분자": temp.find_all("td")[1].find_all("span")[1].get("title"),
        "코드번호": re.findall(
            r"\\d{8}",
            temp.find_all("td")[1].find("a").get("href")
        )[0],
        "보고서명": re.sub(
            r"\\r|\\n|\\t",
            "",
            temp.find_all("td")[2].text.strip()
        ),
        "보고서번호": re.findall(
            r"\\d{14}",
            temp.find_all("td")[2].find("a").get("href")
        )[0],
        "접수일자": temp.find_all("td")[4].text
    }
    rows.append(data)

df = pd.DataFrame(rows)
df.head()

이 코드는 12일차에서 가장 결과물다운 코드였습니다. data 딕셔너리 하나가 공시 한 건이고, rows 리스트는 공시 여러 건을 담는 구조입니다. 마지막에 pd.DataFrame(rows)로 바꾸면 표 형태로 정리됩니다.

여기서 다시 느낀 건, 데이터 수집도 결국 샘플 단위로 정리하는 게 중요하다는 점이었습니다. JSON/XML 때도 마찬가지였고, HTML에서도 마찬가지였습니다. 열 단위로 시간만 싹 뽑고, 회사명만 싹 뽑고, 보고서명만 싹 뽑는 방식보다, 한 공시에서 필요한 값을 묶어서 한 줄로 만드는 방식이 훨씬 덜 꼬입니다.

9. 다음 금융: 주소를 찾아도 403이 뜰 수 있었습니다

DART가 숨겨진 요청 주소를 찾는 쪽이었다면, 다음 금융은 “주소를 찾아도 바로 안 될 수 있다”는 걸 보여줬습니다.

처음 다음 금융 메인 페이지를 요청하면 에러는 안 나지만, 내부의 종목 정보는 비어 있는 상태였습니다.

import urllib.request

url = "<https://finance.daum.net/>"
res = urllib.request.urlopen(url)
html = res.read().decode("utf-8")
html[:500]

그래서 개발자도구 Network 탭을 통해 인기 검색 상위 10개 요청 주소를 찾았습니다.

url = "<https://finance.daum.net/api/search/ranks?limit=10>"

브라우저에서는 잘 되는데, 코드에서 그냥 요청하면 403이 날 수 있었습니다.

res = urllib.request.urlopen(url)
res.read().decode("utf-8")

자료에서도 다음 금융 내부 API 주소를 찾았지만, 코드에서 직접 접근하면 HTTP Error 403이 날 수 있다고 정리되어 있었습니다.

10. 403 대응: referer와 user-agent 넣기

403이 뜨는 경우에는 요청 헤더를 같이 보내야 했습니다. 핵심은 브라우저에서 요청한 것처럼 referer, user-agent를 맞춰주는 것이었습니다.

url = "<https://finance.daum.net/api/search/ranks?limit=10>"

req = urllib.request.Request(
    url,
    data=None,
    headers={
        "referer": "<https://finance.daum.net/>",
        "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                      "AppleWebKit/605.1.15 (KHTML, like Gecko) "
                      "Version/18.5 Safari/605.1.15"
    }
)

res = urllib.request.urlopen(req).read().decode("utf-8")
res[:500]

JSON 응답이기 때문에 json.loads()로 파이썬 자료형으로 바꿀 수 있습니다.

import json

data = json.loads(res)
data["data"][0]

requests를 쓰면 조금 더 깔끔하게 작성할 수 있습니다.

import requests

url = "<https://finance.daum.net/api/search/ranks?limit=10>"

headers = {
    "referer": "<https://finance.daum.net/>",
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 (KHTML, like Gecko)"
}

res = requests.get(url, headers=headers)
data = res.json()
data["data"][0]

이 부분에서 가장 크게 느낀 건, 일반 사이트는 단순히 URL만 안다고 끝나는 게 아니라는 점이었습니다. 브라우저에서는 되는데 코드에서는 안 되는 경우가 있고, 이럴 때는 Network 탭에서 요청 헤더까지 확인해야 했습니다. 자료에서도 referer, user-agent를 헤더에 넣어 요청하는 방식이 정리되어 있었습니다.

11. 오후 코테 시험

15시 이후에는 코테 시험을 봤습니다. 문제는 문자열, 중첩 구조, 딕셔너리, 반복문, 키패드, 항공권 최저가 등 여러 유형이 섞여 있었습니다. 문제 파일에는 문자열 숫자 합산기, 중첩 구조 최댓값 찾기, 특정 모음 제거기, 딕셔너리 키-값 반전 및 정렬, 반복문 문제, 베이킹 대회 우승자 선발, 키패드 문제 등이 정리되어 있었습니다.

이번 시험에서 느낀 건, 문제를 읽고 방향을 잡는 것보다 그걸 파이썬 문법으로 빠르게 구현하는 힘이 아직 부족하다는 점이었습니다. 특히 2번과 4번에서 많이 막혔습니다.

1번 문자열 숫자 합산기

1번은 문자열 안에서 숫자인 문자만 골라 각 자릿수를 더하는 문제였습니다. .isdigit()을 쓰면 되는 문제였고, 풀이 파일에서도 직접 구현한 코드가 있었습니다.

def solution1(s):
    llist = []
    cnt = 0

    for i in s:
        llist.append(i)

    for i in llist:
        if i.isdigit():
            cnt += int(i)
        else:
            pass

    return cnt

지금 다시 보면 첫 번째 for문으로 굳이 llist를 만들 필요 없이 바로 순회해도 됐을 것 같습니다.

def solution1(s):
    total = 0

    for ch in s:
        if ch.isdigit():
            total += int(ch)

    return total

또는 더 줄이면 이렇게도 가능합니다.

def solution1(s):
    return sum(int(ch) for ch in s if ch.isdigit())

이 문제에서 남은 건 .isdigit() 같은 기본 메서드를 정확히 알고 있으면 코드가 훨씬 짧아진다는 점이었습니다. 그리고 문제 파일 첫 부분에 파이썬 공식 문서 링크가 참조 가능 문서로 적혀 있었기 때문에, 앞으로는 막히는 함수나 메서드가 있을 때 공식 문서를 더 잘 찾아보는 연습도 필요하다고 느꼈습니다.

2번 중첩 구조 최댓값 찾기

2번은 이번 시험에서 가장 아쉬웠던 문제 중 하나였습니다. 리스트나 튜플 안에 또 리스트/튜플이 계속 들어갈 수 있고, 그 안의 모든 숫자 중 최댓값을 찾아야 했습니다. 문제 설명에서도 depth에 상관없이 가장 깊은 숫자까지 확인해야 한다고 되어 있었습니다.

처음에는 list(t).pop()을 활용해서 풀어보려고 했습니다.

def solution2(t):
    llist = []
    while llist == []:
        if type(list(t).pop()) == int:
            ...

이 방식은 방향을 잡다가 멈춘 느낌이었습니다. 이 문제는 재귀나 stack을 떠올려야 하는 문제였는데, 실제 구현까지 연결이 잘 안 됐습니다. 제가 “재귀함수를 써야 하나?”까지 생각한 건 방향 자체는 맞았던 것 같습니다.

재귀로 풀면 이런 구조가 됩니다.

def solution2(t):
    max_value = None

    for item in t:
        if isinstance(item, (list, tuple)):
            value = solution2(item)
        else:
            value = item

        if max_value is None or value > max_value:
            max_value = value

    return max_value

핵심은 두 가지였습니다.

1. 현재 요소가 int면 바로 비교한다.
2. 현재 요소가 list/tuple이면 안쪽으로 다시 들어간다.

이 문제에서 부족했던 건 알고리즘 이름을 모르는 것보다, 중첩 구조를 만났을 때 함수가 자기 자신을 다시 호출하는 구조를 코드로 만드는 힘이었습니다.

3번 특정 모음 제거기

3번은 a, e, i를 대소문자 구분 없이 제거하는 문제였습니다. 이건 직접 구현이 됐습니다.

def solution3(s):
    result = ''

    for i in s:
        if i == 'i' or i == 'a' or i == 'e' or i == 'I' or i == 'A' or i == 'E':
            pass
        else:
            result += i

    return result

다만 다시 보면 조건문이 조금 길었습니다. lower()와 in을 쓰면 더 깔끔하게 쓸 수 있습니다.

def solution3(s):
    result = ""

    for ch in s:
        if ch.lower() not in ["a", "e", "i"]:
            result += ch

    return result

또는 문자열로도 가능합니다.

def solution3(s):
    result = ""

    for ch in s:
        if ch.lower() not in "aei":
            result += ch

    return result

이 문제에서 남은 건, 조건이 길어질 때는 하나하나 or로 이어붙이기보다 비교 대상 묶음을 만들고 in으로 확인하는 방식을 먼저 생각해야 한다는 점이었습니다.

4번 딕셔너리 키-값 반전 및 정렬

4번은 이번 시험에서 제일 아쉬웠던 문제입니다. 기존 딕셔너리의 key와 value를 뒤집고, 같은 value가 여러 개 있으면 key들을 리스트로 묶어야 했습니다. 문제 파일에서도 기존 value가 동일한 경우 새로운 key가 겹치므로 리스트로 묶고, 리스트 내부는 오름차순 정렬해야 한다고 되어 있었습니다.

처음 시도는 이런 식이었습니다.

def solution4(d):
    rd = {}
    llist = []

    for x in d.values():
        rd[x] = ""

    for i in range(len(d)):
        if d.values[i] != rd.keys():
            pass
        else:
            llist.append(d.values[i])

        rd[i] = list(set(llist))
        llist = []

    return rd

여기서 에러가 났습니다.

TypeError: 'builtin_function_or_method' object is not subscriptable

이건 d.values를 리스트처럼 인덱싱하려고 해서 생긴 문제였습니다. d.values()는 메서드 호출이고, 그 결과도 바로 인덱싱해서 쓰기 좋은 형태가 아닙니다. 풀이 파일에도 이 에러가 그대로 남아 있었습니다.

이 문제는 items()로 key, value를 같이 꺼내면 훨씬 자연스럽습니다.

def solution4(d):
    result = {}

    for key, value in d.items():
        if value not in result:
            result[value] = []

        result[value].append(key)

    for value in result:
        result[value].sort()

    return result

예시로 보면:

input_dict2 = {1:40, 2:10, 3:20, 6:30, 5:30, 4:30}
solution4(input_dict2)

결과는 이렇게 나와야 합니다.

{40: [1], 10: [2], 20: [3], 30: [4, 5, 6]}

이 문제에서 느낀 건 아주 명확했습니다. 딕셔너리는 아직 더 연습이 필요합니다. 특히 아래 패턴은 익숙해져야 할 것 같습니다.

for key, value in d.items():
    ...
if value not in result:
    result[value] = []
result[value].append(key)

즉, 딕셔너리 문제는 무작정 key와 value를 뒤집는 문제가 아니라 새로운 key가 이미 있는지 확인하고, 있으면 누적하고, 없으면 초기화하는 구조를 떠올려야 했습니다.

9번 베이킹 대회 우승자 선발

풀이 파일을 보면 9번에서도 딕셔너리 누적을 시도한 흔적이 있었습니다. 참가자별 점수를 합산해서 가장 높은 사람을 찾는 문제였습니다.

처음 코드에서는 이런 부분이 있었습니다.

def solution9(scores):
    vic = {}

    for i in range(len(scores)):
        if scores[i][0] in vic:
            vic[f'{scores[i][0]}'] = scores[i][1] + scores[i][1]
        else:
            vic[f'{scores[i][0]}'] = scores[i][1]

    maxxx = max(vic, key=vic.get)
    return maxxx

여기서 점수를 누적할 때 기존 점수에 새 점수를 더해야 하는데, 코드에서는 scores[i][1] + scores[i][1]로 현재 점수를 두 번 더하고 있었습니다. 더 자연스러운 구조는 이렇습니다.

def solution9(scores):
    total = {}

    for name, score in scores:
        if name not in total:
            total[name] = 0

        total[name] += score

    return max(total, key=total.get)

이 문제도 결국 4번과 비슷했습니다. 핵심은 딕셔너리 누적입니다.

처음 나온 key면 0으로 초기화
이미 있으면 기존 값에 더하기
마지막에 value가 가장 큰 key 찾기

이 패턴을 익히면 4번, 9번 같은 문제를 훨씬 빨리 풀 수 있을 것 같습니다.

12. 오늘 느낀 점

12일차는 앞뒤가 다른 듯하면서도 결국 같은 이야기를 하고 있었습니다. 오전의 크롤링도, 오후의 코테도 결국은 구조를 읽고 규칙을 코드로 옮기는 일이었습니다.

DART에서는 HTML 구조를 보고 tbody > tr > td로 들어가야 했고, 회사명은 태그 텍스트에서, 시장 구분은 속성값에서, 코드번호는 속성 문자열 일부에서 뽑아야 했습니다. 다음 금융에서는 주소를 찾아도 403이 뜰 수 있어서 referer, user-agent를 같이 봐야 했습니다.

코테에서는 문자열 문제는 어느 정도 풀었지만, 중첩 구조와 딕셔너리 문제에서 많이 막혔습니다. 특히 4번 딕셔너리 반전 문제와 9번 점수 누적 문제를 보면서, 제가 아직 dict.items(), key 존재 여부 확인, 리스트 누적, value 기준 max() 같은 패턴을 자연스럽게 쓰지 못한다는 걸 느꼈습니다.

그래서 오늘 남은 건 이거였습니다.

크롤링은 사이트 구조를 읽는 연습이고,
코테는 자료구조를 읽는 연습이다.
둘 다 결국 구조를 코드로 바꾸는 일이다.

마무리

12일차는 웹 데이터 수집과 코테 시험이 같이 있었던 날이었습니다. 데이터 수집 쪽에서는 DART를 통해 HTML 태그/속성/정규식으로 필요한 값을 추출하고 DataFrame으로 정리하는 흐름을 봤고, 다음 금융을 통해 403이 발생할 때 headers를 맞춰야 한다는 점을 배웠습니다. 코테 쪽에서는 1번/3번처럼 문자열을 다루는 문제는 풀었지만, 2번 중첩 구조와 4번 딕셔너리 반전 문제에서 부족함을 많이 느꼈습니다.

특히 오늘은 파이썬 공식 문서를 참고할 수 있다는 점도 다시 보게 됐습니다. 문제 파일 맨 앞에 파이썬 공식 메뉴얼이 참조 가능 문서로 적혀 있었고, 앞으로는 막히는 함수나 메서드가 있을 때 검색만 하기보다 공식 문서에서 직접 확인하는 습관도 더 들여야겠다고 느꼈습니다.

다음에 보완해야 할 부분은 명확합니다.

1. BeautifulSoup에서 태그/속성 접근 더 연습하기
2. 정규식으로 숫자 패턴 추출하는 연습하기
3. 딕셔너리 누적 패턴 익숙해지기
4. 중첩 리스트/튜플은 재귀나 stack으로 푸는 연습하기
5. 파이썬 공식 문서에서 필요한 메서드 찾는 연습하기

오늘은 잘 풀었다기보다는, 어디가 부족한지 정확히 보인 날에 가까웠습니다. 그래서 오히려 다음에 뭘 연습해야 하는지는 더 선명해졌습니다.