学习Flask主站源码,原来可以这样学!
flask—website,是flask曾经的主站源码,使用flask制作,包含模版渲染,数据库操作,openID认证, 全文检索等功能。对于学习如何使用flask制作一个完备的web站点,很有参考价值,我们一起来学习它。
项目结构
flask-website已经归档封存,我们使用最后的版本8b08
,包括如下几个模块:
模块 | 描述 |
---|---|
run.py | 启动脚本 |
websiteconfig.py | 设置脚本 |
update-doc-searchindex.py | 更新索引脚本 |
database.py | 数据库模块 |
docs.py | 索引文档模块 |
openid_auth.py | oauth认证 |
search.py | 搜素模块 |
utils.py | 工具类 |
listings | 一些展示栏 |
views | 蓝图模块,包括社区,扩展,邮件列表,代码片段等 |
static | 网站的静态资源 |
templates | 网站的模版资源 |
flask-website的项目结构,可以作为flask的脚手架,按照这个目录规划构建自己的站点:
.
├── LICENSE
├── Makefile
├── README
├── flask_website
│ ├── __init__.py
│ ├── database.py
│ ├── docs.py
│ ├── flaskystyle.py
│ ├── listings
│ ├── openid_auth.py
│ ├── search.py
│ ├── static
│ ├── templates
│ ├── utils.py
│ └── views
├── requirements.txt
├── run.py
├── update-doc-searchindex.py
└── websiteconfig.py
run.py作为项目的启动入口 requirements.txt描述项目的依赖包 flask_website是项目的主模块,里面包括:存放静态资源的static目录; 存放模版文件的templates目录;存放一些蓝图模块的views模块,使用这些蓝图构建网站的不同页面。
网站入口
网站的入口run.py代码很简单,导入app并运行:
from flask_website import app
app.run(debug=True)
app是基于flask,使用websiteconfig
中的配置进行初始化
app = Flask(__name__)
app.config.from_object('websiteconfig')
app中设置了一些全局实现,比如404页面定义,全局用户,关闭db连接,和模版时间:
@app.errorhandler(404)
def not_found(error):
return render_template('404.html'), 404
@app.before_request
def load_current_user():
g.user = User.query.filter_by(openid=session['openid']).first() \
if 'openid' in session else None
@app.teardown_request
def remove_db_session(exception):
db_session.remove()
@app.context_processor
def current_year():
return {'current_year': datetime.utcnow().year}
加载view部分使用了两种方式,第一种是使用flask的add_url_rule函数,设置了文档的搜索实现,这些url执行docs模块:
app.add_url_rule('/docs/', endpoint='docs.index', build_only=True)
app.add_url_rule('/docs//' , endpoint='docs.show',
build_only=True)
app.add_url_rule('/docs//.latex/Flask.pdf' , endpoint='docs.pdf',
build_only=True)
第二种是使用flask的蓝图功能:
from flask_website.views import general
from flask_website.views import community
from flask_website.views import mailinglist
from flask_website.views import snippets
from flask_website.views import extensions
app.register_blueprint(general.mod)
app.register_blueprint(community.mod)
app.register_blueprint(mailinglist.mod)
app.register_blueprint(snippets.mod)
app.register_blueprint(extensions.mod)
最后app还定义了一些jinja模版的工具函数:
app.jinja_env.filters['datetimeformat'] = utils.format_datetime
app.jinja_env.filters['dateformat'] = utils.format_date
app.jinja_env.filters['timedeltaformat'] = utils.format_timedelta
app.jinja_env.filters['displayopenid'] = utils.display_openid
模版渲染
现在主流的站点都是采用前后端分离的结构,后端提供纯粹的API,前端使用vue等构建。这种结构对于构建小型站点,会比较复杂,有牛刀杀鸡的感觉。对个人开发者,还需要学习更多的前端知识。而使用后端的模版渲染方式构建页面,是比较传统的方式,对小型站点比较实用。
本项目就是使用模版构建,在general蓝图中:
mod = Blueprint('general', __name__)
@mod.route('/')
def index():
if request_wants_json():
return jsonify(releases=[r.to_json() for r in releases])
return render_template(
'general/index.html',
latest_release=releases[-1],
# pdf link does not redirect, needs version
# docs version only includes major.minor
docs_pdf_version='.'.join(releases[-1].version.split('.', 2)[:2])
)
可以看到首页有2种输出方式,一种是json化的输出,另一种是html方式输出,我们重点看看第二种方式。函数render_template传递了模版路径,latest_release和docs_pdf_version两个变量值。
模版也是模块化的,一般是根据页面布局而来。比如分成左右两栏的结构,或者上下结构,布局定义的模版一般叫做layout。比如本项目的模版就从上至下定义成下面5块:
head 一般定义html页面标题(浏览器栏),css样式/js-script的按需加载等 body_title 定义页面的标题 message 定义一些统一的通知,提示类的展示空间 body 页面的正文部分 footer 统一的页脚
使用layout模版定义,将网站的展示风格统一下来,各个页面可以继承和扩展。下面是head块和message块的定义细节:
{% block head %}
{% block title %}Welcome{% endblock %} | Flask (A Python Microframework)
type=text/css href="{{ url_for('static', filename='style.css') }}">
"shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
{% endblock %}
...
"{{ url_for('general.index') }}">overview //
"{{ url_for('docs.index') }}">docs //
"{{ url_for('community.index') }}">community //
"{{ url_for('extensions.index') }}">extensions //
"https://psfmember.org/civicrm/contribute/transact?reset=1&id=20">donate
{% for message in get_flashed_messages() %}
{{ message }}
{% endfor %}
...
本项目首页的general/index继承自全局的layout,并对其中的body部分进行覆盖,使用自己的配置:
{% extends "layout.html" %}
....
{% block body %}
- "{{ latest_release.detail_url }}">Download latest release ({{ latest_release.version }})
- "{{ url_for('docs.index') }}">Read the documentation
- "{{ url_for('mailinglist.index') }}">Join the mailinglist
- Fork it on github
- Add issues and feature requests
...
这个列表主要使用了蓝图中传入的latest_release变量,展示最新文档(pdf)的url
数据库操作
网站有交互,必定要持久化数据。本项目使用的sqlite的数据库,比较轻量级。数据库使用sqlalchemy封装的ORM实现。下面的代码展示了如何创建一个评论:
@mod.route('/comments//' , methods=['GET', 'POST'])
@requires_admin
def edit_comment(id):
comment = Comment.query.get(id)
snippet = comment.snippet
form = dict(title=comment.title, text=comment.text)
if request.method == 'POST':
...
form['title'] = request.form['title']
form['text'] = request.form['text']
..
comment.title = form['title']
comment.text = form['text']
db_session.commit()
flash(u'Comment was updated.')
return redirect(snippet.url)
...
创建comment对象 从html的form表单中获取用户提交的title和text 对comment对象进行赋值和提交 刷新页面的提示信息(在模版的message部分展示) 返回到新的url
借助sqlalchemy,数据模型的操作API简单易懂。要使用数据库,需要先创建数据库连接,构建模型等, 主要在database模块:
DATABASE_URI = 'sqlite:///' + os.path.join(_basedir, 'flask-website.db')
# 创建引擎
engine = create_engine(app.config['DATABASE_URI'],
convert_unicode=True,
**app.config['DATABASE_CONNECT_OPTIONS'])
# 创建session(连接)
db_session = scoped_session(sessionmaker(autocommit=False,
autoflush=False,
bind=engine))
# 初始化
def init_db():
Model.metadata.create_all(bind=engine)
# 定义基础模型
Model = declarative_base(name='Model')
Model.query = db_session.query_property()
Comment数据模型定义:
class Comment(Model):
__tablename__ = 'comments'
id = Column('comment_id', Integer, primary_key=True)
snippet_id = Column(Integer, ForeignKey('snippets.snippet_id'))
author_id = Column(Integer, ForeignKey('users.user_id'))
title = Column(String(200))
text = Column(String)
pub_date = Column(DateTime)
snippet = relation(Snippet, backref=backref('comments', lazy=True))
author = relation(User, backref=backref('comments', lazy='dynamic'))
def __init__(self, snippet, author, title, text):
self.snippet = snippet
self.author = author
self.title = title
self.text = text
self.pub_date = datetime.utcnow()
def to_json(self):
return dict(author=self.author.to_json(),
title=self.title,
pub_date=http_date(self.pub_date),
text=unicode(self.rendered_text))
@property
def rendered_text(self):
from flask_website.utils import format_creole
return format_creole(self.text)
Comment模型按照结构化的方式定义了表名,6个字段,2个关联关系和json化和文本化的展示方法。
sqlalchemy的使用,在之前的文章中有过介绍,本文就不再赘述。
openID认证
一个小众的网站,构建自己的账号即麻烦也不安全,使用第三方的用户体系会比较合适。本项目使用的是Flask-OpenID这个库提供的optnID登录认证。
用户登录的时候,会根据用户选择的三方登录站点,跳转到对应的网站进行认证:
@mod.route('/login/', methods=['GET', 'POST'])
@oid.loginhandler
def login():
..
openid = request.values.get('openid')
if not openid:
openid = COMMON_PROVIDERS.get(request.args.get('provider'))
if openid:
return oid.try_login(openid, ask_for=['fullname', 'nickname'])
..
从对应的模版上更容易理解这个过程, 可以看到默认支持AOL/Google/Yahoo三个账号体系认证:
{% block body %}
{% endblock %}
在三方站点认证完成后,会建立本站点的用户和openid的绑定关系:
@mod.route('/first-login/', methods=['GET', 'POST'])
def first_login():
...
db_session.add(User(request.form['name'], session['openid']))
db_session.commit()
flash(u'Successfully created profile and logged in')
...
session中的openid是第三方登录成功后写入session
三方登录的逻辑过程大概就如上所示,先去三方平台登录,然后和本地站点的账号进行关联。其具体的实现,主要依赖Flask-OpenID这个模块, 我们大概了解即可。
全文检索
全文检索对于一个站点非常重要,可以帮助用户在网站上快速找到适合的内容。本项目展示了使用whoosh这个纯python实现的全文检索工具,构建网站内容检索,和使用ElasticSearch这样大型的检索库不一样。总之,本项目使用的都是小型工具,纯python实现。
全文检索从/search/
入口进入:
@mod.route('/search/')
def search():
q = request.args.get('q') or ''
page = request.args.get('page', type=int) or 1
results = None
if q:
results = perform_search(q, page=page)
if results is None:
abort(404)
return render_template('general/search.html', results=results, q=q)
q是搜素的关键字,page是翻页的页数 使用perform_search方法对索引进行查询 如果找不到内容展示404;如果找到内容,展示结果
在search模块中提供了search方法,前面调用的perform_search函数是其别名:
def search(query, page=1, per_page=20):
with index.searcher() as s:
qp = qparser.MultifieldParser(['title', 'content'], index.schema)
q = qp.parse(unicode(query))
try:
result_page = s.search_page(q, page, pagelen=per_page)
except ValueError:
if page == 1:
return SearchResultPage(None, page)
return None
results = result_page.results
results.highlighter.fragmenter.maxchars = 512
results.highlighter.fragmenter.surround = 40
results.highlighter.formatter = highlight.HtmlFormatter('em',
classname='search-match', termclass='search-term',
between=u' … ')
return SearchResultPage(result_page, page)
从ttile和content中搜素关键字q 设置使用unicode编码 将检索结果封装成SearchResultPage
重点在index.searcher()
这个索引, 它使用下面方法构建:
from whoosh import highlight, analysis, qparser
from whoosh.support.charset import accent_map
...
def open_index():
from whoosh import index, fields as f
if os.path.isdir(app.config['WHOOSH_INDEX']):
return index.open_dir(app.config['WHOOSH_INDEX'])
os.mkdir(app.config['WHOOSH_INDEX'])
analyzer = analysis.StemmingAnalyzer() | analysis.CharsetFilter(accent_map)
schema = f.Schema(
url=f.ID(stored=True, unique=True),
id=f.ID(stored=True),
title=f.TEXT(stored=True, field_boost=2.0, analyzer=analyzer),
type=f.ID(stored=True),
keywords=f.KEYWORD(commas=True),
content=f.TEXT(analyzer=analyzer)
)
return index.create_in(app.config['WHOOSH_INDEX'], schema)
index = open_index()
whoosh创建本地的索引文件 whoosh构建搜素的数据结构,包括url,title,,关键字和内容 关键字和内容参与检索
索引需要构建和刷新:
def update_documentation_index():
from flask_website.docs import DocumentationPage
writer = index.writer()
for page in DocumentationPage.iter_pages():
page.remove_from_search_index(writer)
page.add_to_search_index(writer)
writer.commit()
文档索引构建在docs模块中:
DOCUMENTATION_PATH = os.path.join(_basedir, '../flask/docs/_build/dirhtml')
WHOOSH_INDEX = os.path.join(_basedir, 'flask-website.whoosh')
class DocumentationPage(Indexable):
search_document_kind = 'documentation'
def __init__(self, slug):
self.slug = slug
fn = os.path.join(app.config['DOCUMENTATION_PATH'],
slug, 'index.html')
with open(fn) as f:
contents = f.read().decode('utf-8')
title, text = _doc_body_re.search(contents).groups()
self.title = Markup(title).striptags().split(u'—')[0].strip()
self.text = Markup(text).striptags().strip().replace(u'¶', u'')
@classmethod
def iter_pages(cls):
base_folder = os.path.abspath(app.config['DOCUMENTATION_PATH'])
for dirpath, dirnames, filenames in os.walk(base_folder):
if 'index.html' in filenames:
slug = dirpath[len(base_folder) + 1:]
# skip the index page. useless
if slug:
yield DocumentationPage(slug)
文档读取DOCUMENTATION_PATH目录下的源文件(项目文档) 读取文件的标题和文本,构建索引文件
小结
本文我们走马观花的查看了flask-view这个flask曾经的主站。虽然没有深入太多细节,但是我们知道了模版渲染,数据库操作,OpenID认证和全文检索四个功能的实现方式,建立了相关技术的索引。如果我们需要构建自己的小型web项目,比如博客,完全可以以这个项目为基础,修改实现。
经过数周的调整,接下我们开始进入python影响力巨大的项目之一: Django。敬请期待。
小技巧
本项目提供了2个非常实用的小技巧。第1个是json化和html化输出,这样用户可以自由选择输出方式,同时站点也可以构建纯API的接口。这个功能是使用下面的request_wants_json函数提供:
def request_wants_json():
# we only accept json if the quality of json is greater than the
# quality of text/html because text/html is preferred to support
# browsers that accept on */*
best = request.accept_mimetypes \
.best_match(['application/json', 'text/html'])
return best == 'application/json' and \
request.accept_mimetypes[best] > request.accept_mimetypes['text/html']
request_wants_json函数中判断头部的mime类型,进行根据是application/json
还是text/html
决定展示方式。
第2个小技巧是认证装饰器, 前面一个是登录验证,后一个是超级管理认证:
def requires_login(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if g.user is None:
flash(u'You need to be signed in for this page.')
return redirect(url_for('general.login', next=request.path))
return f(*args, **kwargs)
return decorated_function
def requires_admin(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not g.user.is_admin:
abort(401)
return f(*args, **kwargs)
return requires_login(decorated_function)
这两个装饰器,在view的API上使用, 比如编辑snippet需要登录,评论需要管理员权限:
@mod.route('/edit//' , methods=['GET', 'POST'])
@requires_login
def edit(id):
...
@mod.route('/comments//' , methods=['GET', 'POST'])
@requires_admin
def edit_comment(id):
...
参考链接
https://github.com/pallets/flask-website
推荐阅读:
入门: 最全的零基础学Python的问题 | 零基础学了8个月的Python | 实战项目 |学Python就是这条捷径
干货:爬取豆瓣短评,电影《后来的我们》 | 38年NBA最佳球员分析 | 从万众期待到口碑扑街!唐探3令人失望 | 笑看新倚天屠龙记 | 灯谜答题王 |用Python做个海量小姐姐素描图 |碟中谍这么火,我用机器学习做个迷你推荐系统电影
趣味:弹球游戏 | 九宫格 | 漂亮的花 | 两百行Python《天天酷跑》游戏!
AI: 会做诗的机器人 | 给图片上色 | 预测收入 | 碟中谍这么火,我用机器学习做个迷你推荐系统电影
小工具: Pdf转Word,轻松搞定表格和水印! | 一键把html网页保存为pdf!| 再见PDF提取收费! | 用90行代码打造最强PDF转换器,word、PPT、excel、markdown、html一键转换 | 制作一款钉钉低价机票提示器! |60行代码做了一个语音壁纸切换器天天看小姐姐!|
年度爆款文案
点阅读原文,看原创200个趣味案例!