멀티캠퍼스 데이터분석 4월 21일 수업 내용 — Flask, HTML/CSS, BeautifulSoup, 주식 데이터 크롤링
이론
1. 플라스크 (Flask)
2. 웹 서버
3. 정적 / 동적 웹 페이지
4. HTML 기초
5. HTML 주요 태그
6. Hyperlink & 경로
7. list / table / image / form 태그
8. CSS 셀렉터
실습
Flask는 파이썬 기반의 웹 프레임워크로, micro 프레임워크를 강조합니다.
웹 서버는 하드웨어와 소프트웨어 두 가지 측면으로 구분합니다.
| 구분 | 설명 |
|---|---|
| 하드웨어 | HTML, Image, CSS, JS 등 컴포넌트 파일을 저장하고 클라이언트에게 전달하는 컴퓨터 |
| 소프트웨어 | 사용자의 파일 접근을 관리, HTTP 프로토콜로 요청을 처리하고 응답 |

💡 브라우저가 파일을 필요로 할 때 → HTTP로 요청 → 올바른 서버에 도달 → HTTP 서버가 파일을 HTTP로 응답
| 구분 | 설명 |
|---|---|
| 정적 웹 페이지 | 서버에 미리 저장된 파일이 그대로 전달, 데이터가 변경되지 않는 한 고정된 페이지를 보게 됨 |
| 동적 웹 페이지 | 서버의 데이터를 스크립트로 가공 처리 후 생성·전달, 상황·시간·요청에 따라 페이지가 달라짐 |

HTML은 웹페이지의 내용과 구조를 담당하는 마크업 언어입니다.
마크업 언어 — 특별한 기호나 표기를 사용하여 글의 서식과 스타일을 정해주는 언어 (프로그래밍 언어 X)
<!DOCTYPE html>
<html>
<head>
<!-- 브라우저에 표시되지 않는 영역: title, 외부 파일 참조, 메타데이터 -->
</head>
<body>
<!-- 브라우저에 표시되는 영역 -->
</body>
</html>


이름 = "값" 쌍으로 구성


| 태그 | 설명 |
|---|---|
<title> | 문서 제목, 브라우저 탭에 표시 |
<style> | HTML 문서의 CSS 스타일 정의 |
<link> | 외부 CSS 파일 연결 등 외부 리소스 연계 |
<script> | JS 코드 포함 또는 src로 외부 JS 파일 로드 |
| 태그 | 설명 |
|---|---|
<h1> ~ <h6> | 제목 태그, h1이 가장 크고 중요 |
<p> | 단락 지정, 본문에 자주 사용 |
<b>, <strong> | 굵은 글씨 |

하이퍼링크는 문서 안에서 다른 문서나 자료로 즉시 접근할 수 있는 참조 고리입니다.
누르면 웹 사이트나 프로그램으로 이동합니다.







<img src="경로" alt="설명" width="너비" height="높이">

유저가 입력한 데이터를 수집하기 위해 사용합니다.
입력 방식: input, textarea, button, select, checkbox, radio button, submit button

| 전송 방식 | 설명 |
|---|---|
GET | URL에 ?key=value 형태로 데이터 노출, 최대 255자 제한, 보안 취약 |
POST | request body에 데이터 담아 전송, URL에 노출되지 않아 보안 우수, GET보다 느림 |
서버에 전송되는 데이터는 name을 키, value를 값으로 하여 key=value 형태로 전송됩니다.


| 셀렉터 | 설명 |
|---|---|
* | 문서 내 모든 요소 |
태그명 | 지정된 태그명을 가진 요소 |
#id값 | id 속성이 일치하는 요소 (id는 문서에서 유일) |
.class값 | class 값이 일치하는 요소 (여러 개 지정 가능) |




