17일차는 크게 Selenium으로 DART 공시 페이지를 직접 제어하는 실습과 Python 시각화 기초로 나뉘었다. 전날까지는 requests, BeautifulSoup으로 HTML을 가져와 태그 중심으로 파싱했다면, 이번에는 실제 크롬 브라우저를 띄우고 Selenium의 driver를 통해 페이지 요소에 접근했다. 즉, “HTML을 받아서 분석하는 방식”에서 한 단계 더 가서, 브라우저 자체를 코드로 움직이며 데이터를 가져오는 방식을 본 날이었다. Selenium 실습 자료에서도 DART 날짜를 datetime.strftime("%Y.%m.%d")로 맞추고, Service, webdriver.Chrome, By.XPATH를 이용해 DART 공시 페이지에 접근하는 흐름이 정리되어 있었다.
1. Selenium으로 DART 페이지 접속하기
이번 Selenium 실습은 지난 DART 크롤링과 연결됐다. DART는 날짜와 페이지 번호가 URL에 들어가는 구조였기 때문에, 먼저 날짜를 datetime으로 만들고 DART에서 요구하는 YYYY.MM.DD 형식으로 바꿨다.
import selenium
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
import re
import time
import datetime
path = '/Users/seoyeongkim/Library/Mobile Documents/com~apple~CloudDocs/SK플래닛/pandas/chromedriver-mac-arm64/chromedriver'
date_year = 2026
date_month = 4
date_day = 16
date = datetime.datetime(date_year, date_month, date_day)
date_YYYYMMDD = date.strftime("%Y.%m.%d")
page = 1
url = f'<https://dart.fss.or.kr/dsac001/mainAll.do?selectDate={date_YYYYMMDD}¤tPage={page}>'
이전에는 날짜를 문자열로 직접 넣었다면, 이번에는 datetime으로 날짜를 만들고 strftime()으로 원하는 형식으로 바꿨다. 이 방식이 좋은 이유는 나중에 날짜 범위를 여러 개 돌릴 때 직접 문자열을 바꾸는 것보다 훨씬 안정적이기 때문이다.
그 다음은 ChromeDriver 경로를 Service로 지정하고, 크롬 브라우저를 띄웠다.
s = Service(path)
driver = webdriver.Chrome(service=s)
driver.get(url)
여기서 driver는 내가 코드로 조작할 브라우저라고 보면 된다. driver.get(url)을 실행하면 사람이 주소창에 URL을 입력한 것처럼 브라우저가 해당 페이지로 이동한다. Selenium 자료에서도 driver를 “컨트롤하려는 브라우저에 대한 변수화”로 보고, 원하는 주소를 driver.get(url)로 열어주는 구조가 나와 있었다.
2. Selenium은 브라우저 로딩을 기다려야 한다
requests나 urllib로 요청할 때는 응답이 온 뒤에 다음 코드가 실행되는 느낌이 강했다. 그런데 Selenium은 브라우저와 연동되어 있기 때문에, 페이지가 아직 완전히 로딩되지 않았는데 코드가 먼저 실행되면 요소를 못 찾는 문제가 생길 수 있다.
이 부분이 Selenium에서 꽤 중요하게 느껴졌다. 브라우저가 느리게 뜨거나 페이지 이동 후 정보가 아직 안 들어왔는데 바로 find_element()를 실행하면 “해당 요소가 없다”는 에러가 날 수 있다. Selenium 녹취에서도 Selenium은 브라우저와 연동되기 때문에, 사이트 정보가 충분히 담길 때까지 기다려야 하고, 코드가 먼저 내려가면 요소가 없다고 에러가 뜰 수 있다고 설명되어 있었다.
그래서 페이지를 클릭해서 이동하거나, 새 페이지 내용을 읽어야 할 때는 중간에 기다리는 처리가 필요하다.
time.sleep(5)
물론 무조건 sleep()만 쓰는 게 정답은 아니지만, 이번 실습에서는 Selenium의 흐름을 이해하는 단계였기 때문에 먼저 명시적으로 기다리는 방식으로 접근했다.
3. XPATH로 DART 공시 시간 가져오기
DART 공시 목록에서 첫 번째 공시 시간을 가져오기 위해 개발자도구에서 XPATH를 복사했다. 첫 번째 공시 시간의 XPATH는 아래와 같은 형태였다.
b_path = '//*[@id="listContents"]/div[2]/table/tbody/tr[1]/td[1]'
temp = driver.find_element(By.XPATH, b_path)
temp.text
여기서 find_element(By.XPATH, b_path)는 브라우저 안에서 해당 XPATH에 해당하는 요소를 찾는다. 그리고 .text를 붙이면 태그 안에 들어 있는 텍스트만 가져온다. 자료에서도 첫 번째 공시 시간 XPATH와 Full XPATH를 비교하고, 내가 수집할 정보에 대한 경로가 여러 개에 적용 가능한 규칙인지 확인해야 한다고 정리되어 있었다.
공시 시간 5개를 출력할 때는 tr[1], tr[2], tr[3]처럼 바뀌는 부분을 for문으로 처리했다.
for i in range(1, 6):
b_path = f'//*[@id="listContents"]/div[2]/table/tbody/tr[{i}]/td[1]'
temp = driver.find_element(By.XPATH, b_path)
print(temp.text)
이 부분이 Selenium 실습에서 가장 기본이 되는 패턴처럼 느껴졌다. 개발자도구로 하나의 요소 경로를 찾고, 그 경로 안에서 반복되는 숫자 부분을 확인한 뒤, f-string으로 규칙화하는 방식이다.
4. .text와 get_attribute() 차이
DART 페이지에서 필요한 정보가 항상 태그 사이의 텍스트에 있는 것은 아니었다. 회사 이름은 텍스트로 가져올 수 있었지만, 회사가 속한 시장 정보는 title 속성 안에 있었다.
co_cat_path = '//*[@id="listContents"]/div[2]/table/tbody/tr[1]/td[2]/span/span'
temp = driver.find_element(By.XPATH, co_cat_path)
temp.text
temp.get_attribute("title")
.text는 태그 사이에 보이는 텍스트를 가져오고, get_attribute("title")은 태그의 title 속성값을 가져온다. 지난 BeautifulSoup 실습에서도 .text와 .get("href"), .get("title")을 구분했는데, Selenium에서도 비슷한 구분이 필요했다.
회사 코드도 마찬가지였다. 회사 코드 자체가 화면에 바로 텍스트로 있는 것이 아니라, href 안의 문자열에 들어 있었다. 그래서 href를 가져온 뒤 정규식으로 8자리 숫자만 추출했다.
co_path = '//*[@id="listContents"]/div[2]/table/tbody/tr[1]/td[2]/span/a'
temp = driver.find_element(By.XPATH, co_path)
co_name = temp.text
co_id = re.findall(r'[0-9]{8}', temp.get_attribute("href"))[0]
공시 번호도 href 속성 안에서 14자리 숫자를 뽑았다.
rcp_path = '/html/body/div[4]/div[2]/div[1]/div[2]/div[3]/div[2]/table/tbody/tr[1]/td[3]/a'
rcp_name = driver.find_element(By.XPATH, rcp_path).text
rcp_no = re.findall(
r'\\d{14}',
driver.find_element(By.XPATH, rcp_path).get_attribute("href")
)[0]
여기서도 다시 느낀 건, 크롤링에서 필요한 값은 세 군데 중 하나에 있을 수 있다는 점이다.
1. 태그 사이의 텍스트
2. 태그의 속성값
3. 속성값 안의 일부 문자열
이걸 구분하지 못하면 .text만 계속 찍다가 원하는 값을 못 찾을 수 있다.
5. XPATH는 규칙화 가능한 경로인지 먼저 봐야 한다
공시 이름과 번호를 가져올 때는 XPATH 선택이 조금 더 중요했다. 어떤 XPATH는 특정 공시의 고유 ID가 들어가 있어서, 다른 행에 반복 적용하기 어려웠다.
예를 들면 이런 형태는 규칙화가 애매하다.
//*[@id="r_20260416900775"]
이 값은 특정 공시 번호가 들어간 ID라서, 다음 공시에도 같은 방식으로 바꾸기 어렵다. 그래서 이런 경우에는 Full XPATH나 위쪽 tbody/tr/td 구조를 이용해서 반복 가능한 경로를 잡는 게 더 낫다. Selenium 자료에서도 특정 ID 기반 접근은 규칙이 애매할 수 있고, Full XPATH나 위쪽 경로를 다시 확인해야 한다고 정리되어 있었다.
즉 Selenium에서는 “요소를 찾았다”에서 끝나는 게 아니라, 그 경로가 반복문에 들어갈 수 있는 경로인지를 확인해야 했다.
6. 앞의 5개 공시 정보 출력하기
시간, 회사명, 회사가 속한 시장, 회사 코드, 공시명, 공시 번호를 앞의 5개 공시에 대해 출력하는 코드를 만들었다.
for i in range(1, 6):
# 보고 시간
b_path = f'//*[@id="listContents"]/div[2]/table/tbody/tr[{i}]/td[1]'
rcp_time = driver.find_element(By.XPATH, b_path).text
# 회사가 속한 시장
co_cat_path = f'//*[@id="listContents"]/div[2]/table/tbody/tr[{i}]/td[2]/span/span'
co_cat = driver.find_element(By.XPATH, co_cat_path).get_attribute("title")
# 회사 이름
co_path = f'//*[@id="listContents"]/div[2]/table/tbody/tr[{i}]/td[2]/span/a'
co_name = driver.find_element(By.XPATH, co_path).text
# 회사 코드
co_id = re.findall(
r'\\d{8}',
driver.find_element(By.XPATH, co_path).get_attribute("href")
)[0]
# 공시 정보
rcp_path = f'//*[@id="listContents"]/div[2]/table/tbody/tr[{i}]/td[3]/a'
rcp_name = driver.find_element(By.XPATH, rcp_path).text
# 공시 번호
rcp_no = re.findall(
r'\\d{14}',
driver.find_element(By.XPATH, rcp_path).get_attribute("href")
)[0]
print(rcp_time, co_name, co_cat, co_id, rcp_name, rcp_no)
이 코드가 좋았던 이유는 지난 DART BeautifulSoup 실습과 거의 같은 데이터를 Selenium으로 다시 가져왔다는 점이다. 지난번에는 requests로 HTML을 받고, BeautifulSoup으로 태그를 찾았다면, 이번에는 브라우저를 띄운 상태에서 driver.find_element()로 직접 요소를 찾았다. 자료에서도 시간, 회사 이름, 회사 속한 시장, 회사 코드, 공시 이름, 공시 번호를 앞의 5개 공시에 대해 출력하는 실습이 정리되어 있었다.
다만 Selenium은 매번 브라우저와 통신하기 때문에 상대적으로 느릴 수 있다. 그래서 자료에서도 페이지 정보가 이미 driver 안에 들어와 있다면, driver.page_source를 가져와 BeautifulSoup으로 처리하는 방식도 가능하다고 언급되어 있었다.
7. 페이지 클릭과 대기 처리
페이지 번호를 클릭해서 다음 페이지로 넘어가는 것도 Selenium으로 처리했다.
n_path = '//*[@id="listContents"]/div[3]/div[2]/ul/li[3]/a'
driver.find_element(By.XPATH, n_path).click()
페이지를 클릭한 뒤에는 새 페이지 정보가 브라우저에 충분히 들어올 때까지 기다려야 한다.
time.sleep(5)
이걸 빼면 코드가 페이지 로딩보다 먼저 실행되어, 아직 없는 요소를 찾으려다 에러가 날 수 있다. Selenium 자료에도 페이지를 클릭한 뒤에는 새로운 페이지 정보가 driver에 담길 때까지 충분히 기다려야 한다고 강조되어 있었다.
이 부분이 Selenium과 requests 방식의 큰 차이처럼 느껴졌다. Selenium은 사람이 브라우저를 쓰는 것과 비슷하게 움직일 수 있는 대신, 그만큼 페이지 로딩과 화면 상태를 신경 써야 한다.
8. Matplotlib: 그래프는 figure와 axes를 구분해야 했다
다음 파트에서는 Python 시각화 기초로 넘어갔다. 가장 먼저 본 패키지는 matplotlib이었다. Matplotlib은 Python에서 가장 기본이 되는 그래프 패키지이고, 다른 시각화 패키지와 함께 쓰이는 경우도 많다. 녹취에서도 Matplotlib은 파이썬 계열에서 가장 기본이 되는 그래프 패키지이지만, 그래프 자체가 아주 예쁘다기보다는 데이터 처리 중 경향을 빠르게 확인하거나 다른 패키지의 판을 잡는 데 많이 쓰인다고 설명했다.
가장 중요한 구분은 figure와 axes였다.
fig = plt.figure()
axes = fig.add_axes([0.5, 0.5, 0.5, 0.5])
axes.plot(x, y, "r")
axes.set_xlabel("X---X")
axes.set_ylabel("Y---y")
axes.set_title("MyGraph")
figure는 전체 스케치북이고, axes는 그 안에서 실제로 그래프를 그릴 영역이다. 처음에는 그냥 plt.plot()만 생각했는데, 직접 영역을 지정하려면 figure와 axes를 구분해야 했다.
add_axes([left, bottom, width, height])에서 숫자는 0~1 사이의 비율로 위치와 크기를 지정한다. 이 구조를 이해하면 그래프 안에 작은 그래프를 넣는 것도 가능했다.
fig = plt.figure()
axes1 = fig.add_axes([0, 0, 1, 1])
axes2 = fig.add_axes([0.1, 0.5, 0.4, 0.3])
여기서 axes1은 큰 메인 그래프 영역이고, axes2는 그 안에 들어가는 작은 그래프 영역이다. 결국 그래프를 그린다는 건 단순히 선을 긋는 게 아니라, 어떤 판에, 어떤 영역을 만들고, 그 영역에 어떤 순서쌍을 찍을 것인가를 정하는 일이었다.
9. subplots()로 여러 그래프를 나눠 그리기
직접 add_axes()로 위치를 잡는 방식도 있지만, 여러 그래프를 일정하게 나눠 그릴 때는 subplots()가 편했다.
fig, axes = plt.subplots(nrows=1, ncols=3)
이렇게 하면 한 줄에 3개의 그래프 영역을 만든다. axes는 여러 개의 그래프 영역을 담고 있기 때문에, axes[0], axes[1], axes[2]처럼 접근할 수 있다.
그래프 크기도 조절했다.
fig = plt.figure(figsize=(12, 6), dpi=100)
fig, axes = plt.subplots(figsize=(12, 6))
fig, axes = plt.subplots(figsize=(3, 10))
여기서 figsize는 그래프의 가로/세로 비율과 크기를 조절하는 옵션이고, dpi는 해상도와 관련된다. 녹취에서도 figsize는 옆으로 길게 볼지, 세로로 길게 볼지 목적에 맞게 설정하는 것이고, dpi는 해상도 개념이라고 설명했다.
이 부분은 나중에 여러 그래프를 한 화면에 모아야 할 때 중요할 것 같다. 단일 그래프를 그리는 것보다, 여러 그래프를 어떻게 배치해서 비교할 것인가가 시각화에서는 더 중요한 문제가 될 수 있기 때문이다.
10. Telecom 데이터와 Seaborn: boxplot과 violinplot 비교
마지막으로 Telecom 데이터 실습에서는 seaborn을 사용했다. Seaborn은 Matplotlib 위에서 더 쉽게 통계형 그래프를 그릴 수 있게 도와주는 느낌이었다. 특히 Seaborn은 data, x, y, hue를 지정하는 방식이라 Pandas의 pivot_table처럼 “어떤 데이터프레임에서 어떤 컬럼을 축으로 쓸 것인가”를 세팅하는 느낌이 있었다.
import matplotlib.pyplot as plt
fig, axes = plt.subplots(nrows=1, ncols=2, sharey=True)
sns.boxplot(
data=data,
y="Total intl calls",
ax=axes[0]
)
sns.violinplot(
data=data,
y="Total intl calls",
ax=axes[1]
)
여기서 boxplot은 사분위수 중심으로 데이터 분포를 보여주고, violinplot은 분포의 모양까지 같이 보여준다. 녹취에서도 boxplot은 최솟값, 중앙값, 25%, 75% 분위 등을 시각적으로 확인하는 데 쓰이고, violinplot은 분포의 쏠림까지 더 직관적으로 볼 수 있다고 설명했다.
특히 ax=axes[0], ax=axes[1]가 중요했다. Matplotlib으로 전체 판과 두 개의 영역을 먼저 만들고, Seaborn 그래프를 각각 왼쪽/오른쪽 영역에 꽂아 넣는 방식이었다. 즉 Matplotlib은 판을 잡고, Seaborn은 그 판 위에 원하는 통계 그래프를 그리는 식으로 같이 쓸 수 있었다.
이 부분은 대시보드와도 연결되는 느낌이었다. 여러 그래프를 하나의 판에 모아서 비교하면, 각각 따로 볼 때보다 데이터의 차이를 훨씬 쉽게 볼 수 있다.
마무리
17일차는 Selenium과 시각화가 같이 나온 날이었다. Selenium에서는 DART 공시 페이지에 직접 접속해서 XPATH로 요소를 찾고, .text, get_attribute(), 정규식으로 필요한 값을 뽑았다. 특히 Selenium은 브라우저와 연결되어 있기 때문에 페이지 로딩을 기다려야 하고, XPATH가 반복문에 쓸 수 있는 규칙적인 경로인지 확인해야 한다는 점이 중요했다.
시각화에서는 Matplotlib의 figure와 axes 개념을 잡고, add_axes(), subplots(), figsize를 통해 그래프를 어디에 어떻게 그릴지 정했다. 그리고 Telecom 데이터에서는 Seaborn의 boxplot, violinplot을 Matplotlib의 subplot 영역에 각각 넣으면서, 여러 그래프를 하나의 판에 모아 비교하는 방식을 봤다.
이번에 남은 핵심은 아래와 같다.
1. Selenium은 HTML을 가져오는 것이 아니라 브라우저를 코드로 제어하는 방식이다.
2. XPATH는 찾는 것보다 반복 가능한 경로인지 확인하는 것이 중요하다.
3. 태그 텍스트는 .text, 속성값은 get_attribute()로 가져온다.
4. 페이지 이동 후에는 브라우저 정보가 로딩될 시간을 고려해야 한다.
5. Matplotlib에서는 figure와 axes를 구분해야 한다.
6. Seaborn은 data, x, y, hue, ax를 지정해서 그래프를 그리는 방식이 편하다.
Selenium은 확실히 requests + BeautifulSoup보다 무겁고 느릴 수 있지만, 로그인, 클릭, 페이지 이동처럼 브라우저 조작이 필요한 상황에서는 쓸 수밖에 없는 도구라는 느낌이었다. 반대로 시각화는 단순히 그래프 하나를 그리는 것이 아니라, 어떤 그래프를 어떤 위치에 배치해서 어떤 비교를 보여줄지까지 생각해야 한다는 점이 남았다.
'[SK플래닛] ASAC 빅데이터전문가 11기 > 학습기록' 카테고리의 다른 글
| [SK플래닛] ASAC 빅데이터전문가 11기 | 20일차 (0) | 2026.05.18 |
|---|---|
| [SK플래닛] ASAC 빅데이터전문가 11기 | 19일차 (0) | 2026.05.15 |
| [SK플래닛] ASAC 빅데이터전문가 11기 | 16일차 (1) | 2026.05.13 |
| [SK플래닛] ASAC 빅데이터전문가 11기 | 16일차 (0) | 2026.05.08 |
| [SK플래닛] ASAC 빅데이터전문가 11기 | 15일차 (0) | 2026.05.07 |
