[Python] Regularly send a day's stock analysis report by email
Article directory
Suppose my current needs are:
Suppose I have realized the time-sharing/daily/weekly/monthly K-line of a certain stock and the drawing of corresponding indicators (RSI, BOLL, OBV, MACD).
If I want the system to send me analysis reports on several self-selected stocks every day after the stock market closes, I can do it in the following way:
1) Let the system calculate and draw the time-sharing/daily/weekly/monthly K-lines and corresponding indicators of several stocks today;
2) The drawn picture is automatically uploaded to Alibaba Cloud OSS, and a link about the uploaded picture is returned
url
;3) According to the given
html
template, useJinja2
the toolkit to dynamically render today's stock drawing resultshtml
into4) The system sends the generated
html
file to the designated recipient in the form of email;
It is divided into 4 modules, and the email sending, html template rendering, image bed building, and timing tasks are introduced in turn. The complete code to realize this requirement is not given here.
1. Python sends email
References:
1) EmailSender
Encapsulation
The code of the packaged mail sending class EmailSender
is as follows: including text content, sending attachments with txt
, pdf
, pictures and .html
-
MIMEMultipart
Attachments are allowed; -
If you want to add one
txt
orhtml
text attachments, useMIMEText
encapsulation; -
If you want to add a
pdf
text attachment, useMIMEApplication
encapsulation, refer to # [Mail Tips] How to use Python to elegantly send emails with pdf attachments ; -
If you want to add a photo attachment, use
MIMEImage
the package;
# 参考 https://blog.csdn.net/weixin_55154866/article/details/128098092
# 参考 https://www.runoob.com/python/python-email.html
# 参考 https://blog.csdn.net/YP_FlowerSky/article/details/124451913
import smtplib
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
import pdfplumber
from pathlib import Path
class EmailSender:
def __init__(self,mail_host,mail_user,mail_pass,sender):
'''
@param mail_host 设置登录及服务器信息,比如网易邮箱是smtp.163.com; type=str
@param mail_user 邮箱用户名; type=str
@param mail_pass 邮箱授权码; type=str
@param sender 邮件发送方邮箱地址; type=str
'''
self.mail_host = mail_host
self.mail_user = mail_user
self.mail_pass = mail_pass
self.sender = sender
'''初始化一封邮件'''
def init_email(self,receivers,content,subject):
'''
@param receivers 接收方邮箱集合,适合群发; type=list
@param content 文本内容;type=str
@param subject 主题; type=str
'''
# 参考 https://blog.csdn.net/YP_FlowerSky/article/details/124451913
self.receivers = receivers
# 添加一个MIMEmultipart类,处理正文及附件
self.message = MIMEMultipart() #MIMEMultipart可以允许带附件
self.message['From'] = self.sender #发送方邮箱地址
self.message['To'] = ','.join(receivers) #接收方邮箱地址, 将['[email protected]','[email protected]']处理成'[email protected],[email protected]'的str
self.message['Subject'] = subject
self.message.attach(MIMEText(content,'plain', 'utf-8')) # 文本内容 (plain文本格式,utf-8编码)
'''为邮件添加附件'''
def email_wrapper(self,filePath,fileType="text"):
'''
@param filePath 文件路径; type=str
@param fileType 文件类型,可选 ['text','html','image']; type=str
'''
if(fileType == 'text'):
suffix = filePath.split(".")[-1]
# 添加一个pdf文本附件, 用MIMEApplication封装 参考 https://blog.csdn.net/popboy29/article/details/126396549
if (suffix == 'pdf'):
with open(filePath, "rb") as f:
pdf_attach = MIMEApplication(f.read(), _subtype="pdf")
#如果出现邮件发送成功,但邮箱接收到的附件变为bin格式的情况时,检查add_header是否出错 参考https://blog.csdn.net/hxchuadian/article/details/125773738
pdf_attach.add_header('Content-Disposition', 'attachment', filename=str(Path(filePath).name))
self.message.attach(pdf_attach)
else:
#添加一个txt文本附件,用MIMEText封装
with open(filePath,'r')as h:
content2 = h.read()
#设置txt参数
text_attach = MIMEText(content2,'plain','utf-8')
#附件设置内容类型,方便起见,设置为二进制流
text_attach['Content-Type'] = 'application/octet-stream'
#设置附件头,添加文件名
text_attach['Content-Disposition'] = f'attachment;filename="{
filePath}"'
self.message.attach(text_attach)
if (fileType == 'html'):
# 推荐使用html格式的正文内容,这样比较灵活,可以附加图片地址,调整格式等
with open(filePath,'r') as f:
# 设置html格式参数
html_attach = MIMEText(f.read(), 'base64', 'gb2312') # 将html文件以附件的形式发送
html_attach['Content-Type'] = 'application/octet-stream'
html_attach.add_header('Content-Disposition', 'attachment',filename=str(Path(filePath).name)) # filename是指下载的附件的命名
self.message.attach(html_attach)
if (fileType == 'image'):
# 添加照片附件,用MIMEImage封装
with open(filePath, 'rb') as fp:
picture_attach = MIMEImage(fp.read())
# 与txt文件设置相似
picture_attach['Content-Type'] = 'application/octet-stream'
picture_attach['Content-Disposition'] = f'attachment;filename="{
filePath}"'
# 将内容附加到邮件主体中
self.message.attach(picture_attach)
#登录并发送
def sendEmail(self):
try:
smtpObj = smtplib.SMTP()
smtpObj.connect(self.mail_host, 25)
smtpObj.login(self.mail_user, self.mail_pass)
smtpObj.sendmail(
self.sender, self.receivers, self.message.as_string()) #receivers群发, receivers是一个列表['[email protected]','[email protected]']
print('success')
smtpObj.quit()
except smtplib.SMTPException as e:
print('error', e)
2) Possible problems
reference
-
If it occurs
list‘ object has no attribute ‘encode‘_list' object has no attribute 'encode
, the main reason isself.message['To']
that the assignment is wrong. In order to realize group sending, it is necessary to['[email protected]','[email protected]']
process'[email protected],[email protected]'
thestr
type -
If the email is sent successfully, but the attachment received by the mailbox becomes
bin
formatted, checkadd_header
whether there is an error. You can refer to the solution to change the picture attachment into bin format when sending emails with Python
2. Jinja2 dynamically renders html pages
Refer to Python's jinja2 template engine to generate HTML
html
The template is as follows:
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<html align='left'>
<body>
<h1>{
{today}}股票分析报告</h1>
<table>
{% for stock in stocks %}
<tr align='center'>
<td>{
{ stock.code }}</td>
<td>{
{ stock.codeName }}</td>
<td><a href="{
{stock.minute_kline_path}}">分时K线图</a> </td>
<td><a href="{
{stock.daily_kline_path}}">日K线图</a> </td>
<td><a href="{
{stock.week_kline_path}}">周K线图</a> </td>
<td><a href="{
{stock.month_kline_path}}">月K线图</a> </td>
</tr>
{% endfor%}
</table>
</body>
</html>
python
code show as below:
import datetime
from jinja2 import Environment, FileSystemLoader
import datetime
import sys
import os
from pathlib import Path
curPath = os.path.abspath(os.path.dirname(__file__))
rootPath = os.path.split(curPath)[0]
sys.path.append(rootPath)
imgDir = os.path.join(rootPath,"html_task/temp/")
def generate_html(today, stocks):
env = Environment(loader=FileSystemLoader('./'))
template = env.get_template('template.html')
with open("result.html", 'w+') as fout:
html_content = template.render(today=today,
stocks=stocks)
fout.write(html_content)
if __name__ == "__main__":
today = datetime.datetime.now().strftime("%Y-%m-%d")
stocks = []
stock1_path = os.path.join(imgDir,"sh601728")
stock2_path = os.path.join(imgDir,"sz000722")
stock1 = {
'code': 'sh601728', 'codeName': '中国电信',
'minute_kline_path': stock1_path + "/" + "minute_K_line.png",
'daily_kline_path': stock1_path + "/" + "daily_K_line.png",
'week_kline_path': stock1_path + "/" + "week_K_line.png",
"month_kline_path" : stock1_path + "/" + "month_K_line.png", }
stock2 = {
'code': 'sz000722', 'codeName': '湖南发展',
'minute_kline_path': stock2_path + "/" + "minute_K_line.png",
'daily_kline_path': stock2_path + "/" + "daily_K_line.png",
'week_kline_path': stock2_path + "/" + "week_K_line.png",
"month_kline_path" : stock2_path + "/" + "month_K_line.png", }
stocks.append(stock1)
stocks.append(stock2)
generate_html(today, stocks) #图片无法正常显示,会报错:Not allowed to load local resource
# 图片无法正常显示 解决方法参考 http://www.kuazhi.com/post/319149.html
The resulting html
visualization looks like this:
But there is a problem - clicking the link can not download or access the picture normally , the main reason is: the browser prohibits the webpage from accessing the local file for security reasons, because the picture is stored in the project directory, so it cannot be accessed through the local url access. (Refer to the browser error: Not allowed to load local resource reasons and solutions_ Sanshui's blog that can't unscrew the bottle cap-CSDN blog )
So here we plan to use the picture link returned by the picture bed url
to solve Not allowed to load local resource
the problem.
3. Alibaba Cloud OSS builds a map bed
reference
1) Python uploads pictures to OSS
Use python
to upload the picture to Alibaba Cloud OSS (very cheap, bought a 1-year 9 RMB resource package), and then url
access the picture through the link. Among them, it is recommended to use the sum of the RAM user , which is the name of the purchased instance and the regional node of this instance. For the specific acquisition method, refer to the usage process of Alibaba Cloud OSSACCESS_KEY_ID
ACCESS_KEY_SECRET
BUCKET_NAME
OSS
ENDPOINT
OSS
# 使用阿里云OSS + picGo搭建图床 参考 https://www.jianshu.com/p/111ce9603ea6l
# -*- coding: utf-8 -*-
import datetime
import oss2
import unittest
# 阿里云OSS使用流程 参考 https://zhuanlan.zhihu.com/p/567771838
ACCESS_KEY_ID = "LTAI5*****Hu6m" #RAM账号access_key_id,如果没有用主账号登录工作台创建并授权,关于RAM角色参考 https://ram.console.aliyun.com/roles
ACCESS_KEY_SECRET = "mkit8YsLh*****TYmoh1QRzK" #RAM账号access_key_secret
ENDPOINT = "oss-cn-shenzhen.aliyuncs.com" #可以在bucket中获取地域节点endpoint 参考 https://zhuanlan.zhihu.com/p/567771838
BUCKET_NAME = "w*****i-20200401"
#参考 https://www.likecs.com/show-308529932.html#sc=900
class Oss:
"""
oss存储类
上传bytes流,返回状态码和url
"""
def __init__(self, access_key_id=ACCESS_KEY_ID, access_key_secret=ACCESS_KEY_SECRET,
endpoint=ENDPOINT, bucket_name=BUCKET_NAME):
# 阿里云主账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建RAM账号。
auth = oss2.Auth(access_key_id, access_key_secret)
# Endpoint以杭州为例,其它Region请按实际情况填写。'http://oss-cn-hangzhou.aliyuncs.com'
self.bucket = oss2.Bucket(auth, endpoint, bucket_name)
def upload_bytes(self, file_bytes, image_name):
"""上传bytes文件"""
result = self.bucket.put_object('{}'.format(image_name), file_bytes)
class OSSTest(unittest.TestCase):
def test_oss_uploadFile(self):
oss_obj = Oss()
with open("temp/sh601728/minute_K_line.png","rb") as f:
oss_obj.upload_bytes(f.read(),"minute_K_line.png")
def test_oss_downloadFile(self):
# 上传后,可以访问的 url 的组成
photo_name = 'minute_K_line.png'
domain = f'https://{
BUCKET_NAME}.{
ENDPOINT}/'
url_photo = domain + photo_name
print(url_photo)
#访问该图片时可能会报错:You have no right to access this object because of bucket acl.
# 解决方法:在bucket的权限控制中,将私有修改为公共读 参考 https://blog.csdn.net/zsy3757486/article/details/126938973
2) PicGo
Upload images to OSS using
[Releases · Molunerfinn/PicGo · GitHub](https://github.com/Molunerfinn/PicGo/releases)
After downloading and installing from the official website PicGo
, configure Alibaba Cloud OSS , , and in the image bed configuration , you can upload images. For details, refer to Using Alibaba Cloud OSS to Build Image Beds - Short BookACCESS_KEY_ID
ACCESS_KEY_SECRET
BUCKET_NAME
存储区域地址
3) Image link access error resolution
Under normal circumstances, uploading to pictures can be accessed through the following links:
https://{
BUCKET_NAME}.{
ENDPOINT}/photo_name #photo_name是上传到图片名称
url
However, an error may be reported when testing the image connection:You have no right to access this object because of bucket acl.
Solution: In bucket
the permission control of , change private to public read . Refer to [Alibaba Cloud OSS] You have no right to access this object because of bucket acl._Luyao Yezi's Blog-CSDN Blog
4. APScheduler scheduled tasks
reference
- Implementation of Python timing tasks
- The difference between BlockingScheduler and BackgroundScheduler
- Detailed Explanation of APScheduler Timing Tasks
APScheduler timing framework: I finally found a way to call me to wake up every day
APScheduler is a Python timing task framework , which is very convenient to use. Provides tasks based on date, fixed time interval and crontab type, and can persist tasks and run applications in daemon mode .
Use APScheduler needs to be installed
pip install apscheduler
Let’s first look at an example of calling me to get up at 6:30 every morning from Monday to Friday ( the index of Monday is 0, and the index of Friday is 4 )
from apscheduler.schedulers.blocking import BlockingScheduler
from datetime import datetime
# 输出时间
def job():
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
# BlockingScheduler
scheduler = BlockingScheduler()
scheduler.add_job(job, 'cron', day_of_week='0-5', hour=6, minute=30)
scheduler.start()
1) Four components of APScheduler
The four components of APScheduler are: trigger, job store, executor, and scheduler .
a, trigger (trigger)
Containing scheduling logic, each job has its own trigger that determines which job will run next. Aside from their own initial configuration, triggers are completely stateless.
APScheduler has three built-in triggers:
date: trigger at a specific time point
interval: trigger at a fixed time interval
cron: trigger periodically at a specific time
b. Job store
Storing scheduled jobs, the default job storage is to simply save the job in memory, other job storage is to save the job in the database. A job's data is serialized when saved in the persistent job store, and deserialized when loaded. Schedulers cannot share the same job store.
APScheduler uses MemoryJobStore by default, you can modify the DB storage scheme
c. Executor
To handle the running of jobs, they usually do this by submitting the specified callable objects in the job to a thread or into the pool. When the job is complete, the executor will notify the scheduler.
There are two most commonly used executors:
ProcessPoolExecutor
ThreadPoolExecutor
d. Scheduler
Usually there is only one scheduler in an application, and the developer of the application usually does not directly deal with job storage, schedulers and triggers, instead, the scheduler provides a suitable interface to deal with these. Configuring job storage and executors can be done in the scheduler, such as adding, modifying and removing jobs.
2) Configure the scheduler scheduler
APScheduler
There are many different ways to configure the scheduler, you can use a configuration dictionary or pass it in as a parameter keyword. You can also create the scheduler first, then configure and add jobs, so you can get more flexibility in different environments.
Let's look at a simple BlockingScheduler
example
from apscheduler.schedulers.blocking import BlockingScheduler
from datetime import datetime
def job():
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
# 定义BlockingScheduler
sched = BlockingScheduler()
sched.add_job(job, 'interval', seconds=5)
sched.start()
The above code creates one BlockingScheduler
and uses the default memory store and default executor. (The default options are MemoryJobStore
and ThreadPoolExecutor
, where the maximum number of threads in the thread pool is 10). After the configuration is complete, use start()
the method to start.
If you want to pass parameters to the job , you can add_job
use args
parameters in , if you want to set the specified id for the job, you can use id
parameters
rom datetime import datetime
from apscheduler.schedulers.blocking import BlockingScheduler
def func(name):
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print(now + f" Hello world, {
name}")
scheduler = BlockingScheduler()
scheduler.add_job(func, 'interval', seconds=3, args=["desire"], id="func")
scheduler.start()
Remove jobs :
remove_job
1) Call the method by the ID of the job- 2) Call the method through
add_job()
the job instance obtained inremove()
- If a job finishes scheduling (i.e. its triggers are no longer fired), it is automatically removed
If job_id
it does not exist, remove_job
an error will be reported , which can be handled with try-except
# remove
job = scheduler.add_job(func, 'interval', seconds=3, args=["desire"], id="job_remove")
job.remove()
# remove_job
scheduler.add_job(func, 'interval', seconds=3, args=["desire"], id="job_remove")
scheduler.remove_job(job_id="job_remove")
Terminate the executors in the scheduler :
scheduler.shutdown() #终止调度器中的任务存储器以及执行器
scheduler.shutdown(wait=False)
By default, the task memory and executor will be terminated, and then all currently executing jobs will be completed (automatically terminated). wait=False
This parameter will terminate directly without waiting for any running tasks to complete . But if the scheduler is not executed, shutdown()
an error will be reported.