Web开发

Flask Web开发 – 搭建微信公众号后台系统

Flask Web开发 – 搭建微信公众号后台系统

Python酷的文章一般在 https://pythonlibrary.net/ 网页上首发,而随后同步的我们的微信,然后由于我们的文章都是包含大量代码的干货文章,对于微信阅读其实效果并不是特别理想,因此我们后来将微信公众号的方向转为为订阅用户提供文章摘要,新文章发布提醒,以及文章搜索的功能,未来我们还考虑增加例如智能对话,或者AI助手等等功能,读者如果有更好的意见和建议也可以发送给我们。公众号自带的后台管理就没办法满足要求了,因此我们使用Flask搭建了微信公众号的后台系统,同时我们认为这也是一个很好的机会来给大家讲解如何使用Flask进行Web开发和后端开发。

为什么要用Flask

Flask是一个在Python世界上非常流行的Web开发框架,它非常的微型,不像Django提供了很多开箱即用的功能,Flask本身仅仅提供了请求,路由等等核心功能,用户可以自由的在开源社区选取高质量的Flask扩展来组合实现想要的功能,对于新手开发者可以很快的上手,并从中学习到更多通用的知识,如果说学习Django就是如何学习使用Django来搭建Web应用的话,学习Flask是学习使用Python来搭建Web应用。对于资深开发人员也可以保质保量的完成项目,而且我们在 跟我一起读源码 – 如何阅读开源代码 的文章中也说过,这个项目的源代码实现非常的优雅,有着很大的社区基础和支持。

考虑到看这篇文章的读者可能大多是刚入门Python Web开发,结合基于以上Flask的优势,我们使用了Flask作为微信公众号后台的开发框架。

读者将学到什么

在本篇文章中我们不会从一个Hello World的示例讲起,而是以Flask为基础,介绍一个通用的Flask项目结构,读者能够将这个项目结构应用到自己的 Python后端开发 大型项目中,同时在这个结构中,完成我们微信公众号后台提供的功能,包括信息收发(接收和回复)以及文章资源搜索。因此需要读者对Web开发的基本概念有一些认识,如果你还从来没有读过或者运行过任何一个Python Web框架的Hello World,或者对Python Web开发没有任何认知,建议读者先阅读 数据可视化 – 利用Bokeh和Bottle.py在网上展示你的数据 这篇文章(文章第三章节可以帮着你掌握这些基础知识)。

在本篇文章中,我们涵盖的技术点包括

  • 项目文件结构搭建
  • REST API
  • 日志
  • 开发环境和生产环境配置
  • 在Web应用中使用定时任务(定时抓取数据)
  • 微信后台接口

认识项目

在学习如何搭建Python Web项目结构之前,我们先来介绍一下本项目的框架。用户向我们的微信公众号发送消息,然后公众号会使用Post方法调起我们开发的Flask后台,在Flask后端,我们会验证接收到的消息是否合法,如果合法则去文章缓存中查询是否有满足用户查询的文章,如果有则返回文章链接,如果没有则返回帮助信息。

另外,为了提升用户体验,加快相应速度,我们在https://pythonlibrary.net发布的文章会被Flask后端周期性的拉取并做缓存。

为了满足上边描述的这些功能,在这个项目中我们主要用到的第三方库有

  • Flask:Python Web 框架
  • Flask-Restx:Flask-Restplus 的下一代开源项目,用于构架Rest API
  • click:用来构建命令行工具
  • requests:Web请求工具,用于从https://pythonlibrary.net来拉取数据
  • beautifulsoup:用来分析从网站拉取到的html和xml数据
  • apscheduler:用于创建后台定时任务,定时从网站拉取数据

所有的需求都放在项目的requirements里边了,大家可以使用pip一键安装。(使用requirements文件来放置Python项目用到的依赖是一个非常好的编码习惯)

项目结构

我们采用的代码文件夹结构如下,其中

  • config.py:包含了所有支持用户可以修改的配置,例如微信公众号的Token,log文件的存储位置,因为本应用中没有使用到数据库,如果你的项目中需要用到数据库,那么数据库的uri也可以放到这里来配置
  • manage.py:利用click库实现的一个命令行入口,用于启动和管理Web服务,例如,如果要将服务运行起来可以运行python manage.py run
  • requirements.txt 和 requirements文件夹:包含了项目需要的外部依赖库,可以通过pip一键安装
  • app文件夹:该文件夹中包含了后端的核心代码,其中
    • apis文件夹:包含了实现对接微信公众号的REST API
    • core文件夹:包含了后端的业务逻辑,本项目中主要是周期性文章拉取
    • apiv1.py:将apis里边的REST API集合起来,放置到Flask蓝图里边,形成统一的访问链接
    • init.py:加载配置,使用工厂模式创建app,并启动周期拉取文章的任务
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
│  config.py
│  manage.py
│  requirements.txt
├─app
│  │  apiv1.py
│  │  __init__.py
│  │  
│  ├─apis
│  │  │  __init__.py
│  │  │  
│  │  ├─chat
│  │  │  │  ai.py
│  │  │  │  receive.py
│  │  │  │  reply.py
│  │  │  │  resources.py
│  │  │  │  __init__.py
│  │  │          
│  ├─core
│  │  │  articles.py
│      
├─requirements
│      common.txt
│      dev.txt
│      prod.txt

