400-915-1135
学习Flask主站源码,构建自己的Web站点(自己建网站花钱吗)自己建造网站,

学习Flask主站源码,构建自己的Web站点(自己建网站花钱吗)自己建造网站,

发表日期:2023-03-06 10:01:26   作者来源:众诚企业建站   浏览:176

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

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.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/<path:page>/, endpoint=docs.show

,

                 build_only=True)

app.add_url_rule(/docs/<version>/.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)

<link rel=stylesheet type=text/css href="{{ url_for(static, filename=style.css) }}"

>

<link rel="shortcut icon" href="{{ url_for(static, filename=favicon.ico) }}"

>

<script type

=text/javascript

  src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"

>

{% endblock %}

  ...

    <a href="{{ url_for(general.index) }}"

>overview //

    <a href="{{ url_for(docs.index) }}"

>docs //

    <a href="{{ url_for(community.index) }}"

>community //

    <a href="{{ url_for(extensions.index) }}"

>extensions //

    <a href="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 %}

    <li><a href="{{ latest_release.detail_url }}"

>Download latest release ({{ latest_release.version }})

    <li><a href="{{ url_for(docs.index) }}"

>Read the documentation

    <li><a href="{{ 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/<int:id>/, methods=[GETPOST

    ])

    @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=[GETPOST

    ])

    @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=[fullnamenickname

    ])

        ..

    从对应的模版上更容易理解这个过程, 可以看到默认支持AOL/Google/Yahoo三个账号体系认证:

    {% block body %}

      <form action=""

     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=[GETPOST

    ])

    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(pagetype

    =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([titlecontent

    ], 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<span class=ellipsis> … </span>

    )

            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/jsontext/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/<int:id>/, methods=[GETPOST

    ])

    @requires_login

    def edit(id):

        ...

    @mod.route(/comments/<int:id>/, methods=[GETPOST

    ])

    @requires_admin

    def edit_comment(id):

        ...

    参考链接

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

    还不过瘾?试试它们

    Python 的八个实用的“无代码”特性

    Python猫 2021 文章小结,翻译竟比原创多!

    我私藏的那些实用的终端命令行工具

    Java 之父 James Gosling 聊编程语言设计

    收藏了 30+ 实用的 Python 办公自动化库!

    Python 操作 Redis 必备神器:redis-py 源码阅读

    如果你觉得本文有帮助请慷慨分享点赞,感谢啦