06 주식분석보고서
1. 주식 분석 보고서 자동화 프로젝트
1.1. 프로젝트 개요
주식 정보를 자동으로 수집하여 분석 보고서를 만들고 이메일로 전송
- [최종 결과물]
- 주식 일별 시세 데이터 수집
- 차트 및 테이블 이미지 생성
- PowerPoint 보고서 자동 작성
- 이메일 자동 전송
1.2. 필요한 라이브러리 설치
라이브러리 — 다른 사람이 만들어 놓은 도구 모음이다. 파이썬에 기본 포함되지 않아 따로 설치한다.
matplotlib— 차트(그래프)를 그리는 도구pandas— 표 형태의 데이터를 다루는 도구python-pptx— 파워포인트 파일을 만드는 도구beautifulsoup4— 파이썬용 HTML 및 XML 데이터 추출html5lib— 웹 브라우저(크롬, 파이어폭스 등)와 동일한 방식으로 HTML을 분석
1pip install matplotlib2pip install pandas3pip install python-pptx4pip install lxml5pip install beautifulsoup46pip install html5lib7pip install request1.3. Step 1: 종목코드 및 일별 시세 가져오기
1.3.1. 종목코드 이해하기
종목코드 — 주식 시장에 등록된 회사마다 붙는 고유 번호이다. 사람의 주민번호와 같은 개념이다.
1삼성전자 → 005930 (6자리 숫자)1.3.2. 종목코드 다운로드
한국 거래소(KRX)에서 공개한 상장법인목록을 다운로드한다.
KRX 자료 다운로드1import pandas as pd2
3def get_stock_code():4 stock_code = pd.read_html("http://kind.krx.co.kr/corpgeneral/corpList.do?method=download", encoding='cp949', header=0)[0]5 stock_code = stock_code[['회사명', '종목코드']]6 stock_code = stock_code.rename(columns={'회사명': 'company', '종목코드': 'code'})7 stock_code.code = stock_code.code.map('{:06d}'.format)8 print(stock_code)9 return stock_code10get_stock_code()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 → 005930
9: return stock_code — 완성된 데이터를 돌려준다
11: stock_code = get_stock_code() — 함수를 실행하여 결과를 변수에 저장한다
12: print(stock_code.head()) — 상위 5개 데이터를 출력하여 확인한다
1.3.3. 일별 시세 가져오기
크롤링 — 웹페이지에서 데이터를 자동으로 수집하는 것이다. 네이버 금융에서 특정 회사의 일별 시세를 가져온다.
URL 구조:
1http://finance.naver.com/item/sise_day.nhn?code={종목코드}&page={페이지번호}1import requests2
3def get_stock(code):4 df = pd.DataFrame()5
6 for page in range(1, 21):7 url = 'http://finance.naver.com/item/sise_day.nhn?code={code}'.format(code=code)8 url = '{url}&page={page}'.format(url=url, page=page)9 header = {'User-Agent': '<your-user-agent>'}10 res = requests.get(url, headers=header)11 current_df = pd.read_html(res.text, header=0)[0]12 df = df.append(current_df, ignore_index=True)13
14 return df15
16code = '005930'17df = get_stock(code)18print(df.head())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)
정제 — 수집한 데이터에서 빈 값이나 형식이 맞지 않는 부분을 정리하는 과정이다.
1def clean_data(df):2 df = df.dropna()3 df = df.rename(columns={4 '날짜': 'date',5 '종가': 'close',6 '전일비': 'diff',7 '시가': 'open',8 '고가': 'high',9 '저가': 'low',10 '거래량': 'volume'11 })12 df[['close', 'diff', 'open', 'high', 'low', 'volume']] = \13 df[['close', 'diff', 'open', 'high', 'low', 'volume']].astype(int)14 df['date'] = pd.to_datetime(df['date'])15 df = df.sort_values(by=['date'], ascending=True)16 return df17
18df = clean_data(df)19print(df)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) — 정제된 데이터를 출력하여 확인한다
요약
정제 전: NaN 값, 문자형 숫자, 형식 불일치 정제 후: 정렬된 숫자 데이터, datetime 형식
1.4. Step 2: 보고자료 준비하기
수집한 데이터를 차트(그래프)와 테이블(표) 이미지로 만든다. 나중에 파워포인트에 넣기 위한 준비 단계이다.
1.4.1. 주식 차트 생성
matplotlib — 파이썬에서 그래프를 그리는 도구이다. 날짜별 종가(마감 가격)를 꺾은선 그래프로 그린다.
1import matplotlib.pyplot as plt2from pandas.plotting import table3import os4
5plt.figure(figsize=(10, 4))6plt.plot(df['date'], df['close'])7plt.xlabel('date')8plt.ylabel('close')9
10chart_fname = os.path.join("res/stock_report", f'{company}_chart.png')11plt.savefig(chart_fname)12plt.show()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일간의 시세를 표 이미지로 만든다.
1plt.figure(figsize=(15, 4))2ax = plt.subplot(111, frame_on=False)3ax.xaxis.set_visible(False)4ax.yaxis.set_visible(False)5
6df_sorted = df.sort_values(by=['date'], ascending=False)7table(ax, df_sorted.head(10), loc='center', cellLoc='center', rowLoc='center')8
9table_fname = os.path.join("res/stock_report", f'{company}_table.png')10plt.savefig(table_fname)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 기본 설정
1import datetime2from pptx import Presentation3from pptx.util import Inches4
5today = datetime.datetime.today().strftime('%Y%m%d')6prs = Presentation()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. 제목 슬라이드 추가
1title_slide_layout = prs.slide_layouts[0]2slide = prs.slides.add_slide(title_slide_layout)3
4title = slide.shapes.title5title.text = "주식 보고서"6
7subtitle = slide.placeholders[1]8subtitle.text = f"보고서 작성일: {today}"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. 차트 & 테이블 슬라이드 추가
1title_only_slide_layout = prs.slide_layouts[5]2slide = prs.slides.add_slide(title_only_slide_layout)3
4shapes = slide.shapes5
6latest_close = df.iloc[0]['close']7shapes.title.text = f'{company}, {latest_close} 원에 거래 마감'8
9left = Inches(0.5)10top = Inches(2)11width = Inches(9)12height = Inches(2.5)13pic1 = slide.shapes.add_picture(chart_fname, left, top, width=width, height=height)14
15left = Inches(-1)16top = Inches(4)17width = Inches(12)18height = Inches(3)19pic2 = slide.shapes.add_picture(table_fname, left, top, width=width, height=height)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. 보고서 저장
1ppt_fname = os.path.join("res/stock_report", 'stock_report.pptx')2prs.save(ppt_fname)3print(f"보고서 저장 완료: {ppt_fname}")1: os.path.join(...) — 저장할 파일 경로를 만든다
2: prs.save(ppt_fname) — 파워포인트 파일을 저장한다
3: print(...) — 저장 완료 메시지를 출력한다
1.6. Step 4: 이메일 전송
SMTP — 이메일을 보내는 통신 규약이다. 우체국에서 편지를 배달하는 규칙과 같다. 파이썬의 smtplib로 네이버 메일 서버에 접속하여 보고서를 첨부한 이메일을 보낸다.
1.6.1. SMTP로 이메일 보내는 함수
1import smtplib2from email.mime.multipart import MIMEMultipart3from email.mime.base import MIMEBase4from email import encoders5import os6
7def send_email(smtp_info, msg):8 with smtplib.SMTP(smtp_info["smtp_server"], smtp_info["smtp_port"]) as server:9 server.starttls()10 server.login(smtp_info["smtp_user_id"], smtp_info["smtp_user_pw"])11 response = server.sendmail(msg['from'], msg['to'], msg.as_string())12
13 if not response:14 print("이메일 전송 성공")15 else:16 print(f"오류: {response}")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 — 이메일에 텍스트, 이미지, 파일 등을 함께 담는 형식이다. 편지 봉투에 여러 서류를 넣는 것과 같다.
1def make_multimsg(msg_dict):2 multi = MIMEMultipart(_subtype='mixed')3
4 for key, value in msg_dict.items():5 if key == 'text':6 from email.mime.text import MIMEText7 with open(value['filename'], encoding='utf-8') as fp:8 msg = MIMEText(fp.read(), _subtype=value['subtype'])9 elif key == 'image':10 from email.mime.image import MIMEImage11 with open(value['filename'], 'rb') as fp:12 msg = MIMEImage(fp.read(), _subtype=value['subtype'])13 else:14 with open(value['filename'], 'rb') as fp:15 msg = MIMEBase(value['maintype'], _subtype=value['subtype'])16 msg.set_payload(fp.read())17 encoders.encode_base64(msg)18
19 _, fname = os.path.split(value['filename'])20 msg.add_header('Content-Disposition', 'attachment', filename=fname)21 multi.attach(msg)22
23 return multi1: 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. 메일 전송 실행
1smtp_info = {2 "smtp_server": "smtp.naver.com",3 "smtp_user_id": "your_email@naver.com",4 "smtp_user_pw": "your_password",5 "smtp_port": 5876}7
8msg_dict = {9 'application': {10 'maintype': 'application',11 'subtype': 'octet-stream',12 'filename': 'res/stock_report/stock_report.pptx'13 }14}15
16msg = make_multimsg(msg_dict)17msg['from'] = smtp_info["smtp_user_id"]18msg['to'] = "recipient@example.com"19msg['subject'] = "주식 분석 보고서"20
21send_email(smtp_info, msg)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) — 이메일을 전송한다
주의
smtp_user_id와 smtp_user_pw에 실제 아이디와 비밀번호를 넣어야 한다. 코드에 비밀번호를 직접 적으면 보안에 위험하므로 환경변수나 별도 설정 파일을 사용하는 것이 좋다.
1.7. 전체 코드 통합
1import pandas as pd2import requests3import matplotlib.pyplot as plt4from pandas.plotting import table5from pptx import Presentation6from pptx.util import Inches7import smtplib8import datetime9import os10from email.mime.multipart import MIMEMultipart11from email.mime.base import MIMEBase12from email.mime.text import MIMEText13from email.mime.image import MIMEImage14from email import encoders15
16# ─── 함수 정의 ───17
18def get_stock_code():19 stock_code = pd.read_html('http://kind.krx.co.kr/corpgeneral/corpList.do?method=download', header=0)[0]20 stock_code = stock_code[['회사명', '종목코드']]21 stock_code = stock_code.rename(columns={'회사명': 'company', '종목코드': 'code'})22 stock_code.code = stock_code.code.map('{:06d}'.format)23 return stock_code24
25def get_stock(code):26 df = pd.DataFrame()27 for page in range(1, 21):28 url = 'http://finance.naver.com/item/sise_day.nhn?code={code}'.format(code=code)29 url = '{url}&page={page}'.format(url=url, page=page)30 header = {'User-Agent': '<your-user-agent>'}31 res = requests.get(url, headers=header)32 current_df = pd.read_html(res.text, header=0)[0]33 df = df.append(current_df, ignore_index=True)34 return df35
36def clean_data(df):37 df = df.dropna()38 df = df.rename(columns={39 '날짜': 'date',40 '종가': 'close',41 '전일비': 'diff',42 '시가': 'open',43 '고가': 'high',44 '저가': 'low',45 '거래량': 'volume'46 })47 df[['close', 'diff', 'open', 'high', 'low', 'volume']] = \48 df[['close', 'diff', 'open', 'high', 'low', 'volume']].astype(int)49 df['date'] = pd.to_datetime(df['date'])50 df = df.sort_values(by=['date'], ascending=True)51 return df52
53def create_chart(df, company):54 plt.figure(figsize=(10, 4))55 plt.plot(df['date'], df['close'])56 plt.xlabel('date')57 plt.ylabel('close')58 chart_fname = os.path.join("res/stock_report", f'{company}_chart.png')59 plt.savefig(chart_fname)60 plt.show()61 return chart_fname62
63def create_table(df, company):64 plt.figure(figsize=(15, 4))65 ax = plt.subplot(111, frame_on=False)66 ax.xaxis.set_visible(False)67 ax.yaxis.set_visible(False)68 df_sorted = df.sort_values(by=['date'], ascending=False)69 table(ax, df_sorted.head(10), loc='center', cellLoc='center', rowLoc='center')70 table_fname = os.path.join("res/stock_report", f'{company}_table.png')71 plt.savefig(table_fname)72 return table_fname73
74def create_pptx(df, company, chart_fname, table_fname):75 today = datetime.datetime.today().strftime('%Y%m%d')76 prs = Presentation()77
78 title_slide_layout = prs.slide_layouts[0]79 slide = prs.slides.add_slide(title_slide_layout)80 title = slide.shapes.title81 title.text = "주식 보고서"82 subtitle = slide.placeholders[1]83 subtitle.text = f"보고서 작성일: {today}"84
85 title_only_slide_layout = prs.slide_layouts[5]86 slide = prs.slides.add_slide(title_only_slide_layout)87 shapes = slide.shapes88 latest_close = df.iloc[-1]['close']89 shapes.title.text = f'{company}, {latest_close} 원에 거래 마감'90
91 left = Inches(0.5)92 top = Inches(2)93 width = Inches(9)94 height = Inches(2.5)95 slide.shapes.add_picture(chart_fname, left, top, width=width, height=height)96
97 left = Inches(-1)98 top = Inches(4)99 width = Inches(12)100 height = Inches(3)101 slide.shapes.add_picture(table_fname, left, top, width=width, height=height)102
103 ppt_fname = os.path.join("res/stock_report", 'stock_report.pptx')104 prs.save(ppt_fname)105 print(f"보고서 저장 완료: {ppt_fname}")106 return ppt_fname107
108def send_email(smtp_info, msg):109 with smtplib.SMTP(smtp_info["smtp_server"], smtp_info["smtp_port"]) as server:110 server.starttls()111 server.login(smtp_info["smtp_user_id"], smtp_info["smtp_user_pw"])112 response = server.sendmail(msg['from'], msg['to'], msg.as_string())113 if not response:114 print("이메일 전송 성공")115 else:116 print(f"오류: {response}")117
118def make_multimsg(msg_dict):119 multi = MIMEMultipart(_subtype='mixed')120 for key, value in msg_dict.items():121 if key == 'text':122 with open(value['filename'], encoding='utf-8') as fp:123 msg = MIMEText(fp.read(), _subtype=value['subtype'])124 elif key == 'image':125 with open(value['filename'], 'rb') as fp:126 msg = MIMEImage(fp.read(), _subtype=value['subtype'])127 else:128 with open(value['filename'], 'rb') as fp:129 msg = MIMEBase(value['maintype'], _subtype=value['subtype'])130 msg.set_payload(fp.read())131 encoders.encode_base64(msg)132 _, fname = os.path.split(value['filename'])133 msg.add_header('Content-Disposition', 'attachment', filename=fname)134 multi.attach(msg)135 return multi136
137# ─── 실행 ───138
139company = '삼성전자'140
141# Step 1: 데이터 수집142stock_code_df = get_stock_code()143code = stock_code_df[stock_code_df.company == company].code.values[0].strip()144df = get_stock(code)145df = clean_data(df)146
147# Step 2: 보고자료 생성148chart_fname = create_chart(df, company)149table_fname = create_table(df, company)150
151# Step 3: 보고서 작성152ppt_fname = create_pptx(df, company, chart_fname, table_fname)153
154# Step 4: 이메일 전송155smtp_info = {156 "smtp_server": "smtp.naver.com",157 "smtp_user_id": "your_email@naver.com",158 "smtp_user_pw": "your_password",159 "smtp_port": 587160}161
162msg_dict = {163 'application': {164 'maintype': 'application',165 'subtype': 'octet-stream',166 'filename': ppt_fname167 }168}169
170msg = make_multimsg(msg_dict)171msg['from'] = smtp_info["smtp_user_id"]172msg['to'] = "recipient@example.com"173msg['subject'] = "주식 분석 보고서"174
175send_email(smtp_info, msg)176
177print("모든 작업이 완료되었다!")