配置文件和读取

在做Web项目的时候,我们会有各种各样的配置信息,用来控制项目的运行,例如我们需要设置数据库的位置,设置一些应用的Token等等,在源码中我们将这些配置集中起来,更容易管理和修改,尤其是在Web开发中,有生产环境和开发环境,用到的设置可能会不一致,在一个统一的配置文件中就可以很好的管理了。

我们项目中的config.py就是我了这个目的而实现的,在文件中,有一个叫做Config的类,它包含两个类方法start_hook和init_app,start_hook会在创建Flask App对象之前被调用,而init_app会在App对象被创建好以后进行一些必要的初始化,它接收Flask App对象作为参数。

1
2
3
4
5
6
7
8
9
class Config(object):

    @classmethod
    def start_hook(cls):
        pass

    @classmethod
    def init_app(cls, app):
        pass

我们其他的配置都是集成自这个基础的Config类,在我们这个项目,有LocalmachineConfig,ProductionConfig和DockerConfig三种配置,并形成一个配置字典。

1
2
3
4
5
config = {
    'development': LocalmachineConfig,
    'production': DockerConfig,
    'default': LocalmachineConfig
}

其中LocalmachineConfig为我们的开发环境配置,DockerConfig为我们的生成环境配置,因为我们使用了Docker作为部署方式,而DockerConfig又继承自ProductionConfig,默认的环境为开发环境,也就是说当我们调用 python manage.py run 来运行应用的时候,默认是运行开发环境,只有当我们在环境变量中设置FLASK_CONFIG=production 才会启动生产环境。

接下来我们以生产环境配置ProductionConfig为例来看一下,具体怎么来添加和设置配置信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class ProductionConfig(Config):
    DEBUG = False

    WEIWIN_TOKEN = 'foobar'

    CONFIG_DIR = '/usr/config'
    SITEMAP_URL = "http://wordpress/post-sitemap.xml"

    @classmethod
    def init_app(cls, app):
        app.logger.removeHandler(default_handler)
        formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s')
        # TODO - split log files if size is too big
        file_handler = RotatingFileHandler("/var/log/" + 'pythonlibrary_api_log.log', maxBytes=1024 * 1024 * 100, backupCount=10)
        file_handler.setLevel(logging.INFO)
        file_handler.setFormatter(formatter)
        app.logger.addHandler(file_handler)
        app.logger.setLevel(logging.INFO)

所有的配置可以以成员变量的方式来指定,比如微信后台的授权码,可以用WEIWIN_TOKEN来指定,我们的需要周期拉取的网站地址由SITEMAP_URL 来指定。

这些配置信息都可以通过Flask的 current_app对象来获取,例如,可以通过

1
current_app.config.get('SITEMAP_URL')

来获取到SITEMAP_URL配置信息

而在init_app中,可以对一些必要的需要初始化的功能进行初始化。在这里,我们将Flask内置的logger的格式进行了修改,同时,将日志文件输出到/var/log/pythonlibrary_api_log.log文件中,如果不做配置的话,默认日志会打印到控制台。

周期爬取文章

在Web应用中,定时的爬取其他来源并进行缓存也是一个很常见的应用场景,在我们这个项目中,我们需要从https://pythonlibray.net中定期的抓取文章并保存,从而在用户进行搜索的时候能够快速响应。通常这些周期获取到的数据会保存到数据库,或者为了性能会保存到Redis中,但是我们这个项目由于数据量较小,我们进行了简化,这些数据就直接放到内存的数据结构中。

为了实现定时周期性的运行抓取任务,我们使用了 apscheduler 这个库,我们可以利用这个库,来定义自己的周期任务,它会安装我们设定好的周期策略来运行已经定义好的任务,也就是爬取数据的任务。

