주식분석보고서
주식분석보고서
코드 블록의 Try it Yourself 버튼으로 직접 실행할 수 있다.
구문
1. 주식 분석 보고서 자동화 프로젝트
1.1. 프로젝트 개요
주식 정보를 자동으로 수집하여 분석 보고서를 만들고 이메일로 전송
- [최종 결과물]
- 주식 일별 시세 데이터 수집
- 차트 및 테이블 이미지 생성
- PowerPoint 보고서 자동 작성
- 이메일 자동 전송
1.2. 필요한 라이브러리 설치
라이브러리 — 다른 사람이 만들어 놓은 도구 모음이다. 파이썬에 기본 포함되지 않아 따로 설치한다.
matplotlib— 차트(그래프)를 그리는 도구pandas— 표 형태의 데이터를 다루는 도구python-pptx— 파워포인트 파일을 만드는 도구beautifulsoup4— 파이썬용 HTML 및 XML 데이터 추출html5lib— 웹 브라우저(크롬, 파이어폭스 등)와 동일한 방식으로 HTML을 분석
pip install matplotlib
pip install pandas
pip install python-pptx
pip install lxml
pip install beautifulsoup4
pip install html5lib
pip install request
1.3. Step 1: 종목코드 및 일별 시세 가져오기
1.3.1. 종목코드 이해하기
종목코드 — 주식 시장에 등록된 회사마다 붙는 고유 번호이다. 사람의 주민번호와 같은 개념이다.
삼성전자 → 005930 (6자리 숫자)
1.3.2. 종목코드 다운로드
한국 거래소(KRX)에서 공개한 상장법인목록을 다운로드한다.
import pandas as pd
def get_stock_code():
stock_code = pd.read_html("http://kind.krx.co.kr/corpgeneral/corpList.do?method=download", encoding='cp949', header=0)[0]
stock_code = stock_code[['회사명', '종목코드']]
stock_code = stock_code.rename(columns={'회사명': 'company', '종목코드': 'code'})
stock_code.code = stock_code.code.map('{:06d}'.format)
print(stock_code)
return stock_code
get_stock_code()
None: 1:
import pandas as pd— pandas 라이브러리를pd라는 짧은 이름으로 가져온다3:
def get_stock_code():— 종목코드를 가져오는 함수를 만든다4~5:
pd.read_html(...)— 웹페이지의 표(HTML 테이블)를 읽어온다.header=0은 첫 번째 행을 열 이름으로 사용한다.[0]은 페이지에 있는 여러 표 중 첫 번째를 선택한다6:
stock_code[['회사명', '종목코드']]— 전체 열 중 회사명과 종목코드 두 개만 남긴다7:
.rename(columns={...})— 열 이름을 한글에서 영어로 바꾼다8:
.map('{:06d}'.format)— 종목코드를 6자리로 맞춘다. 예:5930→0059309:
return stock_code— 완성된 데이터를 돌려준다11:
stock_code = get_stock_code()— 함수를 실행하여 결과를 변수에 저장한다12:
print(stock_code.head())— 상위 5개 데이터를 출력하여 확인한다
1.3.3. 일별 시세 가져오기
크롤링 — 웹페이지에서 데이터를 자동으로 수집하는 것이다. 네이버 금융에서 특정 회사의 일별 시세를 가져온다.
URL 구조:
http://finance.naver.com/item/sise_day.nhn?code={종목코드}&page={페이지번호}
import requests
def get_stock(code):
df = pd.DataFrame()
for page in range(1, 21):
url = 'http://finance.naver.com/item/sise_day.nhn?code={code}'.format(code=code)
url = '{url}&page={page}'.format(url=url, page=page)
header = {'User-Agent': '<your-user-agent>'}
res = requests.get(url, headers=header)
current_df = pd.read_html(res.text, header=0)[0]
df = df.append(current_df, ignore_index=True)
return df
code = '005930'
df = get_stock(code)
print(df.head())
None: 1:
import requests— 웹페이지에 접속하는 라이브러리를 가져온다3:
def get_stock(code):— 종목코드를 받아 일별 시세를 가져오는 함수를 만든다4:
df = pd.DataFrame()— 빈 표(데이터프레임)를 만든다. 여기에 데이터를 쌓는다6:
for page in range(1, 21):— 1페이지부터 20페이지까지 반복한다. 약 40주간의 데이터이다7:
url = '...{code}'.format(code=code)— 종목코드를 넣어 URL을 만든다8:
url = '{url}&page={page}'.format(...)— 페이지 번호를 URL에 추가한다9:
header = {'User-Agent': '...'}— 브라우저인 척 접속하기 위한 설정이다. 없으면 차단된다10:
res = requests.get(url, headers=header)— 해당 URL에 접속하여 페이지 내용을 가져온다11:
current_df = pd.read_html(res.text, header=0)[0]— 가져온 HTML에서 표 데이터를 추출한다12:
df = df.append(current_df, ignore_index=True)— 추출한 데이터를 기존 표에 이어 붙인다14:
return df— 모든 페이지의 데이터가 합쳐진 표를 돌려준다16:
code = '005930'— 삼성전자의 종목코드를 지정한다17:
df = get_stock(code)— 함수를 실행하여 시세 데이터를 가져온다18:
print(df.head())— 상위 5개 데이터를 출력하여 확인한다
1.3.4. 데이터 정제 (Clean)
정제 — 수집한 데이터에서 빈 값이나 형식이 맞지 않는 부분을 정리하는 과정이다.
def clean_data(df):
df = df.dropna()
df = df.rename(columns={
'날짜': 'date',
'종가': 'close',
'전일비': 'diff',
'시가': 'open',
'고가': 'high',
'저가': 'low',
'거래량': 'volume'
})
df[['close', 'diff', 'open', 'high', 'low', 'volume']] = \
df[['close', 'diff', 'open', 'high', 'low', 'volume']].astype(int)
df['date'] = pd.to_datetime(df['date'])
df = df.sort_values(by=['date'], ascending=True)
return df
df = clean_data(df)
print(df)
None: 1:
def clean_data(df):— 데이터를 정제하는 함수를 만든다2:
df.dropna()— 빈 값(NaN)이 있는 행을 삭제한다3~11:
.rename(columns={...})— 한글 열 이름을 영어로 바꾼다. 코드에서 다루기 편하다12~13:
.astype(int)— 문자형 숫자를 정수(int)로 변환한다. 계산할 수 있게 된다14:
pd.to_datetime(...)— 날짜 문자열을 날짜 형식으로 변환한다. 정렬이나 비교가 가능해진다15:
.sort_values(by=['date'], ascending=True)— 날짜 기준으로 오래된 순서부터 정렬한다16:
return df— 정제된 데이터를 돌려준다18:
df = clean_data(df)— 함수를 실행하여 정제된 데이터를 저장한다19:
print(df)— 정제된 데이터를 출력하여 확인한다
Tip: 정제 전: NaN 값, 문자형 숫자, 형식 불일치 정제 후: 정렬된 숫자 데이터, datetime 형식
1.4. Step 2: 보고자료 준비하기
수집한 데이터를 차트(그래프)와 테이블(표) 이미지로 만든다. 나중에 파워포인트에 넣기 위한 준비 단계이다.
1.4.1. 주식 차트 생성
matplotlib — 파이썬에서 그래프를 그리는 도구이다. 날짜별 종가(마감 가격)를 꺾은선 그래프로 그린다.
import matplotlib.pyplot as plt
from pandas.plotting import table
import os
plt.figure(figsize=(10, 4))
plt.plot(df['date'], df['close'])
plt.xlabel('date')
plt.ylabel('close')
chart_fname = os.path.join("res/stock_report", f'{company}_chart.png')
plt.savefig(chart_fname)
plt.show()
None: 1:
import matplotlib.pyplot as plt— 그래프 그리기 도구를plt라는 이름으로 가져온다2:
from pandas.plotting import table— 표를 이미지로 만드는 도구를 가져온다3:
import os— 파일 경로를 다루는 도구를 가져온다5:
plt.figure(figsize=(10, 4))— 가로 10, 세로 4 크기의 그래프 틀을 만든다6:
plt.plot(df['date'], df['close'])— x축은 날짜, y축은 종가로 꺾은선 그래프를 그린다7:
plt.xlabel('date')— x축 이름을 'date'로 지정한다8:
plt.ylabel('close')— y축 이름을 'close'로 지정한다10:
os.path.join(...)— 저장할 파일 경로를 만든다11:
plt.savefig(chart_fname)— 그래프를 PNG 이미지 파일로 저장한다12:
plt.show()— 그래프를 화면에 표시한다
1.4.2. 일별 시세 테이블 생성
최근 10일간의 시세를 표 이미지로 만든다.
plt.figure(figsize=(15, 4))
ax = plt.subplot(111, frame_on=False)
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
df_sorted = df.sort_values(by=['date'], ascending=False)
table(ax, df_sorted.head(10), loc='center', cellLoc='center', rowLoc='center')
table_fname = os.path.join("res/stock_report", f'{company}_table.png')
plt.savefig(table_fname)
None: 1:
plt.figure(figsize=(15, 4))— 가로 15, 세로 4 크기의 그래프 틀을 만든다2:
plt.subplot(111, frame_on=False)— 테두리 없는 영역을 만든다. 표만 보이게 하기 위한 설정이다3:
ax.xaxis.set_visible(False)— x축 눈금을 숨긴다4:
ax.yaxis.set_visible(False)— y축 눈금을 숨긴다6:
.sort_values(by=['date'], ascending=False)— 최신 날짜가 위에 오도록 정렬한다7:
table(ax, df_sorted.head(10), ...)— 상위 10개 데이터를 표로 그린다. 가운데 정렬이다9:
os.path.join(...)— 저장할 파일 경로를 만든다10:
plt.savefig(table_fname)— 표를 PNG 이미지 파일로 저장한다
1.5. Step 3: 보고서 작성하기
python-pptx — 파이썬으로 파워포인트(.pptx) 파일을 만드는 도구이다. 앞에서 만든 차트와 테이블 이미지를 슬라이드에 넣는다.
1.5.1. PowerPoint 기본 설정
import datetime
from pptx import Presentation
from pptx.util import Inches
today = datetime.datetime.today().strftime('%Y%m%d')
prs = Presentation()
None: 1:
import datetime— 날짜/시간을 다루는 도구를 가져온다2:
from pptx import Presentation— 파워포인트 파일을 만드는 도구를 가져온다3:
from pptx.util import Inches— 인치 단위로 크기를 지정하는 도구를 가져온다5:
.today().strftime('%Y%m%d')— 오늘 날짜를20260308형식의 문자열로 만든다6:
prs = Presentation()— 빈 파워포인트 파일을 만든다
1.5.2. 제목 슬라이드 추가
title_slide_layout = prs.slide_layouts[0]
slide = prs.slides.add_slide(title_slide_layout)
title = slide.shapes.title
title.text = "주식 보고서"
subtitle = slide.placeholders[1]
subtitle.text = f"보고서 작성일: {today}"
None: 1:
prs.slide_layouts[0]— 0번 레이아웃(제목 슬라이드)을 선택한다2:
prs.slides.add_slide(...)— 선택한 레이아웃으로 새 슬라이드를 추가한다4:
slide.shapes.title— 슬라이드의 제목 영역을 가져온다5:
title.text = "주식 보고서"— 제목에 텍스트를 넣는다7:
slide.placeholders[1]— 1번 자리표시자(부제목 영역)를 가져온다8:
subtitle.text = f"..."— 부제목에 오늘 날짜를 넣는다
1.5.3. 차트 & 테이블 슬라이드 추가
title_only_slide_layout = prs.slide_layouts[5]
slide = prs.slides.add_slide(title_only_slide_layout)
shapes = slide.shapes
latest_close = df.iloc[0]['close']
shapes.title.text = f'{company}, {latest_close} 원에 거래 마감'
left = Inches(0.5)
top = Inches(2)
width = Inches(9)
height = Inches(2.5)
pic1 = slide.shapes.add_picture(chart_fname, left, top, width=width, height=height)
left = Inches(-1)
top = Inches(4)
width = Inches(12)
height = Inches(3)
pic2 = slide.shapes.add_picture(table_fname, left, top, width=width, height=height)
None: 1:
prs.slide_layouts[5]— 5번 레이아웃(제목만 있는 슬라이드)을 선택한다2:
prs.slides.add_slide(...)— 새 슬라이드를 추가한다4:
slide.shapes— 슬라이드 안의 도형 모음을 가져온다6:
df.iloc[0]['close']— 첫 번째 행의 종가(마감 가격)를 가져온다7:
shapes.title.text = f'...'— 슬라이드 제목에 회사명과 종가를 넣는다9~12:
Inches(...)— 차트 이미지의 위치와 크기를 인치 단위로 지정한다13:
add_picture(...)— 차트 이미지를 슬라이드에 삽입한다15~18:
Inches(...)— 테이블 이미지의 위치와 크기를 지정한다19:
add_picture(...)— 테이블 이미지를 슬라이드에 삽입한다
1.5.4. 보고서 저장
ppt_fname = os.path.join("res/stock_report", 'stock_report.pptx')
prs.save(ppt_fname)
print(f"보고서 저장 완료: {ppt_fname}")
None: 1:
os.path.join(...)— 저장할 파일 경로를 만든다2:
prs.save(ppt_fname)— 파워포인트 파일을 저장한다3:
print(...)— 저장 완료 메시지를 출력한다
1.6. Step 4: 이메일 전송
SMTP — 이메일을 보내는 통신 규약이다. 우체국에서 편지를 배달하는 규칙과 같다. 파이썬의 smtplib로 네이버 메일 서버에 접속하여 보고서를 첨부한 이메일을 보낸다.
1.6.1. SMTP로 이메일 보내는 함수
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
import os
def send_email(smtp_info, msg):
with smtplib.SMTP(smtp_info["smtp_server"], smtp_info["smtp_port"]) as server:
server.starttls()
server.login(smtp_info["smtp_user_id"], smtp_info["smtp_user_pw"])
response = server.sendmail(msg['from'], msg['to'], msg.as_string())
if not response:
print("이메일 전송 성공")
else:
print(f"오류: {response}")
None: 1:
import smtplib— 이메일 전송 도구를 가져온다2:
from email.mime.multipart import MIMEMultipart— 첨부파일이 있는 메일을 만드는 도구이다3:
from email.mime.base import MIMEBase— 파일 첨부 형식을 다루는 도구이다4:
from email import encoders— 첨부파일을 인코딩(변환)하는 도구이다5:
import os— 파일 경로를 다루는 도구를 가져온다7:
def send_email(smtp_info, msg):— SMTP 정보와 메시지를 받아 이메일을 보내는 함수이다8:
smtplib.SMTP(...)— 메일 서버에 접속한다.with를 쓰면 끝나면 자동으로 연결을 끊는다9:
server.starttls()— TLS 보안 연결을 시작한다. 이메일 내용을 암호화한다10:
server.login(...)— 아이디와 비밀번호로 로그인한다11:
server.sendmail(...)— 발신자, 수신자, 메일 내용을 넣어 전송한다13~16: 전송 결과를 확인한다. 응답이 비어 있으면 성공, 아니면 오류를 출력한다
1.6.2. 첨부파일 포함 메시지 생성
MIME — 이메일에 텍스트, 이미지, 파일 등을 함께 담는 형식이다. 편지 봉투에 여러 서류를 넣는 것과 같다.
def make_multimsg(msg_dict):
multi = MIMEMultipart(_subtype='mixed')
for key, value in msg_dict.items():
if key == 'text':
from email.mime.text import MIMEText
with open(value['filename'], encoding='utf-8') as fp:
msg = MIMEText(fp.read(), _subtype=value['subtype'])
elif key == 'image':
from email.mime.image import MIMEImage
with open(value['filename'], 'rb') as fp:
msg = MIMEImage(fp.read(), _subtype=value['subtype'])
else:
with open(value['filename'], 'rb') as fp:
msg = MIMEBase(value['maintype'], _subtype=value['subtype'])
msg.set_payload(fp.read())
encoders.encode_base64(msg)
_, fname = os.path.split(value['filename'])
msg.add_header('Content-Disposition', 'attachment', filename=fname)
multi.attach(msg)
return multi
None: 1:
def make_multimsg(msg_dict):— 첨부파일 정보를 받아 메시지를 만드는 함수이다2:
MIMEMultipart(_subtype='mixed')— 여러 종류의 첨부파일을 담을 수 있는 빈 메시지를 만든다4:
for key, value in msg_dict.items():— 첨부파일 정보를 하나씩 꺼내 반복한다5~8:
key == 'text'— 텍스트 파일이면 UTF-8로 읽어 텍스트 형식으로 첨부한다9~12:
key == 'image'— 이미지 파일이면 바이너리(rb)로 읽어 이미지 형식으로 첨부한다13~17: 그 외 파일(pptx 등)은 범용 형식(MIMEBase)으로 읽고 base64로 인코딩한다
19:
os.path.split(...)— 전체 경로에서 파일명만 분리한다20:
add_header(...)— 첨부파일의 이름을 지정한다21:
multi.attach(msg)— 만들어진 첨부파일을 메시지에 추가한다23:
return multi— 완성된 메시지를 돌려준다
1.6.3. 메일 전송 실행
smtp_info = {
"smtp_server": "smtp.naver.com",
"smtp_user_id": "your_email@naver.com",
"smtp_user_pw": "your_password",
"smtp_port": 587
}
msg_dict = {
'application': {
'maintype': 'application',
'subtype': 'octet-stream',
'filename': 'res/stock_report/stock_report.pptx'
}
}
msg = make_multimsg(msg_dict)
msg['from'] = smtp_info["smtp_user_id"]
msg['to'] = "recipient@example.com"
msg['subject'] = "주식 분석 보고서"
send_email(smtp_info, msg)
None: 1~6:
smtp_info— 메일 서버 접속 정보를 딕셔너리에 저장한다
smtp_server— 네이버 메일 서버 주소이다
smtp_port— 587번 포트를 사용한다 (TLS 보안 전송용)8~14:
msg_dict— 첨부할 파일 정보를 지정한다
maintype,subtype— 파일 종류를 지정한다.octet-stream은 범용 바이너리 파일이다
filename— 첨부할 파일 경로이다16:
make_multimsg(msg_dict)— 첨부파일이 포함된 메시지를 만든다17:
msg['from']— 보내는 사람 이메일을 지정한다18:
msg['to']— 받는 사람 이메일을 지정한다19:
msg['subject']— 메일 제목을 지정한다21:
send_email(smtp_info, msg)— 이메일을 전송한다
Warning:
smtp_user_id와smtp_user_pw에 실제 아이디와 비밀번호를 넣어야 한다. 코드에 비밀번호를 직접 적으면 보안에 위험하므로 환경변수나 별도 설정 파일을 사용하는 것이 좋다.
1.7. 전체 코드 통합
import pandas as pd
import requests
import matplotlib.pyplot as plt
from pandas.plotting import table
from pptx import Presentation
from pptx.util import Inches
import smtplib
import datetime
import os
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
from email import encoders
# ─── 함수 정의 ───
def get_stock_code():
stock_code = pd.read_html('http://kind.krx.co.kr/corpgeneral/corpList.do?method=download', header=0)[0]
stock_code = stock_code[['회사명', '종목코드']]
stock_code = stock_code.rename(columns={'회사명': 'company', '종목코드': 'code'})
stock_code.code = stock_code.code.map('{:06d}'.format)
return stock_code
def get_stock(code):
df = pd.DataFrame()
for page in range(1, 21):
url = 'http://finance.naver.com/item/sise_day.nhn?code={code}'.format(code=code)
url = '{url}&page={page}'.format(url=url, page=page)
header = {'User-Agent': '<your-user-agent>'}
res = requests.get(url, headers=header)
current_df = pd.read_html(res.text, header=0)[0]
df = df.append(current_df, ignore_index=True)
return df
def clean_data(df):
df = df.dropna()
df = df.rename(columns={
'날짜': 'date',
'종가': 'close',
'전일비': 'diff',
'시가': 'open',
'고가': 'high',
'저가': 'low',
'거래량': 'volume'
})
df[['close', 'diff', 'open', 'high', 'low', 'volume']] = \
df[['close', 'diff', 'open', 'high', 'low', 'volume']].astype(int)
df['date'] = pd.to_datetime(df['date'])
df = df.sort_values(by=['date'], ascending=True)
return df
def create_chart(df, company):
plt.figure(figsize=(10, 4))
plt.plot(df['date'], df['close'])
plt.xlabel('date')
plt.ylabel('close')
chart_fname = os.path.join("res/stock_report", f'{company}_chart.png')
plt.savefig(chart_fname)
plt.show()
return chart_fname
def create_table(df, company):
plt.figure(figsize=(15, 4))
ax = plt.subplot(111, frame_on=False)
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
df_sorted = df.sort_values(by=['date'], ascending=False)
table(ax, df_sorted.head(10), loc='center', cellLoc='center', rowLoc='center')
table_fname = os.path.join("res/stock_report", f'{company}_table.png')
plt.savefig(table_fname)
return table_fname
def create_pptx(df, company, chart_fname, table_fname):
today = datetime.datetime.today().strftime('%Y%m%d')
prs = Presentation()
title_slide_layout = prs.slide_layouts[0]
slide = prs.slides.add_slide(title_slide_layout)
title = slide.shapes.title
title.text = "주식 보고서"
subtitle = slide.placeholders[1]
subtitle.text = f"보고서 작성일: {today}"
title_only_slide_layout = prs.slide_layouts[5]
slide = prs.slides.add_slide(title_only_slide_layout)
shapes = slide.shapes
latest_close = df.iloc[-1]['close']
shapes.title.text = f'{company}, {latest_close} 원에 거래 마감'
left = Inches(0.5)
top = Inches(2)
width = Inches(9)
height = Inches(2.5)
slide.shapes.add_picture(chart_fname, left, top, width=width, height=height)
left = Inches(-1)
top = Inches(4)
width = Inches(12)
height = Inches(3)
slide.shapes.add_picture(table_fname, left, top, width=width, height=height)
ppt_fname = os.path.join("res/stock_report", 'stock_report.pptx')
prs.save(ppt_fname)
print(f"보고서 저장 완료: {ppt_fname}")
return ppt_fname
def send_email(smtp_info, msg):
with smtplib.SMTP(smtp_info["smtp_server"], smtp_info["smtp_port"]) as server:
server.starttls()
server.login(smtp_info["smtp_user_id"], smtp_info["smtp_user_pw"])
response = server.sendmail(msg['from'], msg['to'], msg.as_string())
if not response:
print("이메일 전송 성공")
else:
print(f"오류: {response}")
def make_multimsg(msg_dict):
multi = MIMEMultipart(_subtype='mixed')
for key, value in msg_dict.items():
if key == 'text':
with open(value['filename'], encoding='utf-8') as fp:
msg = MIMEText(fp.read(), _subtype=value['subtype'])
elif key == 'image':
with open(value['filename'], 'rb') as fp:
msg = MIMEImage(fp.read(), _subtype=value['subtype'])
else:
with open(value['filename'], 'rb') as fp:
msg = MIMEBase(value['maintype'], _subtype=value['subtype'])
msg.set_payload(fp.read())
encoders.encode_base64(msg)
_, fname = os.path.split(value['filename'])
msg.add_header('Content-Disposition', 'attachment', filename=fname)
multi.attach(msg)
return multi
# ─── 실행 ───
company = '삼성전자'
# Step 1: 데이터 수집
stock_code_df = get_stock_code()
code = stock_code_df[stock_code_df.company == company].code.values[0].strip()
df = get_stock(code)
df = clean_data(df)
# Step 2: 보고자료 생성
chart_fname = create_chart(df, company)
table_fname = create_table(df, company)
# Step 3: 보고서 작성
ppt_fname = create_pptx(df, company, chart_fname, table_fname)
# Step 4: 이메일 전송
smtp_info = {
"smtp_server": "smtp.naver.com",
"smtp_user_id": "your_email@naver.com",
"smtp_user_pw": "your_password",
"smtp_port": 587
}
msg_dict = {
'application': {
'maintype': 'application',
'subtype': 'octet-stream',
'filename': ppt_fname
}
}
msg = make_multimsg(msg_dict)
msg['from'] = smtp_info["smtp_user_id"]
msg['to'] = "recipient@example.com"
msg['subject'] = "주식 분석 보고서"
send_email(smtp_info, msg)
print("모든 작업이 완료되었다!")