网易首页 > 网易号 > 正文 申请入驻

学习Flask主站源码,构建自己的Web站点

0
分享至

大家好,我是肖恩,源码解析每周见

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 %}
"" method=post>


For some of the features on this site (such as creating snippets
or adding comments) you have to be signed in. You don't need to
create an account on this website, just sign in with an existing
OpenID account.


OpenID URL:


Alternatively you can directly sign in by clicking on one of
the providers here in case you don't know the identity URL:


  • AOL
  • Google
  • Yahoo

{% 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猫技术交流群开放啦!群里既有国内一二线大厂在职员工,也有国内外高校在读学生,既有十多年码龄的编程老鸟,也有中小学刚刚入门的新人,学习氛围良好!想入群的同学,请在公号内回复『 交流群』,获取猫哥的微信 (谢绝广告党,非诚勿扰!)~

还不过瘾?试试它们

如果你觉得本文有帮助

请慷慨分享 点赞 ,感谢啦

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
吸金200亿!落魄的诺基亚,2024年居然卖断货,重回世界第二!

吸金200亿!落魄的诺基亚,2024年居然卖断货,重回世界第二!

小柱解说游戏
2024-12-01 22:02:22
A股有两消息疯传,如果是真的,A股的底部将在明天出现!

A股有两消息疯传,如果是真的,A股的底部将在明天出现!

一丛深色花儿
2024-12-02 13:09:14
台湾女主持穿全!透!明雨衣直播采访火上热搜!男生看完直呼:眼睛怀孕了啊啊

台湾女主持穿全!透!明雨衣直播采访火上热搜!男生看完直呼:眼睛怀孕了啊啊

经典段子
2024-11-30 22:40:51
发量危机❗日本网友:27岁三笘薫头发逐渐稀疏,似乎有秃头趋势

发量危机❗日本网友:27岁三笘薫头发逐渐稀疏,似乎有秃头趋势

直播吧
2024-12-01 18:23:11
中国彻底崩塌的专业,月薪从1.5万降到3000,大批毕业生无法就业

中国彻底崩塌的专业,月薪从1.5万降到3000,大批毕业生无法就业

教育导向分享
2024-11-30 17:36:11
大兴机场被北漂睡成“洗浴中心”,撕开当下社会最体面的一幕

大兴机场被北漂睡成“洗浴中心”,撕开当下社会最体面的一幕

这班我上够了
2024-11-30 13:50:03
为什么肯德基麦当劳从来不验取餐码?不怕别人冒领吗?原来是这样

为什么肯德基麦当劳从来不验取餐码?不怕别人冒领吗?原来是这样

有趣的火烈鸟
2024-12-01 22:48:12
背靠背打森林狼!老詹:飞机上我肯定睡不着 这场的一切还未散去

背靠背打森林狼!老詹:飞机上我肯定睡不着 这场的一切还未散去

直播吧
2024-12-02 12:21:11
让你的性生活火热!这些小技巧你一定要知道!

让你的性生活火热!这些小技巧你一定要知道!

智见派
2024-12-01 13:01:01
新加坡人力部:90%应届大学毕业生在毕业六个月内找到工作

新加坡人力部:90%应届大学毕业生在毕业六个月内找到工作

新加坡眼
2024-11-30 18:54:04
即将下架!华为:深表歉意

即将下架!华为:深表歉意

21世纪经济报道
2024-12-02 09:11:06
难以置信,2019年的民营企业五百强前10名,已经有5家暴雷了

难以置信,2019年的民营企业五百强前10名,已经有5家暴雷了

说故事的阿袭
2024-12-02 09:24:00
调查11000名中老年人后,发现爱打麻将者,最终会变这样…

调查11000名中老年人后,发现爱打麻将者,最终会变这样…

华人星光
2024-10-24 13:27:21
哈登你太强了,詹姆斯用22年创造的纪录,如今被你打破了

哈登你太强了,詹姆斯用22年创造的纪录,如今被你打破了

大西体育
2024-12-02 14:44:16
张碧晨这身材真好,不明白华晨宇怎么就看不上她。都已经有女儿了

张碧晨这身材真好,不明白华晨宇怎么就看不上她。都已经有女儿了

人情皆文史
2024-11-01 00:13:49
英超:利物浦2-0送曼城4连败!各项7连胜+9分优势领跑 萨拉赫传射

英超:利物浦2-0送曼城4连败!各项7连胜+9分优势领跑 萨拉赫传射

侃球熊弟
2024-12-02 01:37:24
具俊晔请老婆吃日式海鲜料理,大S笑逐颜开,隔着屏幕都听到了

具俊晔请老婆吃日式海鲜料理,大S笑逐颜开,隔着屏幕都听到了

鑫鑫说说
2024-12-02 10:29:12
反政府军距离首都150公里,巴沙尔的弟弟发动政变?

反政府军距离首都150公里,巴沙尔的弟弟发动政变?

西楼饮月
2024-12-01 12:49:41
以前禁止卫星锅,大家都偷偷装,为何现在没人管了反而没人使用了

以前禁止卫星锅,大家都偷偷装,为何现在没人管了反而没人使用了

慎独赢
2024-11-30 22:20:03
冯绍峰新女友正脸首曝光,与男方一路热聊,女方是素人长得很漂亮

冯绍峰新女友正脸首曝光,与男方一路热聊,女方是素人长得很漂亮

娱乐书坊
2024-12-02 11:53:11
2024-12-02 15:00:49
Python猫
Python猫
人生苦短,我用Python。博客:https://pythoncat.top
664文章数 8103关注度
往期回顾 全部

科技要闻

11月成绩单:小鹏首破3万,蔚来小米破2万

头条要闻

泰国坠崖孕妇已签约MCN机构 曾被婆婆指责生意做太大

头条要闻

泰国坠崖孕妇已签约MCN机构 曾被婆婆指责生意做太大

体育要闻

强势比6根手指!瓜迪奥拉回击利物浦球迷

娱乐要闻

这是麦琳?烟熏妆神似安室奈美惠!

财经要闻

刘世锦:扩大消费需求要找准重点或痛点

汽车要闻

小米汽车:11月交付继续超2万辆 全年冲刺13万辆

态度原创

手机
时尚
健康
教育
本地

手机要闻

前十个月vivo第一,华为国产第二,小米第三

今年“卫衣+渣女裤”也太好穿了!显瘦、时髦,谁穿谁好看!

花18万治疗阿尔茨海默病,值不值?

教育要闻

期中考试后遗症,震荡了三周,定睛一看,马上又要期末考试了!

本地新闻

云游中国|来伦布夏果感受充满Passion的人生

无障碍浏览 进入关怀版