學(xué)習(xí)Flask主站源碼,原來可以這樣學(xué)!

flask—website,是flask曾經(jīng)的主站源碼,使用flask制作,包含模版渲染,數(shù)據(jù)庫操作,openID認(rèn)證, 全文檢索等功能。對(duì)于學(xué)習(xí)如何使用flask制作一個(gè)完備的web站點(diǎn),很有參考價(jià)值,我們一起來學(xué)習(xí)它。
項(xiàng)目結(jié)構(gòu)
flask-website已經(jīng)歸檔封存,我們使用最后的版本8b08,包括如下幾個(gè)模塊:
| 模塊 | 描述 |
|---|---|
| run.py | 啟動(dòng)腳本 |
| websiteconfig.py | 設(shè)置腳本 |
| update-doc-searchindex.py | 更新索引腳本 |
| database.py | 數(shù)據(jù)庫模塊 |
| docs.py | 索引文檔模塊 |
| openid_auth.py | oauth認(rèn)證 |
| search.py | 搜素模塊 |
| utils.py | 工具類 |
| listings | 一些展示欄 |
| views | 藍(lán)圖模塊,包括社區(qū),擴(kuò)展,郵件列表,代碼片段等 |
| static | 網(wǎng)站的靜態(tài)資源 |
| templates | 網(wǎng)站的模版資源 |
flask-website的項(xiàng)目結(jié)構(gòu),可以作為flask的腳手架,按照這個(gè)目錄規(guī)劃構(gòu)建自己的站點(diǎn):
.
├──?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作為項(xiàng)目的啟動(dòng)入口 requirements.txt描述項(xiàng)目的依賴包 flask_website是項(xiàng)目的主模塊,里面包括:存放靜態(tài)資源的static目錄; 存放模版文件的templates目錄;存放一些藍(lán)圖模塊的views模塊,使用這些藍(lán)圖構(gòu)建網(wǎng)站的不同頁面。
網(wǎng)站入口
網(wǎng)站的入口run.py代碼很簡(jiǎn)單,導(dǎo)入app并運(yùn)行:
from?flask_website?import?app
app.run(debug=True)
app是基于flask,使用websiteconfig中的配置進(jìn)行初始化
app?=?Flask(__name__)
app.config.from_object('websiteconfig')
app中設(shè)置了一些全局實(shí)現(xiàn),比如404頁面定義,全局用戶,關(guān)閉db連接,和模版時(shí)間:
@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函數(shù),設(shè)置了文檔的搜索實(shí)現(xiàn),這些url執(zhí)行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的藍(lán)圖功能:
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模版的工具函數(shù):
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
模版渲染
現(xiàn)在主流的站點(diǎn)都是采用前后端分離的結(jié)構(gòu),后端提供純粹的API,前端使用vue等構(gòu)建。這種結(jié)構(gòu)對(duì)于構(gòu)建小型站點(diǎn),會(huì)比較復(fù)雜,有牛刀殺雞的感覺。對(duì)個(gè)人開發(fā)者,還需要學(xué)習(xí)更多的前端知識(shí)。而使用后端的模版渲染方式構(gòu)建頁面,是比較傳統(tǒng)的方式,對(duì)小型站點(diǎn)比較實(shí)用。
本項(xiàng)目就是使用模版構(gòu)建,在general藍(lán)圖中:
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方式輸出,我們重點(diǎn)看看第二種方式。函數(shù)render_template傳遞了模版路徑,latest_release和docs_pdf_version兩個(gè)變量值。
模版也是模塊化的,一般是根據(jù)頁面布局而來。比如分成左右兩欄的結(jié)構(gòu),或者上下結(jié)構(gòu),布局定義的模版一般叫做layout。比如本項(xiàng)目的模版就從上至下定義成下面5塊:
head 一般定義html頁面標(biāo)題(瀏覽器欄),css樣式/js-script的按需加載等 body_title 定義頁面的標(biāo)題 message 定義一些統(tǒng)一的通知,提示類的展示空間 body 頁面的正文部分 footer 統(tǒng)一的頁腳
使用layout模版定義,將網(wǎng)站的展示風(fēng)格統(tǒng)一下來,各個(gè)頁面可以繼承和擴(kuò)展。下面是head塊和message塊的定義細(xì)節(jié):
{%?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?%}
??...
本項(xiàng)目首頁的general/index繼承自全局的layout,并對(duì)其中的body部分進(jìn)行覆蓋,使用自己的配置:
{%?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
??
??...
這個(gè)列表主要使用了藍(lán)圖中傳入的latest_release變量,展示最新文檔(pdf)的url
數(shù)據(jù)庫操作
網(wǎng)站有交互,必定要持久化數(shù)據(jù)。本項(xiàng)目使用的sqlite的數(shù)據(jù)庫,比較輕量級(jí)。數(shù)據(jù)庫使用sqlalchemy封裝的ORM實(shí)現(xiàn)。下面的代碼展示了如何創(chuàng)建一個(gè)評(píng)論:
@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)
????...
創(chuàng)建comment對(duì)象 從html的form表單中獲取用戶提交的title和text 對(duì)comment對(duì)象進(jìn)行賦值和提交 刷新頁面的提示信息(在模版的message部分展示) 返回到新的url
借助sqlalchemy,數(shù)據(jù)模型的操作API簡(jiǎn)單易懂。要使用數(shù)據(jù)庫,需要先創(chuàng)建數(shù)據(jù)庫連接,構(gòu)建模型等, 主要在database模塊:
DATABASE_URI?=?'sqlite:///'?+?os.path.join(_basedir,?'flask-website.db')
#?創(chuàng)建引擎
engine?=?create_engine(app.config['DATABASE_URI'],
???????????????????????convert_unicode=True,
???????????????????????**app.config['DATABASE_CONNECT_OPTIONS'])
#?創(chuàng)建session(連接)????????????????
db_session?=?scoped_session(sessionmaker(autocommit=False,
?????????????????????????????????????????autoflush=False,
?????????????????????????????????????????bind=engine))
#?初始化
def?init_db():
????Model.metadata.create_all(bind=engine)
#?定義基礎(chǔ)模型
Model?=?declarative_base(name='Model')
Model.query?=?db_session.query_property()
Comment數(shù)據(jù)模型定義:
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模型按照結(jié)構(gòu)化的方式定義了表名,6個(gè)字段,2個(gè)關(guān)聯(lián)關(guān)系和json化和文本化的展示方法。
sqlalchemy的使用,在之前的文章中有過介紹,本文就不再贅述。
openID認(rèn)證
一個(gè)小眾的網(wǎng)站,構(gòu)建自己的賬號(hào)即麻煩也不安全,使用第三方的用戶體系會(huì)比較合適。本項(xiàng)目使用的是Flask-OpenID這個(gè)庫提供的optnID登錄認(rèn)證。
用戶登錄的時(shí)候,會(huì)根據(jù)用戶選擇的三方登錄站點(diǎn),跳轉(zhuǎn)到對(duì)應(yīng)的網(wǎng)站進(jìn)行認(rèn)證:
@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'])
????..
從對(duì)應(yīng)的模版上更容易理解這個(gè)過程, 可以看到默認(rèn)支持AOL/Google/Yahoo三個(gè)賬號(hào)體系認(rèn)證:
{%?block?body?%}
??
{%?endblock?%}
在三方站點(diǎn)認(rèn)證完成后,會(huì)建立本站點(diǎn)的用戶和openid的綁定關(guān)系:
@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
三方登錄的邏輯過程大概就如上所示,先去三方平臺(tái)登錄,然后和本地站點(diǎn)的賬號(hào)進(jìn)行關(guān)聯(lián)。其具體的實(shí)現(xiàn),主要依賴Flask-OpenID這個(gè)模塊, 我們大概了解即可。
全文檢索
全文檢索對(duì)于一個(gè)站點(diǎn)非常重要,可以幫助用戶在網(wǎng)站上快速找到適合的內(nèi)容。本項(xiàng)目展示了使用whoosh這個(gè)純python實(shí)現(xiàn)的全文檢索工具,構(gòu)建網(wǎng)站內(nèi)容檢索,和使用ElasticSearch這樣大型的檢索庫不一樣??傊?,本項(xiàng)目使用的都是小型工具,純python實(shí)現(xiàn)。
全文檢索從/search/入口進(jìn)入:
@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是搜素的關(guān)鍵字,page是翻頁的頁數(shù) 使用perform_search方法對(duì)索引進(jìn)行查詢 如果找不到內(nèi)容展示404;如果找到內(nèi)容,展示結(jié)果
在search模塊中提供了search方法,前面調(diào)用的perform_search函數(shù)是其別名:
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中搜素關(guān)鍵字q 設(shè)置使用unicode編碼 將檢索結(jié)果封裝成SearchResultPage
重點(diǎn)在index.searcher()這個(gè)索引, 它使用下面方法構(gòu)建:
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創(chuàng)建本地的索引文件 whoosh構(gòu)建搜素的數(shù)據(jù)結(jié)構(gòu),包括url,title,,關(guān)鍵字和內(nèi)容 關(guān)鍵字和內(nèi)容參與檢索
索引需要構(gòu)建和刷新:
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()
文檔索引構(gòu)建在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目錄下的源文件(項(xiàng)目文檔) 讀取文件的標(biāo)題和文本,構(gòu)建索引文件
小結(jié)
本文我們走馬觀花的查看了flask-view這個(gè)flask曾經(jīng)的主站。雖然沒有深入太多細(xì)節(jié),但是我們知道了模版渲染,數(shù)據(jù)庫操作,OpenID認(rèn)證和全文檢索四個(gè)功能的實(shí)現(xiàn)方式,建立了相關(guān)技術(shù)的索引。如果我們需要構(gòu)建自己的小型web項(xiàng)目,比如博客,完全可以以這個(gè)項(xiàng)目為基礎(chǔ),修改實(shí)現(xiàn)。
經(jīng)過數(shù)周的調(diào)整,接下我們開始進(jìn)入python影響力巨大的項(xiàng)目之一: Django。敬請(qǐng)期待。
小技巧
本項(xiàng)目提供了2個(gè)非常實(shí)用的小技巧。第1個(gè)是json化和html化輸出,這樣用戶可以自由選擇輸出方式,同時(shí)站點(diǎn)也可以構(gòu)建純API的接口。這個(gè)功能是使用下面的request_wants_json函數(shù)提供:
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函數(shù)中判斷頭部的mime類型,進(jìn)行根據(jù)是application/json還是text/html決定展示方式。
第2個(gè)小技巧是認(rèn)證裝飾器, 前面一個(gè)是登錄驗(yàn)證,后一個(gè)是超級(jí)管理認(rèn)證:
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)
這兩個(gè)裝飾器,在view的API上使用, 比如編輯snippet需要登錄,評(píng)論需要管理員權(quán)限:
@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