| 라이브러리 | 역할 |
|---|---|
requests | 웹 서버에 요청을 보내고 응답 수신 (bytes / string 형태) |
bs4 (BeautifulSoup) | HTML 문자 데이터를 파싱하여 태그 기준으로 데이터 추출 |
selenium | 웹 브라우저를 파이썬 코드로 제어 (버튼 클릭, 데이터 입력 등) |
⚠️ 웹 크롤링은 사이트별로 허용 범위가 다릅니다.
상업적 용도 사용 시 법적 문제가 생길 수 있으며, 수업에서는 학습 목적으로만 활용합니다.
# !pip install bs4 selenium
import requests
from bs4 import BeautifulSoup as bs
res = requests.get('https://www.naver.com')
res.content # bytes 형태
res.text # 문자열 형태
html_text = res.text
soup = bs(html_text, 'html.parser')
soup.a # 문서에서 첫 번째 a 태그
soup.a.string # 첫 번째 a 태그의 콘텐츠(텍스트)
soup.a['href'] # 첫 번째 a 태그의 href 속성값
💡
soup.태그명은 문자열의find()처럼 첫 번째 항목만 반환합니다.
| 함수 | 반환 타입 | 설명 |
|---|---|---|
find(태그) | Tag | 첫 번째 태그 반환 |
find(태그, attrs={속성: 값}) | Tag | 특정 속성을 가진 첫 번째 태그 |
find_all(태그) | ResultSet | 모든 태그 반환 (리스트 형태) |
find_all(태그, limit=n) | ResultSet | 최대 n개 반환 |
⚠️
find(),find_all()은 Tag 타입에서만 사용 가능합니다.
ResultSet(리스트 형태)에서는 직접 사용 불가 → 반복문으로 각 Tag에 접근해야 합니다.
soup.find('a') # 첫 번째 a 태그 (Tag 타입)
soup.find_all('a') # 모든 a 태그 (ResultSet 타입)
a_tag = soup.find('a')
a_tag.find('span') # Tag에서 find() 가능 ✅
a_list = soup.find_all('a')
# a_list.find('span') # ResultSet에서 find() 불가 ❌
a_list = soup.find_all('a')
# 방법 1 — for문
a_datas = []
for tag in a_list:
a_datas.append(tag.string)
# 방법 2 — map() + lambda
list(map(lambda x: x.string, a_list))
# 방법 3 — 리스트 컴프리헨션 ✅ 권장
[data.string for data in a_list]

# requests 라이브러리를 이용해서 html 문서를 받는다
result = requests.get('https://finance.naver.com')
# 응답 데이터를 문자형으로 변환 -> BeautifulSoup 파싱
soup = bs(result.text, 'html.parser')
# html문서 안에서 태그명이 div이고 class 속성의 값이 section_sise_top인 태그 찾기
div_data = soup.find('div', attrs={'class': 'section_sise_top'})
# div 안에서 class가 'tbl_home'인 table 모두 찾기 (8개 나옴)
tables = div_data.find_all('table', attrs={'class': 'tbl_home'})

table_data = tables[0]
# thead → 컬럼명 추출
head_data = table_data.find('thead')
th_list = head_data.find_all('th')
cols = [th.string for th in th_list]
# tbody → 값 추출
body_data = table_data.find('tbody')
tr_list = body_data.find_all('tr')
values = []
for tr in tr_list:
row_list = tr.find_all(['th', 'td']) # th, td 동시에 찾기
value_data = [data.get_text().strip() for data in row_list]
values.append(value_data)
import pandas as pd
pd.DataFrame(values, columns=cols)
💡
stringvsget_text()
string— 태그 안 콘텐츠가 단일 텍스트일 때만 반환get_text()— 태그 안 모든 콘텐츠 추출 (중첩 태그 포함)- 크롤링에서는
get_text().strip()이 더 안전합니다.
# 8개의 테이블 중 1개 완성
# 8번 반복을 하는 반복문을 생성 -> 각각의 데이터프레임이 생성 -> 각각 다른 전역 변수에 저장( globals()['변수명] )
# 전역변수의 이름을 반복 실행할때마다 변경하기 위한 기본값
vari_num = 1
for table_data in tables:
# thead를 추출 -> columns 데이터 추출하기 위해서
head_data = table_data.find('thead')
# thead 데이터에서 각각 콘텐츠 데이터를 추출하여 리스트화
cols = [th.string for th in head_data.find_all('th')]
# tbody를 추출
body_data = table_data.find('tbody')
# 여러개의 행이 존재 -> tr_list를 생성
tr_list = body_data.find_all('tr')
# 각각의 tr_data를 추출하여 콘텐츠 데이터를 리스트화 하고 새로운 리스트에 추가
values = []
for tr in tr_list:
row_list = tr.find_all(['th', 'td'])
value_data = [data.get_text().strip() for data in row_list]
values.append(value_data)
# 데이터프레임을 생성하고 전역변수에 저장
globals()[f'df{vari_num}'] = pd.DataFrame(values, columns=cols)
vari_num += 1
# df1 ~ df8 생성됨



# !pip install html5lib
dfs = pd.read_html(str(div_data)) # div 안의 table을 바로 DataFrame 리스트로 반환
dfs[1]
💡
pd.read_html()은 HTML 안의<table>태그를 자동으로 찾아 DataFrame 리스트로 반환합니다.
BeautifulSoup으로 직접 파싱하는 것보다 훨씬 간결합니다.

import pandas as pd
import requests
from bs4 import BeutifulSoup as bs
dfs = pd.read_html('https://comp.wisereport.co.kr/company/c1010001.aspx?cmp_cd=005930', encoding = 'cp949')
dfs[2][0][0].split()[0]
dfs[3]