我们将这部分的实现放在了app/init.py中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def create_app(config_name):
    scheduler = BackgroundScheduler()
    
    config[config_name].start_hook()

    app = Flask(__name__)
    app.register_blueprint(api_v1)

    app.config.from_object(config[config_name])  # Read config from config.py

    app.config['RESTFUL_JSON'] = {
        'ensure_ascii': False
    }

    # Ugly implementation for saving scheduler object and articles cache
    setattr(app, 'articles', None)
    setattr(app, 'ap_scheduler', scheduler)

    post_sitemap = app.config.get('SITEMAP_URL')

    # run task right away at the startup
    update_articles(post_sitemap, app)
    app.ap_scheduler.add_job(update_articles, 'interval', [post_sitemap, app], days=10)
    app.ap_scheduler.start()

    config[config_name].init_app(app)  # call init_app from config

    return app

create_app这个函数主要是用来创建Flask App的,因此如上一节所讲解,在这个函数中,我们首先是调用了Config类的start_hook方法,然后创建了Flask App,再然后,调用了Config类的init_app方法,最后将这个app返回。

除此之外,我们还在这里定义了一个后台任务管理器,并向里边添加了一个update_articles任务,该任务的执行周期为10天,同时在启动任务之前,我们强制运行了一遍这个任务,因为默认情况下apscheduler 第一次启动是不会运行任务的,只有当周期到了以后才会运行,因为我们采用内存数据结构保存爬取的数据,所以Web应用第一次运行的时候,内存数据结构里边是空的,我们又不想等待那么长的时间才能拿到数据来服务用户,所以,需要手动运行一遍这个任务来爬取一些数据进来。

1
2
3
update_articles(post_sitemap, app)    
app.ap_scheduler.add_job(update_articles, 'interval', [post_sitemap, app], days=10)
app.ap_scheduler.start()

另外,大家会问,那么内存数据结构在哪里,爬取到的数据存到哪里了?我们希望这些数据能够被 Web应用全局的访问,因此为了简化代码,我们使用了一个较为简单的实现,如下边代码,我们在Flask App上边创建了一个articles属性来保存爬取到的数据,因为Flask App是全局可以通过current_app来访问的,所以我们的article也是全局可以访问的,在稍微大一点的正式项目中不建议用这种方法,而是要分开定义一个单独的数据存储类。

1
2
# Ugly implementation for saving scheduler object and articles cache
    setattr(app, 'articles', None)

而在core/articles.py文件中对于update_articles这个周期任务的实现为:

1
2
3
4
def update_articles(post_sitemap, app):
    articles = get_all_articles(post_sitemap)
    app.logger.info('Posts updated at: %s' % datetime.now())
    setattr(app, 'articles', articles)

在这里通过get_all_articles爬取到的数据,通过setattr设置给Flask App的articles属性来保存。有了文章以后,我们怎么跟微信公众号后台进行对接呢?答案就是通过API,我们的Flask后端设计一个API,然后微信公众号可以调用这个后端,接下来我们就介绍这一部分代码。

API 设计

在前后端分离的Web开发中,前端和后端的交互是通过API实现的,跟我们这个项目的概念是一样的,因此我们这里讲解的技术在开发有前端的项目中是通用的。而API的规范在设计和实现API的时候是很重要的,通常比较流行的规范是REST,按照这个规范定义出来的API会比较规范,在和团队其他成员进行沟通,或者需要文档化的时候都会有一个统一的标准,我们简单的借鉴了这个思想,但是由于API需要满足微信公众号的要求,因此可能不会完全满足REST规范要求,REST规范也很简单,当你学会了怎么设计API以后,来使用REST规范也就很容易了。

在本项目中,我们使用了flask_restx这个库来辅助我们现在REST规范的API。

版本

在REST规范中,要求我们的API要有版本管理,因为我们未来会有升级API的可能,因此,使用版本管理,可以让API使用者能够开发更加健壮的代码,不会因为API的升级导致API使用者的代码失效,或者导致软件不可用。API的版本体现在API的url中,一个符合REST版本管理要求的API长这个样子 http://domain/api/v1/xxxxxx

其中前半部分http://domain/api/v1/是固定的,里边包含了版本号v1,而后边的xxxxxx则根据每一个API的不同按需设计,在apiv1.py中我们通过下边这样的代码创建了版本为1的API

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from app.apis.chat.resources import ns as chat_ns

api_v1 = Blueprint('api1', __name__, url_prefix='/api/v1')

api = Api(api_v1, version='1.0', title='flask backend reference project',
    description='flask backend reference',
    default='chat', 
    doc=False
)

api.add_namespace(chat_ns)

通过指定Blueprint的url_prefix参数为/api/v1来指定我们的api基础部分为 http://domain/api/v1

而API的后半部分(xxxxxx部分)则在app.apis.chat.resources中实现

资源

