学习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)
{% endblock %}
...
overview //
docs //
community //
extensions //
donate
{% for message in get_flashed_messages() %}
{{ message }}
{% endfor %}
...
本项目首页的general/index继承自全局的layout,并对其中的body部分进行覆盖,使用自己的配置:
{% extends "layout.html" %}
....
{% block body %}
- Download latest release ({{ latest_release.version }})
- Read the documentation
- 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(uComment 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(uSuccessfully 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(uYou 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行代码做了一个语音壁纸切换器天天看小姐姐!|
年度爆款文案
-
1). 卧槽!Pdf转Word用Python轻松搞定 !
-
2).学Python真香!我用100行代码做了个网站,帮人PS旅行图片,赚个鸡腿吃
-
3).首播过亿,火爆全网,我分析了《乘风破浪的姐姐》,发现了这些秘密
-
4). 80行代码!用Python做一个哆来A梦分身
-
5).你必须掌握的20个python代码,短小精悍,用处无穷
-
6). 30个Python奇淫技巧集
-
7). 我总结的80页《菜鸟学Python精选干货.pdf》,都是干货
-
8). 再见Python!我要学Go了!2500字深度分析 !
-
9).发现一个舔狗福利!这个Python爬虫神器太爽了,自动下载妹子图片
点阅读原文,看原创200个趣味案例!
版权声明
所有资源都来源于爬虫采集,如有侵权请联系我们,我们将立即删除