【Python】天气预报(发送网易邮件,微信公众测试号,企业微信),周末用时一天,两万字代码,纯肝货(完整项目)一一CSDN21天学习挑战赛
目录
本系列文章为参与【Python】CSDN21天学习挑战赛,成为更好的自己,根据自身的学习进度并记录自己的学习过程。我也是Python纯小白,和大家一起学习,保持热爱学习的好习惯😁
活动地址:CSDN21天学习挑战赛
前言
不清楚怎么发送消息的,可以参考以下三篇文章:
关于定时任务的文章:
一、新建项目WeatherForecast
1.项目结构
2.新建config.ini配置文件
其中留空的需要填写自己的信息
#######################################################
# 项目应用配置
# web_url:网页地址(用于获取七天天气)
# api_url:接口地址(用于获取当前天气)
#######################################################
[App]
web_url = https://weather.cma.cn/web/weather/53463.html
api_url = https://weather.cma.cn/api/now/53463
#######################################################
# 发送消息平台配置
# wechat_official_account:微信公众号平台
# enterprise_wechat:企业微信
#######################################################
[Platform]
wechat_official_account = WeChatOfficialAccount
enterprise_wechat = EnterpriseWeChat
#######################################################
# 网易邮箱配置
# host:服务器地址
# port:端口
# password:授权码(不是邮箱密码)
# from_addr:登录用户(邮件发送者)
# subtype_plain:文本格式(plain)
# subtype_html:HTML格式(html)
# attachment:附件
# embedded:内嵌
# subtype:邮件格式:plain:文本;html:超文本标记语言(默认plain)
# charset:编码(默认utf-8)
#######################################################
host = smtp.163.com
port = 25
password =
from_addr =
subtype_plain = plain
subtype_html = html
attachment = attachment
embedded = embedded
subtype = plain
charset = utf-8
#######################################################
# 微信公众测试号配置
# app_id:微信公众测试号账号
# app_secret:微信公众测试号密钥
# touser:消息接收者
# template_id:消息模板id
# click_url:点击消息跳转链接(可无)
#######################################################
[WeChatOfficialAccount]
app_id =
app_secret =
touser =
template_id =
click_url = https://blog.csdn.net/sxdgy_?spm=1011.2415.3001.5349
#######################################################
# 企业微信配置
# corp_id:企业ID
# corp_secret:企业密钥
# agent_id:应用ID
#######################################################
[EnterpriseWeChat]
corp_id =
corp_secret =
agent_id =
3.新建config.xml配置文件
配置任务只是配置了执行任务的时间策略
<?xml version="1.0" encoding="utf-8"?>
<Root>
<Node>
<Name>微信公众号消息</Name>
<Description>2022年9月7号执行</Description>
<Triggers>data</Triggers>
<Year>2022</Year>
<Month>9</Month>
<Day>7</Day>
</Node>
<Node>
<Name>网易邮件消息</Name>
<Description>每60秒执行一次</Description>
<Triggers>interval</Triggers>
<Second>60</Second>
</Node>
<Node>
<Name>企业微信消息</Name>
<Description>11分钟7秒时执行</Description>
<Triggers>cron</Triggers>
<Minute>11</Minute>
<Second>7</Second>
</Node>
</Root>
4.新建config_helper.py文件
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
@Time :2022/8/21 7:35
@Auth :小呆瓜
@File :config_helper.py
@IDE :PyCharm
@Description:配置文件辅助
"""
# 导入ini配置文件操作模块
import configparser
# 导入xml操作模块
import xml.etree.ElementTree as ET
class XmlConfigHelper(object):
def __init__(self):
# xml配置路径;注:如果同一目录,可以这样填写,不同目录就用相对路劲或者绝对路径
self.tree = ET.parse('config.xml')
def get_root(self):
"""
获取xml根节点
:return: xml根节点
"""
return self.tree.getroot()
def get_int(self, data) -> int:
"""
获取int整数
:param data: 转换数
:return: 如果字符串或者None或者其他不能转int的,则返回0
"""
try:
if data is None:
return 0
return int(data)
except ValueError:
return 0
class IniConfigHelper(object):
def __init__(self) -> None:
# 实例ini配置解析
self.config = configparser.ConfigParser()
# ini配置路径;注:如果同一目录,可以这样填写,不同目录就用相对路劲或者绝对路径
self.config.read("config.ini", encoding="utf-8")
def get_config(self, section: str, option: str = None):
"""
获取config.ini配置
:param section: 节点
:param option: 项(key)
:return: key对应的值
"""
section_dic = dict(self.config.items(section))
if option is not None:
return section_dic[option]
else:
return section_dic
def get_app_config(self, option: str = None):
"""
项目应用配置
:param option: key
:return: key对应的值
"""
return self.get_config('App', option)
def get_platform_config(self, option: str = None):
"""
获取发送消息平台配置
:param option: key
:return: key对应的值
"""
return self.get_config('Platform', option)
def get_email163_config(self, option: str = None):
"""
获取网易邮箱配置
:param option: key
:return: key对应的值
"""
return self.get_config('Email163', option)
def get_wechat_official_account_config(self, option: str = None):
"""
获取微信公众测试号配置
:param option: key
:return: key对应的值
"""
return self.get_config('WeChatOfficialAccount', option)
def get_enterprise_wechat_config(self, option: str = None):
"""
获取企业微信配置
:param option: key
:return: key对应的值
"""
return self.get_config('EnterpriseWeChat', option)
5.新建app_const.py文件
该文件是应用程序所有常量字段(从配置获取)
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
@Time :2022/8/21 7:42
@Auth :小呆瓜
@File :app_const.py
@IDE :PyCharm
@Description:应用程序常量(配置获取)
"""
# 导入ini配置文件辅助模块
from config_helper import IniConfigHelper
class AppConst(object):
# 实例配置辅助
ini_config = IniConfigHelper()
#######################################################
# 项目应用配置
#######################################################
# 网页地址(用于获取七天天气)
WEB_URL = ini_config.get_app_config('web_url')
# 接口地址(用于获取当前天气)
API_URL = ini_config.get_app_config('api_url')
#######################################################
# 发送消息平台配置
#######################################################
# 微信公众号平台
WECHAT_OFFICIAL_ACCOUNT = ini_config.get_platform_config('wechat_official_account')
# 企业微信平台
ENTERPRISE_WECHAT = ini_config.get_platform_config('enterprise_wechat')
#######################################################
# 网易邮箱配置
#######################################################
# 服务器地址
HOST = ini_config.get_email163_config('host')
# 端口
PORT = ini_config.get_email163_config('port')
# 授权码(不是邮箱密码)
PASSWORD = ini_config.get_email163_config('password')
# 登录用户(邮件发送者)
FROM_ADDR = ini_config.get_email163_config('from_addr')
# 文本格式
SUBTYPE_PLAIN = ini_config.get_email163_config('subtype_plain')
# HTML格式
SUBTYPE_HTML = ini_config.get_email163_config('subtype_html')
# 附件
ATTACHMENT = ini_config.get_email163_config('attachment')
# 内嵌
EMBEDDED = ini_config.get_email163_config('embedded')
# 邮件格式
SUBTYPE = ini_config.get_email163_config('subtype')
# 编码
CHARSET = ini_config.get_email163_config('charset')
#######################################################
# 微信公众测试号配置
#######################################################
# 微信公众测试号账号
APP_ID = ini_config.get_wechat_official_account_config('app_id')
# 微信公众测试号密钥
APP_SECRET = ini_config.get_wechat_official_account_config('app_secret')
# 消息接收者
TOUSER = ini_config.get_wechat_official_account_config('touser')
# 消息模板id
TEMPLATE_ID = ini_config.get_wechat_official_account_config('template_id')
# 点击消息跳转链接,可无
CLICK_URL = ini_config.get_wechat_official_account_config('click_url')
#######################################################
# 企业微信配置
#######################################################
# 企业ID
CORP_ID = ini_config.get_enterprise_wechat_config('corp_id')
# 企业密钥
CORP_SECRET = ini_config.get_enterprise_wechat_config('corp_secret')
# 应用ID(企业微信)
AGENT_ID = ini_config.get_enterprise_wechat_config('agent_id')
6.新建cma.py文件
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
@Time :2022/8/21 7:35
@Auth :小呆瓜
@File :cma.py
@IDE :PyCharm
@Description:获取天气
"""
# 导入正则匹配模块
import re
# 导入网络请求模块
import requests
# 导入应用程序常量
from app_const import AppConst
class CmaWeather(object):
def __init__(self, web_url=AppConst.WEB_URL, api_url=AppConst.API_URL):
"""
构造函数
:param web_url: 网页地址
:param api_url: 接口地址
"""
self.web_url = web_url
self.api_url = api_url
return
def get_html(self, url, headers=None):
"""
获取网页html
:param url: 网页地址
:param headers: 请求头
:return: 网页内容
"""
# 请求地址
resp = requests.get(url, headers=headers)
# 设置响应结果编码为utf-8
resp.encoding = "utf-8"
# 返回结果的文本
return resp.text
def get_api_result(self, headers=None):
"""
获取接口响应数据
:param api_url: 接口地址
:param headers: 请求头
:return: 接口响应json数据
"""
resp = requests.get(self.api_url, headers=headers)
json_data = resp.json()
data = json_data["data"]
return data
def get_re_compile(self) -> str:
"""
获取匹配规则
:return: 匹配规则
"""
re_compile = re.compile(r'.*?<div class="day-item">(?P<day>.*?)</div>'
r'.*?<div class="day-item">(?P<wea>.*?)</div>'
r'.*?<div class="day-item">(?P<win>.*?)</div>'
r'.*?<div class="day-item">(?P<wins>.*?)</div>'
r'.*?<div class="high">(?P<high>.*?)</div>'
r'.*?<div class="low">(?P<low>.*?)</div>'
r'.*?<div class="day-item">(?P<night_wea>.*?)</div>'
r'.*?<div class="day-item">(?P<night_win>.*?)</div>'
r'.*?<div class="day-item">(?P<night_wins>.*?)</div>', re.S)
return re_compile
def get_html_info(self, re_compile, html):
"""
根据re的匹配规则,从html中匹配内容,并返回字典
:param re_compile: 匹配规则
:param html:html内容
:return:字典
"""
result = re_compile.finditer(html)
dic = []
# 循环匹配的结果
for item in result:
# 每个匹配到的信息转成字典
item_dic = item.groupdict()
for dic_key in item_dic:
# 移除空格换行
item_dic[dic_key] = item_dic[dic_key].replace("<br>", "").replace("\n", "").replace("\t", "").replace(
" ", "").replace(" ", "").strip()
dic.append(item_dic)
return dic
def get_current_weather_content(self, data_type: str) -> str:
"""
获取并构造当前天气信息
:param data_type: 消息类型:text,markdown,html
:return: 当前天气信息
"""
json_data = self.get_api_result()
if data_type == 'json':
return json_data
location = json_data["location"]
now = json_data["now"]
# 城市
city = location["path"]
# 降水量
precipitation = now["precipitation"]
# 温度
temperature = now["temperature"]
# 气压
pressure = now["pressure"]
# 湿度
humidity = now["humidity"]
# 风向
wind_direction = now["windDirection"]
# 风向程度
wind_direction_degree = now["windDirectionDegree"]
# 风速
wind_peed = now["windSpeed"]
# 风力等级
wind_scale = now["windScale"]
# 构造消息
if data_type == 'text':
# 两种写法都可以
# return f"城市:{city}" \
# f"\n降水量:{precipitation}" \
# f"\n温度:{temperature}" \
# f"\n气压:{pressure}" \
# f"\n湿度:{humidity}" \
# f"\n风向:{wind_direction}" \
# f"\n风向程度:{wind_direction_degree}" \
# f"\n风速:{wind_peed}" \
# f"\n风力等级:{wind_scale}"
return f"""城市:{city}
降水量:{precipitation}
温度:{temperature}
气压:{pressure}
湿度:{humidity}
风向:{wind_direction}
风向程度:{wind_direction_degree}
风速:{wind_peed}
风力等级:{wind_scale}
"""
elif data_type == 'markdown':
# 两种写法都可以
# return f"> 城市:**`{city}`**" \
# f"\n> 降水量:<font color='warning'>{precipitation}</font>" \
# f"\n> 温度:<font color='info'>{temperature}</font>" \
# f"\n> 气压:{pressure}" \
# f"\n> 湿度:{humidity}" \
# f"\n> 风向:{wind_direction}" \
# f"\n> 风向程度:{wind_direction_degree}" \
# f"\n> 风速:{wind_peed}" \
# f"\n> 风力等级:{wind_scale}"
return f"""> 城市:**`{city}`**
> 降水量:<font color='warning'>{precipitation}</font>
> 温度:<font color='info'>{temperature}</font>
> 气压:{pressure}
> 湿度:{humidity}
> 风向:{wind_direction}
> 风向程度:{wind_direction_degree}
> 风速:{wind_peed}
> 风力等级:{wind_scale}
"""
elif data_type == 'html':
return f"""<label>城市:<font style="color:#E93C3C;background: #EFEFEF;">{city}</font></label>
<label>降水量:<font color="orange">{precipitation}</font></label>
<label>温度:<font color="green">{temperature}</font></label>
<label>气压:{pressure}</label>
<label>湿度:{humidity}</label>
<label>风向:{wind_direction}</label>
<label>风向程度:{wind_direction_degree}</label>
<label>风速:{wind_peed}</label>
<label>风力等级:{wind_scale}</label>
"""
else:
return print('消息类型错误')
def main(self) -> None:
"""
主函数
:return: None
"""
print('*****************************************************')
# 获取网页内容
html = self.get_html(self.web_url)
# 获取匹配规则
re_compile = self.get_re_compile()
# 获取天气信息
html_info = self.get_html_info(re_compile, html)
# 近七天天气信息
for item in html_info:
weather = item['day'], item['wea'], item['win'], item['wins'], item['high'], item['low'], \
item['night_wea'], item['night_win'], item['night_wins']
print(weather)
print('*****************************************************')
if __name__ == '__main__':
cma = CmaWeather()
cma.main()
7.新建email_163.py文件
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
@Time :2022/8/21 7:35
@Auth :小呆瓜
@File :email_163.py
@IDE :PyCharm
@Description:发送邮件
"""
# 导入发送邮件模块
import smtplib
# 导入邮件主题构造模块
from email.header import Header
# 导入邮件文字构造模块
from email.mime.text import MIMEText
# 导入邮件混合构造模块
from email.mime.multipart import MIMEMultipart
# 导入邮件图片处理模块
from email.mime.image import MIMEImage
# 导入应用程序常量
from app_const import AppConst
# 发送网易邮件类
class Email163(object):
def __init__(self, host=AppConst.HOST, port=AppConst.PORT, password=AppConst.PASSWORD, from_addr=AppConst.FROM_ADDR,
subtype_plain=AppConst.SUBTYPE_PLAIN, subtype_html=AppConst.SUBTYPE_HTML,
attachment=AppConst.ATTACHMENT, embedded=AppConst.EMBEDDED, subtype=AppConst.SUBTYPE,
charset=AppConst.CHARSET) -> None:
"""
构造函数
:param host: 服务器地址
:param port: 端口
:param password: 授权码(不是邮箱密码)
:param from_addr: 登录用户(邮件发送者)
:param subtype_plain: 文本格式
:param subtype_html: HTML格式
:param attachment: 附件
:param embedded: 内嵌
:param subtype: 邮件格式:plain:文本,html:超文本标记语言,n(默认plain)
:param charset: 编码,默认utf-8
"""
self.host = host
self.port = port
self.password = password
self.from_addr = from_addr
self.subtype_plain = subtype_plain
self.subtype_html = subtype_html
self.attachment = attachment
self.embedded = embedded
self.subtype = subtype
self.charset = charset
def get_smtp(self) -> smtplib.SMTP:
"""
实例SMTP并验证
:return: smtplib.SMTP
"""
try:
# 实例SMTP
smtp = smtplib.SMTP()
# 连接邮箱服务器
smtp.connect(self.host, self.port)
# 验证授权码
smtp.login(self.from_addr, self.password)
return smtp
except smtplib.SMTPException:
raise f'验证邮箱失败:用户:{self.from_addr},授权码:{self.password}'
def get_message(self, subject: str, text: str, subtype: str = None):
"""
获取邮件信息
:param subject: 主题
:param text: 内容
:param charset: 编码
:return: 消息对象
"""
# 构造邮件内容
# 第一个参数_text:内容
# 第二个参数_subtype:格式
# 第三个参数_charset:编码
if subtype == "attachment":
# 实例混合邮件(附件)
message = MIMEMultipart()
elif subtype == "embedded":
# 实例内嵌的邮件(文本,HTML,图片)
message = MIMEMultipart('related')
else:
# 实例文字邮件
message = MIMEText(text, self.subtype, self.charset)
# 构造邮件主题信息
# 主题
message['Subject'] = Header(subject, self.charset)
# 发送者
message['From'] = Header(self.from_addr, self.charset)
# 接收者,接收者为多人[]时,需要用,拼接为字符串
# message['To'] = Header(','.join(to_addrs), self.charset)
# 返回消息实例
return message
def attach_attachment(self, message, text: str, subtype: str, attachment_list: list[dict[str, str]]) -> None:
"""
附加上传附件(可多文件)
:param message: 邮件消息
:param subtype: 邮件类型
:param attachment_list: 附件列表:格式[{"path":"附件路径1","name":"显示名称"},{"path":"附件路径2","name":"显示名称"}]
:return: None
"""
# 附加正文
if subtype in (self.attachment, self.embedded):
message.attach(MIMEText(text, self.subtype, self.charset))
if subtype == self.attachment: # 附件
for item in attachment_list:
with open(item["path"], 'rb') as f:
file_data = f.read()
# 上传文件
attachment = MIMEText(file_data, 'base64', 'utf-8')
# 指定消息内容为字节流
attachment["Content-Type"] = 'application/octet-stream'
# 消息描述,这里的filename可以随便填写,这里填写的就会在邮件中附件名称显示
attachment["Content-Disposition"] = f'attachment; filename="{item["name"]}"'
# 附加文件
message.attach(attachment)
elif subtype == self.embedded: # 内嵌
for item in attachment_list:
with open(item["path"], 'rb') as f:
img_data = f.read()
# 创建图片
img = MIMEImage(img_data)
# 定义图片ID,在HTML文本中引用
img.add_header('Content-ID', item["name"])
# 附加图片
message.attach(img)
def send_email(self, subject: str, text: str, to_addrs: list[str],
attachment_list: list[dict[str, str]] = None) -> None:
"""
发送网易邮件
:param subject: 主题
:param text: 内容
:param to_addrs: 接收者
:param attachment_list: 附件(默认空,格式[{"path":"文件路径","name":"显示文件名"}])
:return: None
"""
try:
# 获取SMTP实例
smtp = self.get_smtp()
# 获取邮件消息
message = self.get_message(subject, text, self.subtype)
# 处理附件和内嵌
self.attach_attachment(message, text, self.subtype, attachment_list)
# 发送邮件
smtp.sendmail(self.from_addr, ','.join(to_addrs), message.as_string())
# 发送完邮件,关闭服务
smtp.close()
print(f'邮件成功发送给:{to_addrs}')
except smtplib.SMTPException:
raise f'给{to_addrs}发送邮件失败'
8.新建access_token.py文件
因为企业微信和微信公众号获取access_token方式类似,我把它们写在一个文件中,根据实例时的构造函数作为区分
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
@Time :2022/8/21 7:35
@Auth :小呆瓜
@File :access_token.py
@IDE :PyCharm
@Description:获取access_token
"""
# 导入网络请求模块
import requests
# 导入应用程序常量
from app_const import AppConst
class AccessToken(object):
def __init__(self, platform, app_id=AppConst.APP_ID, app_secret=AppConst.APP_SECRET, corp_id=AppConst.CORP_ID,
corp_secret=AppConst.CORP_SECRET) -> None:
"""
构造函数
:param platform: 平台
:param app_id: 微信公众测试号账号
:param app_secret: 微信公众测试号密钥
:param corp_id: 企业ID
:param corp_secret: 企业密钥
"""
self.platform = platform
self.app_id = app_id
self.app_secret = app_secret
self.corp_id = corp_id
self.corp_secret = corp_secret
def get_access_token(self) -> str:
"""
获取access_token凭证
:return: access_token
"""
if self.platform == AppConst.WECHAT_OFFICIAL_ACCOUNT:
url = f"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={self.app_id}&secret={self.app_secret}"
elif self.platform == AppConst.ENTERPRISE_WECHAT:
url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={self.corp_id}&corpsecret={self.corp_secret}"
else:
return print('暂不支持该平台对接')
resp = requests.get(url)
result = resp.json()
if 'access_token' in result:
return result["access_token"]
else:
print(result)
9.新建send_message.py文件
和access_token.py文件一样,我将两者的发送消息写在同个文件
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
@Time :2022/8/21 7:35
@Auth :小呆瓜
@File :send_message.py
@IDE :PyCharm
@Description:发送消息
"""
# 导入json处理模块
import json
# 导入网络请求模块
import requests
# 导入获取access_token模块
from access_token import AccessToken
# 导入应用程序常量
from app_const import AppConst
class SendMessage(object):
# # 实例配置辅助
# ini_config = IniConfigHelper()
# # 微信公众号平台
# WECHAT_OFFICIAL_ACCOUNT = ini_config.get_platform_config('wechat_official_account')
# # 企业微信平台
# ENTERPRISE_WECHAT = ini_config.get_platform_config('enterprise_wechat')
# # 消息接收者
# TOUSER = ini_config.get_wechat_official_account_config('touser')
# # 消息模板id
# TEMPLATE_ID = ini_config.get_wechat_official_account_config('template_id')
# # 点击消息跳转链接,可无
# CLICK_URL = ini_config.get_wechat_official_account_config('click_url')
#
# # 应用ID(企业微信)
# AGENT_ID = ini_config.get_enterprise_wechat_config('agent_id')
def __init__(self, platform, touser=AppConst.TOUSER, template_id=AppConst.TEMPLATE_ID, click_url=AppConst.CLICK_URL,
agent_id=AppConst.AGENT_ID) -> None:
"""
构造函数
:param platform: 平台,可用WeatherForecast.main.Main中类常量WECHATOFFICIALACCOUNT,ENTERPRISEWECHAT
:param touser: 接收者
:param template_id: 模板id
:param click_url: 点击跳转链接
:param agent_id: 应用ID
"""
self.platform = platform
self.touser = touser
self.template_id = template_id
self.click_url = click_url
self.agent_id = agent_id
if self.platform == AppConst.WECHAT_OFFICIAL_ACCOUNT:
self.access_token = AccessToken(AppConst.WECHAT_OFFICIAL_ACCOUNT).get_access_token()
elif self.platform == AppConst.ENTERPRISE_WECHAT:
self.access_token = AccessToken(AppConst.ENTERPRISE_WECHAT).get_access_token()
def get_send_data(self, data, msgtype) -> object:
"""
获取发送消息data
:param data: 消息内容
:param msgtype: 消息类型:
微信公众号:使用模板消息,不需要指定类型
企业微信:文本消息(text),文本卡片消息(textcard),图文消息(news),markdown消息(markdown)
:return:
"""
if self.platform == AppConst.WECHAT_OFFICIAL_ACCOUNT:
# 微信公众测试号这里是使用模板消息发送
location = data["location"]
now = data["now"]
return {
"touser": self.touser,
"template_id": self.template_id,
"url": self.click_url,
"topcolor": "#FF0000",
# json数据对应模板
"data": {
"city": {
"value": location["path"],
# 字体颜色
"color": "#173177"
},
"precipitation": {
"value": str(now["precipitation"]),
"color": "#FEAD39"
},
"temperature": {
"value": str(now["temperature"]),
"color": "#20995F"
},
"pressure": {
"value": str(now["pressure"]),
},
"humidity": {
"value": str(now["humidity"]),
},
"wind_direction": {
"value": str(now["windDirection"]),
},
"wind_direction_degree": {
"value": str(now["windDirectionDegree"]),
},
"wind_speed": {
"value": str(now["windSpeed"]),
},
"wind_scale": {
"value": str(now["windScale"]),
},
}
}
elif self.platform == AppConst.ENTERPRISE_WECHAT:
# touser:@all向该企业应用的全部成员发送;指定接收消息的成员,成员ID列表(多个接收者用‘|’分隔,最多支持1000个)
if msgtype in ('text', 'markdown'):
return {
"touser": "@all",
"msgtype": msgtype,
"agentid": self.agent_id,
# 这里这样写是因为,消息类型是什么,内容的key就是什么
f"{msgtype}": {
"content": data
}
}
elif msgtype == 'textcard':
return {
"touser": "@all",
"msgtype": msgtype,
"agentid": self.agent_id,
f"{msgtype}": data
}
elif msgtype == 'news':
return {
"touser": "@all",
"msgtype": msgtype,
"agentid": self.agent_id,
f"{msgtype}": {
"articles": data
}
}
else:
return print('消息类型错误')
def send_message(self, data, msgtype=None) -> None:
"""
发送消息
:param data: 消息数据
:param msgtype: 消息类型
微信公众号:使用模板消息,不需要指定类型
企业微信:文本消息(text),文本卡片消息(textcard),图文消息(news),markdown消息(markdown)
:return: None
"""
# 模板消息请求地址
if self.platform == AppConst.WECHAT_OFFICIAL_ACCOUNT:
url = f"https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={self.access_token}"
post_data = json.dumps(self.get_send_data(data, msgtype))
elif self.platform == AppConst.ENTERPRISE_WECHAT:
url = f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={self.access_token}"
post_data = json.dumps(self.get_send_data(data, msgtype))
resp = requests.post(url, data=post_data)
result = resp.json()
if result["errcode"] == 0:
print(f"{self.platform} {msgtype} 消息发送成功")
else:
print(result)
10.新建main.py文件
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
@Time :2022/8/21 7:35
@Auth :小呆瓜
@File :main.py
@IDE :PyCharm
@Description:天气预报主文件
"""
# 导入前台调度模块
from apscheduler.schedulers.blocking import BlockingScheduler
# 导入发送消息模块
from send_message import SendMessage
# 导入发送邮件模块
from email_163 import Email163
# 导入获取天气模块
from cma import CmaWeather
# 导入xml配置文件辅助模块
from config_helper import XmlConfigHelper
# 导入应用程序常量
from app_const import AppConst
class Main(object):
def __init__(self):
self.xml_config = XmlConfigHelper()
def get_current_weather_content(self, data_type: str) -> str:
"""
接口获取当前天气信息
:return: 当前天气信息
"""
return cma.get_current_weather_content(data_type)
def send_email_163(self) -> None:
"""
发送网易邮件消息
:return:
"""
print('发送网易邮件消息')
# 实例发送网易邮件
email163 = Email163()
# 主题
subject = '天气预报'
# 接收者(用list[str])
to_addrs = ['980338974@qq.com']
# 邮件内容
text = self.get_current_weather_content('html')
email163.send_email(subject, text, to_addrs)
def send_wechat_official_account(self) -> None:
"""
发送微信公众号消息
:return: None
"""
print('发送微信公众号消息')
# 实例SendMessage
sm = SendMessage(AppConst.WECHAT_OFFICIAL_ACCOUNT)
data = self.get_current_weather_content('json')
sm.send_message(data)
def send_enterprise_wechat(self) -> None:
"""
发送企业微信消息
:return: None
"""
print('发送企业微信消息')
# 实例SendMessage
sm = SendMessage(AppConst.ENTERPRISE_WECHAT)
# 企业微信需要加上消息类型
data = self.get_current_weather_content('markdown')
# 企业微信需要加上消息类型
sm.send_message(data, 'markdown')
def main(self) -> None:
app_name = '天气预报'
print('*****************************************************')
print(f" {app_name} \n")
# 实例一个前台调度
scheduler = BlockingScheduler(timezone='MST')
# 读取配置
root = self.xml_config.get_root()
for node in root.findall('Node'):
name = node.findtext('Name')
description = node.findtext('Description')
triggers = node.findtext('Triggers')
year = self.xml_config.get_int(node.findtext('Year'))
month = self.xml_config.get_int(node.findtext('Month'))
day = self.xml_config.get_int(node.findtext('Day'))
hour = self.xml_config.get_int(node.findtext('Hour'))
minute = self.xml_config.get_int(node.findtext('Minute'))
second = self.xml_config.get_int(node.findtext('Second'))
print(name, description, triggers, year, month, day, hour, minute, second)
if triggers == 'data':
scheduler.add_job(self.send_enterprise_wechat, 'date', run_date=f'{year}-{month}-{day}')
elif triggers == 'interval':
scheduler.add_job(self.send_email_163, 'interval', seconds=second)
elif triggers == 'cron':
scheduler.add_job(self.send_wechat_official_account, 'cron', minute=minute, second=second)
else:
print(f'{name}任务触发器类型错误:{triggers},请指定Triggers:data/interval/cron')
# 输出所有任务信息
# scheduler.print_jobs()
# 开始执行调度
scheduler.start()
print('\n*****************************************************')
if __name__ == '__main__':
# 实例获取天气
cma = CmaWeather()
main = Main()
main.main()
二、运行main.py
等待任务执行
接收邮件消息,不知道什么原因,用网页打开看没有渲染html
用手机打开正常显示,很奇怪
接收微信公众号消息
企业微信,也是手机看正常
电脑客户端看不正常
这里只是一个简单的演示,代码还有很多可以优化的地方,例如access_token,每天请求次数有限制,实际开发中也不可能每次都去请求,浪费了资源,我是因为每天定时发送一次就够了,如果有大量请求的,最好做成配置的超过有效期了再重新获取
三、编程思想
扩展(主要讲 单一职责)
这里扩展讲一下开发过程中的函数封装的思维,比如一个简单的发送天气邮件例子:需要获取一个天气网站上的天气信息,并且每次获取成功都要保存成html文件,然后需要获取网页中当天天气信息,格式化成需要的消息模板后,发送该模板消息到邮箱
很多人可能习惯写在一个类里,分几步执行,这样也没问题,但其实我们应该将任务拆分为一件一件小的事情去处理,这样思考的好处就是,如果后面比如说获取天气的网站变了,邮箱改成了短信发送了,那要修改起来其实整个文件都要改动;
我大致讲一下我的思路,这应该分为以下几件事情(几个函数)
get_html_content(url)
:该函数需要url参数,获取该网页html内容并返回
(获取网页内容)
save_html(html_content)
:该函数需要html_content参数,就是get_html_content函数返回的html内容,然后保存为html文件
(保存网页内容)
get_weather_info(html_content)
:该函数一样需要html_content参数,然后提取html中有用的天气信息并返回,这个函数只是返回有用信息,并没有处理消息内容格式
(获取天气信息)
get_message(weather_info)
:该函数需要weather参数,根据get_weather返回的有用的天气信息,返回构造好后需要发送的消息
(将天气信息构造成要发送的信息)
send_message(message)
:该函数需要message参数,负责发送消息到邮箱(发送消息到邮箱步骤我这里写成一步,应该也是分为几步的,配置信息,发送人,收件人等...道理一样)
(发送信息)
最后一个main方法调用如下:
main(): # 网站url url = "http://www.weather.com.cn/" # 获取网站html内容 html_content = get_html_content(url) # 保存html内容,这一步可以在get_html_content函数中去调用 # save_html(html_content) # 获取网站html中有用得天气信息 weather = get_weather(html_content) # 根据天气信息获取要发送的消息 message = get_message(weather) # 最后,发送消息 send_message(message) # 有人又会问了,刚刚不是说一个函数只做一件事情吗?现在get_html_content这一个函数做了两件事情啊,①获取网页内容;②保存html文件;其实我上面写了:每次获取成功都要保存为html文件,现在这两件事情是强关联的,只是分为两个函数处理事情,这个很多小白开始可能不明白这样做的用意是什么,刚开始学习时我其实也是懵的,这得靠自己悟,熟悉了大家一定都会拥有这种编程思维。我大概提一下,如果save_html中有10行代码处理,好10行代码直接写在get_html_content中,突然哪天获取网页内容成功后不想保存,想要在发送邮件成功后保存,是不是10行代码又得从get_html_content中拿到send_message中,如果我封装为函数处理,是不是在get_html_content中不需要调用save_html,改为send_message去调用save_html就行了,可能有小伙伴会发现,直接在main中调用不就行了,在send_message下面去调用save_html,这是当然可以的,只需要将send_message返回一个发送邮件结果状态就行了。这只是简单的描述,代码量少的时候也没什么关系,可一旦代码量多了,很多事情都放到一起去处理,后期维护真的超累超辛苦的🤕。讲得太多了,尽量保持好的编程习惯,所有编程语言都一样,一个函数只处理一件事情。我们人生又何尝不是呢👨💻
总结
好了,这也是21天学习的一个小成果,虽然活动会结束,但保持热爱学习的心不会结束,心跳不止,学习不止!