【Python】天气预报(发送网易邮件,微信公众测试号,企业微信),周末用时一天,两万字代码,纯肝货(完整项目)一一CSDN21天学习挑战赛



本系列文章为参与【Python】CSDN21天学习挑战赛,成为更好的自己,根据自身的学习进度并记录自己的学习过程。我也是Python纯小白,和大家一起学习,保持热爱学习的好习惯😁

活动地址:CSDN21天学习挑战赛


前言

不清楚怎么发送消息的,可以参考以下三篇文章:

【Python】发送邮件,超详细看图敲码(附完整代码)

【Python】发送微信公众号消息(附完整代码)

【Python】发送企业微信消息(附完整代码)

关于定时任务的文章:

【Python】任务调度模块APScheduler(内含定点报时案例)


一、新建项目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(
                    "&nbsp;", "").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天学习的一个小成果,虽然活动会结束,但保持热爱学习的心不会结束,心跳不止,学习不止!


posted @ 2022-08-24 18:15  小呆瓜耶  阅读(196)  评论(2编辑  收藏  举报