大家好,我是肖恩,源码解析每周见
flask—website,是flask曾经的主站源码,使用flask制作,包含模版渲染,数据库操作,openID认证, 全文检索等功能。对于学习如何使用flask制作一个完备的web站点,很有参考价值,我们一起来学习它。
项目结构
flask-website已经归档封存,我们使用最后的版本8b08,包括如下几个模块:
模块描述run.py启动脚本websiteconfig.py设置脚本update-doc-searchindex.py更新索引脚本database.py数据库模块docs.py索引文档模块openid_auth.pyoauth认证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.pyrun.py作为项目的启动入口requirements.txt描述项目的依赖包flask_website是项目的主模块,里面包括:存放静态资源的static目录; 存放模版文件的templates目录;存放一些蓝图模块的views模块,使用这些蓝图构建网站的不同页面。网站入口
网站的入口run.py代码很简单,导入app并运行:
fromflask_websiteimportappapp.run(debug=True)
app是基于flask,使用websiteconfig中的配置进行初始化
app=Flask(__name__)app.config.from_object(‘websiteconfig’)
app中设置了一些全局实现,比如404页面定义,全局用户,关闭db连接,和模版时间:
@app.errorhandler(404)defnot_found(error):returnrender_template(‘404.html’),404@app.before_requestdefload_current_user():g.user=User.query.filter_by(openid=session[‘openid’]).first()\if’openid’insessionelseNone@app.teardown_requestdefremove_db_session(exception):db_session.remove()@app.context_processordefcurrent_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/<path:page>/’,endpoint=’docs.show’,build_only=True)app.add_url_rule(‘/docs/<version>/.latex/Flask.pdf’,endpoint=’docs.pdf’,build_only=True)
第二种是使用flask的蓝图功能:
fromflask_website.viewsimportgeneralfromflask_website.viewsimportcommunityfromflask_website.viewsimportmailinglistfromflask_website.viewsimportsnippetsfromflask_website.viewsimportextensionsapp.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_datetimeapp.jinja_env.filters[‘dateformat’]=utils.format_dateapp.jinja_env.filters[‘timedeltaformat’]=utils.format_timedeltaapp.jinja_env.filters[‘displayopenid’]=utils.display_openid模版渲染
现在主流的站点都是采用前后端分离的结构,后端提供纯粹的API,前端使用vue等构建。这种结构对于构建小型站点,会比较复杂,有牛刀杀鸡的感觉。对个人开发者,还需要学习更多的前端知识。而使用后端的模版渲染方式构建页面,是比较传统的方式,对小型站点比较实用。
本项目就是使用模版构建,在general蓝图中:
mod=Blueprint(‘general’,__name__)@mod.route(‘/’)defindex():ifrequest_wants_json():returnjsonify(releases=[r.to_json()forrinreleases])returnrender_template(‘general/index.html’,latest_release=releases[-1],#pdflinkdoesnotredirect,needsversion#docsversiononlyincludesmajor.minordocs_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块的定义细节:
<!doctypehtml>{%blockhead%}<title>{%blocktitle%}Welcome{%endblock%}|Flask(APythonMicroframework)</title><metacharset=utf-8><linkrel=stylesheettype=text/csshref=”{{url_for(‘static’,filename=’style.css’)}}”><linkrel=”shortcuticon”href=”{{url_for(‘static’,filename=’favicon.ico’)}}”><scripttype=text/javascriptsrc=”http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js”></script>{%endblock%}<divclass=box>…<pclass=nav><ahref=”{{url_for(‘general.index’)}}”>overview</a>//<ahref=”{{url_for(‘docs.index’)}}”>docs</a>//<ahref=”{{url_for(‘community.index’)}}”>community</a>//<ahref=”{{url_for(‘extensions.index’)}}”>extensions</a>//<ahref=”https://psfmember.org/civicrm/contribute/transact?reset=1&id=20″>donate</a>{%formessageinget_flashed_messages()%}<pclass=message>{{message}}{%endfor%}…
本项目首页的general/index继承自全局的layout,并对其中的body部分进行覆盖,使用自己的配置:
{%extends”layout.html”%}….{%blockbody%}<ul><li><ahref=”{{latest_release.detail_url}}”>Downloadlatestrelease</a>({{latest_release.version}})<li><ahref=”{{url_for(‘docs.index’)}}”>Readthedocumentation</a><li><ahref=”{{url_for(‘mailinglist.index’)}}”>Jointhemailinglist</a><li><ahref=https://github.com/pallets/flask>Forkitongithub</a><li><ahref=https://github.com/pallets/flask/issues>Addissuesandfeaturerequests</a></ul>…这个列表主要使用了蓝图中传入的latest_release变量,展示最新文档(pdf)的url数据库操作
网站有交互,必定要持久化数据。本项目使用的sqlite的数据库,比较轻量级。数据库使用sqlalchemy封装的ORM实现。下面的代码展示了如何创建一个评论:
@mod.route(‘/comments/<int:id>/’,methods=[‘GET’,’POST’])@requires_admindefedit_comment(id):comment=Comment.query.get(id)snippet=comment.snippetform=dict(title=comment.title,text=comment.text)ifrequest.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’Commentwasupdated.’)returnredirect(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))#初始化definit_db():Model.metadata.create_all(bind=engine)#定义基础模型Model=declarative_base(name=’Model’)Model.query=db_session.query_property()
Comment数据模型定义:
classComment(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=snippetself.author=authorself.title=titleself.text=textself.pub_date=datetime.utcnow()defto_json(self):returndict(author=self.author.to_json(),title=self.title,pub_date=http_date(self.pub_date),text=unicode(self.rendered_text))@propertydefrendered_text(self):fromflask_website.utilsimportformat_creolereturnformat_creole(self.text)
Comment模型按照结构化的方式定义了表名,6个字段,2个关联关系和json化和文本化的展示方法。
sqlalchemy的使用,在之前的文章中有过介绍,本文就不再赘述。
openID认证
一个小众的网站,构建自己的账号即麻烦也不安全,使用第三方的用户体系会比较合适。本项目使用的是Flask-OpenID这个库提供的optnID登录认证。
用户登录的时候,会根据用户选择的三方登录站点,跳转到对应的网站进行认证:
@mod.route(‘/login/’,methods=[‘GET’,’POST’])@oid.loginhandlerdeflogin():..openid=request.values.get(‘openid’)ifnotopenid:openid=COMMON_PROVIDERS.get(request.args.get(‘provider’))ifopenid:returnoid.try_login(openid,ask_for=[‘fullname’,’nickname’])..
从对应的模版上更容易理解这个过程, 可以看到默认支持AOL/Google/Yahoo三个账号体系认证:
{%blockbody%}<formaction=””method=post><p>Forsomeofthefeaturesonthissite(suchascreatingsnippetsoraddingcomments)youhavetobesignedin.Youdon’tneedtocreateanaccountonthiswebsite,justsigninwithanexisting<ahref=http://openid.net/>OpenID</a>account.<p>OpenIDURL:<inputtype=textname=openidclass=openidsize=30><inputtype=hiddenname=nextvalue=”{{next}}”><inputtype=submitvalue=Login><p>Alternativelyyoucandirectlysigninbyclickingononeoftheprovidershereincaseyoudon’tknowtheidentityURL:<ul><li><ahref=?provider=aol>AOL</a><li><ahref=?provider=google>Google</a><li><ahref=?provider=yahoo>Yahoo</a></ul></form>{%endblock%}
在三方站点认证完成后,会建立本站点的用户和openid的绑定关系:
@mod.route(‘/first-login/’,methods=[‘GET’,’POST’])deffirst_login():…db_session.add(User(request.form[‘name’],session[‘openid’]))db_session.commit()flash(u’Successfullycreatedprofileandloggedin’)…session中的openid是第三方登录成功后写入session
三方登录的逻辑过程大概就如上所示,先去三方平台登录,然后和本地站点的账号进行关联。其具体的实现,主要依赖Flask-OpenID这个模块, 我们大概了解即可。
全文检索
全文检索对于一个站点非常重要,可以帮助用户在网站上快速找到适合的内容。本项目展示了使用whoosh这个纯python实现的全文检索工具,构建网站内容检索,和使用ElasticSearch这样大型的检索库不一样。总之,本项目使用的都是小型工具,纯python实现。
全文检索从/search/入口进入:
@mod.route(‘/search/’)defsearch():q=request.args.get(‘q’)or”page=request.args.get(‘page’,type=int)or1results=Noneifq:results=perform_search(q,page=page)ifresultsisNone:abort(404)returnrender_template(‘general/search.html’,results=results,q=q)q是搜素的关键字,page是翻页的页数使用perform_search方法对索引进行查询如果找不到内容展示404;如果找到内容,展示结果
在search模块中提供了search方法,前面调用的perform_search函数是其别名:
defsearch(query,page=1,per_page=20):withindex.searcher()ass:qp=qparser.MultifieldParser([‘title’,’content’],index.schema)q=qp.parse(unicode(query))try:result_page=s.search_page(q,page,pagelen=per_page)exceptValueError:ifpage==1:returnSearchResultPage(None,page)returnNoneresults=result_page.resultsresults.highlighter.fragmenter.maxchars=512results.highlighter.fragmenter.surround=40results.highlighter.formatter=highlight.HtmlFormatter(’em’,classname=’search-match’,termclass=’search-term’,between=u'<span class=ellipsis>…</span>’)returnSearchResultPage(result_page,page)从ttile和content中搜素关键字q设置使用unicode编码将检索结果封装成SearchResultPage
重点在index.searcher()这个索引, 它使用下面方法构建:
fromwhooshimporthighlight,analysis,qparserfromwhoosh.support.charsetimportaccent_map…defopen_index():fromwhooshimportindex,fieldsasfifos.path.isdir(app.config[‘WHOOSH_INDEX’]):returnindex.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))returnindex.create_in(app.config[‘WHOOSH_INDEX’],schema)index=open_index()whoosh创建本地的索引文件whoosh构建搜素的数据结构,包括url,title,,关键字和内容关键字和内容参与检索
索引需要构建和刷新:
defupdate_documentation_index():fromflask_website.docsimportDocumentationPagewriter=index.writer()forpageinDocumentationPage.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’)classDocumentationPage(Indexable):search_document_kind=’documentation’def__init__(self,slug):self.slug=slugfn=os.path.join(app.config[‘DOCUMENTATION_PATH’],slug,’index.html’)withopen(fn)asf: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”)@classmethoddefiter_pages(cls):base_folder=os.path.abspath(app.config[‘DOCUMENTATION_PATH’])fordirpath,dirnames,filenamesinos.walk(base_folder):if’index.html’infilenames:slug=dirpath[len(base_folder) 1:]#skiptheindexpage.uselessifslug:yieldDocumentationPage(slug)文档读取DOCUMENTATION_PATH目录下的源文件(项目文档)读取文件的标题和文本,构建索引文件小结
本文我们走马观花的查看了flask-view这个flask曾经的主站。虽然没有深入太多细节,但是我们知道了模版渲染,数据库操作,OpenID认证和全文检索四个功能的实现方式,建立了相关技术的索引。如果我们需要构建自己的小型web项目,比如博客,完全可以以这个项目为基础,修改实现。
经过数周的调整,接下我们开始进入python影响力巨大的项目之一: Django。敬请期待。
小技巧
本项目提供了2个非常实用的小技巧。第1个是json化和html化输出,这样用户可以自由选择输出方式,同时站点也可以构建纯API的接口。这个功能是使用下面的request_wants_json函数提供:
defrequest_wants_json():#weonlyacceptjsonifthequalityofjsonisgreaterthanthe#qualityoftext/htmlbecausetext/htmlispreferredtosupport#browsersthataccepton*/*best=request.accept_mimetypes\.best_match([‘application/json’,’text/html’])returnbest==’application/json’and\request.accept_mimetypes[best]>request.accept_mimetypes[‘text/html’]
request_wants_json函数中判断头部的mime类型,进行根据是application/json还是text/html决定展示方式。
第2个小技巧是认证装饰器, 前面一个是登录验证,后一个是超级管理认证:
defrequires_login(f):@wraps(f)defdecorated_function(*args,**kwargs):ifg.userisNone:flash(u’Youneedtobesignedinforthispage.’)returnredirect(url_for(‘general.login’,next=request.path))returnf(*args,**kwargs)returndecorated_functiondefrequires_admin(f):@wraps(f)defdecorated_function(*args,**kwargs):ifnotg.user.is_admin:abort(401)returnf(*args,**kwargs)returnrequires_login(decorated_function)
@mod.route(‘/edit/<int:id>/’,methods=[‘GET’,’POST’])@requires_logindefedit(id):…@mod.route(‘/comments/<int:id>/’,methods=[‘GET’,’POST’])@requires_admindefedit_comment(id):…参考链接https://github.com/pallets/flask-website
还不过瘾?试试它们
▲Python 的八个实用的“无代码”特性
▲Python猫 2021 文章小结,翻译竟比原创多!
▲我私藏的那些实用的终端命令行工具
▲Java 之父 James Gosling 聊编程语言设计
▲收藏了 30 实用的 Python 办公自动化库!
▲Python 操作 Redis 必备神器:redis-py 源码阅读