合肥网站建设王道下拉強,为什么做网站越早越好,内蒙古工程建设协会网站,wordpress企业站【Python】以邮件的方式定时发送一天的股票分析报告 文章目录【Python】以邮件的方式定时发送一天的股票分析报告1、Python发送邮件1#xff09;EmailSender封装2#xff09;可能存在的问题2、jinja2动态渲染html页面3、阿里云OSS搭建图床1#xff09;Python上传图片到OSS中…【Python】以邮件的方式定时发送一天的股票分析报告 文章目录【Python】以邮件的方式定时发送一天的股票分析报告1、Python发送邮件1EmailSender封装2可能存在的问题2、jinja2动态渲染html页面3、阿里云OSS搭建图床1Python上传图片到OSS中2使用PicGo上传图片到OSS中3图片链接访问报错解决4、APScheduler定时任务1APScheduler四个组件2配置scheduler调度器假设我现在的需求是 假设我已经实现了对某只股票的分时/日/周/月K线以及对应指标RSIBOLLOBVMACD的绘制。 如果我想让系统每天在股市结束后给我发送关于几只自选股票的分析报告我可以通过如下方式实现 1让系统对今天几只股票的分时/日/周/月K线以及对应指标进行计算和绘制 2绘制的图片自动上传到阿里云OSS中并返回关于已上传图片的url链接 3根据给定的html模板利用Jinja2工具包将今天的股票绘制结果动态渲染到html中 4系统将生成的html文件以邮件的形式发送到指定的收件人中 这里分成4个模块依次对邮件发送、html模板渲染、图床搭建、定时任务进行介绍这里并没有给出实现这个需求的完整代码。
1、Python发送邮件
参考资料 简单三步用 Python 发邮件_程序员王饱饱的博客-CSDN博客_python 发送邮件 Python SMTP发送邮件 | 菜鸟教程
1EmailSender封装
封装好的邮件发送类EmailSender代码如下包括文本内容、带txt、pdf、图片和html的附件发送。 MIMEMultipart可以允许带附件 如果想添加一个txt或者html文本附件用MIMEText封装 如果想添加一个pdf文本附件, 用MIMEApplication封装参考# 【Mail小技巧】如何使用Python优雅的发送带有pdf附件的电子邮件 如果想添加一个照片附件用MIMEImage封装
# 参考 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 Pathclass EmailSender:def __init__(self,mail_host,mail_user,mail_pass,sender):param mail_host 设置登录及服务器信息,比如网易邮箱是smtp.163.com; typestrparam mail_user 邮箱用户名; typestrparam mail_pass 邮箱授权码; typestrparam sender 邮件发送方邮箱地址; typestrself.mail_host mail_hostself.mail_user mail_userself.mail_pass mail_passself.sender sender初始化一封邮件def init_email(self,receivers,content,subject):param receivers 接收方邮箱集合适合群发; typelistparam content 文本内容typestrparam subject 主题; typestr# 参考 https://blog.csdn.net/YP_FlowerSky/article/details/124451913self.receivers receivers# 添加一个MIMEmultipart类处理正文及附件self.message MIMEMultipart() #MIMEMultipart可以允许带附件self.message[From] self.sender #发送方邮箱地址self.message[To] ,.join(receivers) #接收方邮箱地址, 将[1046474088qq.com,2802428220qq.com]处理成1046474088qq.com,2802428220qq.com的strself.message[Subject] subjectself.message.attach(MIMEText(content,plain, utf-8)) # 文本内容 (plain文本格式utf-8编码)为邮件添加附件def email_wrapper(self,filePath,fileTypetext):param filePath 文件路径; typestrparam fileType 文件类型可选 [text,html,image]; typestrif(fileType text):suffix filePath.split(.)[-1]# 添加一个pdf文本附件, 用MIMEApplication封装 参考 https://blog.csdn.net/popboy29/article/details/126396549if (suffix pdf):with open(filePath, rb) as f:pdf_attach MIMEApplication(f.read(), _subtypepdf)#如果出现邮件发送成功但邮箱接收到的附件变为bin格式的情况时检查add_header是否出错 参考https://blog.csdn.net/hxchuadian/article/details/125773738pdf_attach.add_header(Content-Disposition, attachment, filenamestr(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] fattachment;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-streamhtml_attach.add_header(Content-Disposition, attachment,filenamestr(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-streampicture_attach[Content-Disposition] fattachment;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是一个列表[1046474088qq.com,2802428220qq.com]print(success)smtpObj.quit()except smtplib.SMTPException as e:print(error, e)2可能存在的问题
参考 如果出现list‘ object has no attribute ‘encode‘_list object has no attribute encode主要原因是self.message[To]赋值有误要想实现群发需要将[1046474088qq.com,2802428220qq.com]处理成1046474088qq.com,2802428220qq.com的str类型 参考Selenium /Python 配置QQ邮箱后台自动发送邮件unittest//发送多人邮件报错: ‘list‘ object has no attribute ‘encode‘_list’ object has no attribute encode 如果出现邮件发送成功但邮箱接收到的附件变为bin格式的情况时检查add_header是否出错。可参考Python 发送邮件时图片附件变为bin格式的解决方案
2、jinja2动态渲染html页面
参考Python之jinja2模板引擎生成HTML_宗而研之的博客-CSDN博客_python 生成html
html模板如下
meta http-equivContent-Type contenttext/html;charsetutf-8
html alignleftbodyh1{{today}}股票分析报告/h1table{% for stock in stocks %}tr aligncentertd{{ stock.code }}/tdtd{{ stock.codeName }}/tdtda href{{stock.minute_kline_path}}分时K线图/a /tdtda href{{stock.daily_kline_path}}日K线图/a /tdtda href{{stock.week_kline_path}}周K线图/a /tdtda href{{stock.month_kline_path}}月K线图/a /td/tr{% endfor%}/table/body
/htmlpython代码如下
import datetimefrom jinja2 import Environment, FileSystemLoader
import datetimeimport 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(loaderFileSystemLoader(./))template env.get_template(template.html)with open(result.html, w) as fout:html_content template.render(todaytoday,stocksstocks)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生成的html可视化效果如下 但是存在一个问题 - 点击链接并不能正常下载或访问图片主要原因是浏览器出于安全方面的考虑禁止网页访问本地文件因为图片是存在项目目录下的所以无法通过本地的url进行访问。参考浏览器报错Not allowed to load local resource 原因及解决办法_扭不开瓶盖的三水的博客-CSDN博客
因此这里打算用图床返回的图片url链接来解决Not allowed to load local resource问题。
3、阿里云OSS搭建图床
参考 阿里云 oss 服务 —— 上传图片获取url 阿里云OSS使用流程 使用阿里云OSS搭建图床 - 简书
1Python上传图片到OSS中
使用python将图片上传到阿里云OSS挺便宜的买了1年9 rmb的资源包中然后通过url链接访问图片。其中建议使用RAM用户的ACCESS_KEY_ID和ACCESS_KEY_SECRETBUCKET_NAME是购买的OSS实例名称ENDPOINT是这个OSS实例的地域节点具体获取方式参考阿里云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#sc900
class Oss:oss存储类上传bytes流返回状态码和urldef __init__(self, access_key_idACCESS_KEY_ID, access_key_secretACCESS_KEY_SECRET,endpointENDPOINT, bucket_nameBUCKET_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.comself.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.pngdomain fhttps://{BUCKET_NAME}.{ENDPOINT}/url_photo domain photo_nameprint(url_photo)#访问该图片时可能会报错You have no right to access this object because of bucket acl.# 解决方法在bucket的权限控制中将私有修改为公共读 参考 https://blog.csdn.net/zsy3757486/article/details/1269389732使用PicGo上传图片到OSS中
在官网[Releases · Molunerfinn/PicGo · GitHub](https://github.com/Molunerfinn/PicGo/releases)下载安装好PicGo之后在图床配置中配置好阿里云OSS的ACCESS_KEY_ID、ACCESS_KEY_SECRET、BUCKET_NAME和存储区域地址后即可实现图片上传具体参考使用阿里云OSS搭建图床 - 简书
3图片链接访问报错解决
正常情况下上传到图片可以通过如下链接访问
https://{BUCKET_NAME}.{ENDPOINT}/photo_name #photo_name是上传到图片名称但是在测试图片url连接时可能会报错You have no right to access this object because of bucket acl.
解决方法在bucket的权限控制中将私有修改为公共读。参考【阿里云OSS】You have no right to access this object because of bucket acl._路遥叶子的博客-CSDN博客
4、APScheduler定时任务
参考
Python 定时任务的实现方式BlockingScheduler与BackgroundScheduler区别APScheduler 定时任务详解
APScheduler定时框架终于找到了可以每天定时喊我起床的方式了
APScheduler是一个 Python 定时任务框架使用起来十分方便。提供了基于日期、固定时间间隔以及 crontab 类型的任务并且可以持久化任务、并以 daemon 方式运行应用。
使用 APScheduler 需要安装
pip install apscheduler首先来看一个周一到周五每天早上6点半喊我起床的例子
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_week1-5, hour6, minute30)
scheduler.start()1APScheduler四个组件
APScheduler 四个组件分别为触发器(trigger)作业存储(job store)执行器(executor)调度器(scheduler)。
a、触发器(trigger)
包含调度逻辑每一个作业有它自己的触发器用于决定接下来哪一个作业会运行。除了他们自己初始配置意外触发器完全是无状态的 APScheduler 有三种内建的 trigger: date: 特定的时间点触发 interval: 固定时间间隔触发 cron: 在特定时间周期性地触发 b、作业存储(job store)
存储被调度的作业默认的作业存储是简单地把作业保存在内存中其他的作业存储是将作业保存在数据库中。一个作业的数据讲在保存在持久化作业存储时被序列化并在加载时被反序列化。调度器不能分享同一个作业存储。 APScheduler 默认使用 MemoryJobStore可以修改使用 DB 存储方案
c、执行器(executor)
处理作业的运行他们通常通过在作业中提交制定的可调用对象到一个线程或者进城池来进行。当作业完成时执行器将会通知调度器。 最常用的 executor 有两种 ProcessPoolExecutor ThreadPoolExecutor d、调度器(scheduler)
通常在应用中只有一个调度器应用的开发者通常不会直接处理作业存储、调度器和触发器相反调度器提供了处理这些的合适的接口。配置作业存储和执行器可以在调度器中完成例如添加、修改和移除作业。
2配置scheduler调度器
APScheduler提供了许多不同的方式来配置调度器你可以使用一个配置字典或者作为参数关键字的方式传入。你也可以先创建调度器再配置和添加作业这样你可以在不同的环境中得到更大的灵活性。
下面来看一个简单的 BlockingScheduler 例子
from apscheduler.schedulers.blocking import BlockingScheduler
from datetime import datetimedef job():print(datetime.now().strftime(%Y-%m-%d %H:%M:%S))
# 定义BlockingScheduler
sched BlockingScheduler()
sched.add_job(job, interval, seconds5)
sched.start()上述代码创建了一个 BlockingScheduler并使用默认内存存储和默认执行器。(默认选项分别是 MemoryJobStore 和 ThreadPoolExecutor其中线程池的最大线程数为10)。配置完成后使用 start() 方法来启动。
如果要给job传参可以在add_job中使用args参数如果要给job设置指定id可以使用id参数
rom datetime import datetimefrom apscheduler.schedulers.blocking import BlockingSchedulerdef 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, seconds3, args[desire], idfunc)
scheduler.start()移除job
1通过job的ID来调用remove_job方法2通过在add_job()中得到的job实例调用remove()方法如果一个job完成了调度例如他的触发器不会再被触发, 它会自动被移除
如果job_id不存在remove_job会报错可以用try - except来处理
# remove
job scheduler.add_job(func, interval, seconds3, args[desire], idjob_remove)
job.remove()# remove_job
scheduler.add_job(func, interval, seconds3, args[desire], idjob_remove)
scheduler.remove_job(job_idjob_remove)终止调度器中的执行器
scheduler.shutdown() #终止调度器中的任务存储器以及执行器
scheduler.shutdown(waitFalse)默认情况会终止任务存储器以及执行器然后等待所有目前执行的job完成后自动终止waitFalse 此参数不会等待任何运行中的任务完成直接终止。但是如果scheduler没有执行shutdown()会报错。