推薦閱讀:
入門:?最全的零基礎(chǔ)學(xué)Python的問題? |?零基礎(chǔ)學(xué)了8個(gè)月的Python??|?實(shí)戰(zhàn)項(xiàng)目?|學(xué)Python就是這條捷徑
干貨:爬取豆瓣短評(píng),電影《后來的我們》?|?38年NBA最佳球員分析?|? ?從萬眾期待到口碑撲街!唐探3令人失望? |?笑看新倚天屠龍記?|?燈謎答題王?|用Python做個(gè)海量小姐姐素描圖?|碟中諜這么火,我用機(jī)器學(xué)習(xí)做個(gè)迷你推薦系統(tǒng)電影
趣味:彈球游戲? |?九宮格? |?漂亮的花?|?兩百行Python《天天酷跑》游戲!
AI:?會(huì)做詩的機(jī)器人?|?給圖片上色?|?預(yù)測(cè)收入?|?碟中諜這么火,我用機(jī)器學(xué)習(xí)做個(gè)迷你推薦系統(tǒng)電影
小工具:?Pdf轉(zhuǎn)Word,輕松搞定表格和水??!?|?一鍵把html網(wǎng)頁保存為pdf!|??再見PDF提取收費(fèi)!?|?用90行代碼打造最強(qiáng)PDF轉(zhuǎn)換器,word、PPT、excel、markdown、html一鍵轉(zhuǎn)換?|?制作一款釘釘?shù)蛢r(jià)機(jī)票提示器!?|60行代碼做了一個(gè)語音壁紙切換器天天看小姐姐!|
年度爆款文案
點(diǎn)閱讀原文,看原創(chuàng)200個(gè)趣味案例!