REST规范中,认为每一个API就是访问一个特定的服务器资源,因此API又可以叫资源,我们这个项目实现了一个chat资源,位置为app/apis/chat/resources。我们前边讲了,由于微信公众号后台对于API的要求,我们不能够设计完全满足REST规范的API,因此,在本项目中,只有版本和资源这两个在概念上满足REST规范,具体针对资源的设计并不满足该规范,读者可以再阅读REST规范去学习更多的REST API知识。

我们希望微信公众号后台通过http://domain/api/v1/chat/message 来跟Flask后端交互,因此在resources.py中实现了下边这个MessageResourceHandler,通过ns.route指定了这个资源的位置为/message,而这个资源的处理函数为MessageResourceHandler,继承自flask_restx的Resource类。

1
2
3
4
ns = Namespace('chat', description='provide chat functionalities')

@ns.route('/message', strict_slashes=False)
class MessageResourceHandler(Resource):

strict_slashes设置为False是告诉Flask应用针对 /message 和 /message/ 都要使用这个类来处理,而这个值如果为True的话,/message 和 /message/ 要分别处理。

总结

利用 flask_restx 来做REST API开发的时候,一个API会由三个部分组成:

  • Blueprint:在apiv1.py中创建
  • Namespace:在resrouces.py中创建
  • Resource:在resrouces.py中通过route中指定

微信公众号对接

通过上边一节的内容,我们学会了如何定义一个API,而API的逻辑要在 MessageResourceHandler中实现,我们的目标是跟微信公众号对接,因此我们的API的实现逻辑要满足微信公众号的要求,要参考微信公众号关于API的文档,可以在下边这个链接找到。本项目需要掌握的微信API调用方法,都可以通过在下边你这个页面学会,事实上,我们这个项目中用到的微信消息处理和回复逻辑基本上就是拷贝了示例代码。

https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Getting_Started_Guide.html

MessageResourceHandler的Get方法

想要把自定义的API绑定到微信公众号后台,为了安全,微信公众号需要对我们的API进行认证,也就是说,微信公众号绑定一个API的时候要验证这个API是不是我们自己的,而认证的方法是通过调用API的Get方法,然后给这个方法传送一个秘钥,在Get方法中对秘钥进行解析比对,如果通过则返回特定字符串,如果不通过则什么都不返回。

我们在MessageResourceHandler中实现了get类方法,并将微信的秘钥认证逻辑进行了实现,有了这个方法以后,就可以在微信公众号的后台将http://domain/api/v1/chat/message绑定到账号。

MessageResourceHandler的Post方法

当API绑定成功以后,微信公众号每次收到订阅用户发来的消息以后,会自动调用绑定API(http://domain/api/v1/chat/message)的Post方法。对应的,我们在MessageResourceHandler中实现了post类方法,并将收到的消息以xml格式的方式打包传入post类方法,我们需要做的就很简单了,通过解析收到的信息,然后利用这些信息去搜索前边已经实现的内存数据缓存,然后在组合出需要回复给用户的消息,通过post方法的return来返回出去。

日志记录

世界上不存在没有bug的软件,一个运行在服务器上的Web应用,如果出问题了,是需要一些手段能够记录下出问题时候的一些信息的,这些信息能够辅助我们解决bug,改善产品功能,而Web应用的日志就是帮我们记录这些信息的。

Flask内部帮忙实现了日志记录器,通过Flask的全部对象current_app就可以访问到这个日志记录器,然后使用和Python内置日期记录器logging模块一样的方法就可以实现日志记录,例如在MessageResourceHandler中的post类方法中,我们通过下边这样的日志记录器调用可以记录当前消息是由哪个用户发过来的,消息的内容是什么。

1
current_app.logger.info("received {} from: {}".format(recMsg.Content, fromUser))

而日志记录的位置,我们在配置章节已经讲过,可以通过在config.py文件中进行配置,在本项目中,我们指定开发环境的日志会打印到标准输出,而在生产环境会记录到:/var/log/pythonlibrary_api_log.log中

实现结果

在本篇文章中,我们对如何使用Flask搭建Web后端进行了讲解,并针对我们项目的关键代码进行了说明,我们建议读者下载项目源码,尝试运行并阅读源码,以更好的掌握这些知识,本项目的代码可以从GitHub仓库:https://github.com/pythonlibrary/flask-wechat-backend 中获取,最终部署完成,以及从微信公众号中将我们自己的后台连接成功以后,公众号就可以处理用户发来的消息了。

  • 收到任何处理不了的信息都会回复帮助信息给用户
  • 查询到结果的搜索将回复文章链接给用户

关于 Python酷

Python之所以如此流行,在于它有强大的生态,使用各种各种的库可以帮助用户最快速的解决问题。Python酷致力于输出高质量的Python库相关教程及技术性文章,帮助用户更好更快速的解决问题