import pandas as pd
import requests
# 크롤링에서 반복문을 이용해서 사이트에 접속 -> 딜레이 기능 부여
import time
# 현재 시간을 로드
from datetime import datetime
# 오늘 날짜 — 파일명에 사용
now = datetime.now()
# 시계열 데이터에서 원하는 데이터만 문자로 추출하는 방법 : strftime(format)
# 2자리년도수-월-일
now_str = now.strftime('%y-%m-%d')
code_list = ['005930', '009150', '000660', '080220', '096770']
base_url = 'https://comp.wisereport.co.kr/company/c1010001.aspx?cmp_cd='
for code in code_list:
df = pd.read_html(base_url + code, encoding='cp949')[3] # 4번째 표 선택
df.to_csv(f'./{code} {now_str}.csv') # 종목코드 날짜.csv로 저장
time.sleep(1) # 딜레이 — 서버 부하 방지
💡
time.sleep(n)— n초 딜레이
크롤링 시 서버에 과도한 요청을 보내지 않기 위해 반드시 딜레이를 추가합니다.
💡 파일명에
now_str을 포함하면 날짜별로 새로운 파일이 생성됩니다.
포함하지 않으면 같은 파일에 데이터가 덮어씌워집니다.
code_list = []
# 반복문을 생성을 하는데 반복 횟수는 명확하지 않다.
while True:
input_code = input('종목 코드를 입력하시오 (종료하려면 ENTER)')
if len(input_code) == 6: # 종목 코드는 6자리
code_list.append(input_code)
if not bool(input_code): # 빈 입력이면 종료
break
# 입력된 종목 코드로 크롤링 실행
now = datetime.now()
now_str = now.strftime('%y-%m-%d')
for code in code_list:
# read_html을 이용해서 table데이터를 크롤링 -> 4번째 표를 선택
df = pd.read_html(base_url + code, encoding='cp949')[3]
# 데이터프레임을 csv 파일로 저장 (파일의 이름은 종목코드.csv)
# 데이터프레임 타입에서 csv 타입으로 변경 -> 타입이 변경될때 사용하는 키워드(to)
# 같은 파일에 새로운 데이터가 입혀지는 형태
# 날짜별로 실행할때마다 새로운 파일이 생성(현재 시간을 로드 파일명으로 사용)
df.to_csv(f'./{code} {now_str}.csv')
# 시간의 딜레이 생성
time.sleep(1)
반복적으로 사용하는 크롤링 코드를 별도의 .py 파일로 분리하면 재사용이 편합니다.
import pandas as pd
import time
from datetime import datetime
# 종목 코드를 사용자 입력으로 수집
code_list = []
while True:
input_code = input('종목 코드를 입력하시오 ( 입력값 종료 시 ENTER )')
if len(input_code) == 6: # 종목 코드는 6자리
code_list.append(input_code)
if not bool(input_code): # 빈 입력 → 반복 종료
break
# 오늘 날짜 — 파일명에 사용
now = datetime.now()
now_str = now.strftime('%y-%m-%d')
base_url = 'https://comp.wisereport.co.kr/company/c1010001.aspx?cmp_cd='
for code in code_list:
# 4번째 표(index 3)를 크롤링
df = pd.read_html(base_url + code)[3]
# '종목코드 날짜.csv' 형태로 저장
# → 같은 종목을 다른 날짜에 실행하면 새로운 파일이 생성됨
df.to_csv(f'./{code} {now_str}.csv')
# 서버 부하 방지를 위한 딜레이
time.sleep(1)

사용자 입력 (while True)
↓
6자리 코드 → code_list에 추가
엔터 입력 → 반복 종료
↓
오늘 날짜 문자열 생성 (now_str)
↓
for code in code_list:
pd.read_html(URL)[3] → 4번째 테이블 크롤링
df.to_csv(파일명) → CSV 저장
time.sleep(1) → 1초 딜레이
💡 모듈화의 장점
- Jupyter Notebook이 아닌 터미널에서 바로 실행 가능 (
python wise.py)- 다른 프로젝트에서
import wise로 재사용 가능- 코드를 기능별로 분리해 유지보수가 편해짐
| 함수 | 설명 |
|---|---|
requests.get(url, params) | 웹 서버에 GET 요청 |
res.text | 응답 데이터를 문자열로 반환 |
res.content | 응답 데이터를 bytes로 반환 |
bs(html, 'html.parser') | HTML 문자열 → BeautifulSoup 파싱 |
soup.태그명 | 첫 번째 태그 반환 |
soup.태그명.string | 첫 번째 태그의 텍스트 반환 |
soup.태그명['속성'] | 첫 번째 태그의 속성값 반환 |
soup.find(태그, attrs={}) | 조건에 맞는 첫 번째 태그 반환 (Tag) |
soup.find_all(태그) | 조건에 맞는 모든 태그 반환 (ResultSet) |
태그.get_text().strip() | 태그 안 전체 텍스트 추출 + 공백 제거 |
pd.read_html(url) | URL의 table 태그를 DataFrame 리스트로 반환 |
df.to_csv('파일명.csv') | DataFrame → CSV 파일 저장 |
time.sleep(n) | n초 딜레이 |
datetime.now().strftime(포맷) | 현재 시간을 문자열로 변환 |