这本书已经覆盖了使用Flask创建web应用需要知道的所有东西。但还是有很多需要你自己去探索。最后一章,我们将讲述额外一些小节,如果有必要的话,他们可以被添加进应用。
这一章,将包含下面内容:
这一章,我们首先将学习如何使用Whoosh和Elasticsearch进行全文搜索。全文搜索对于提供大量内容和选项的Web应用程序(如电子商务网站)非常重要。接下来我们将捕捉信号,这些信号是在应用里某些操作执行时被发送的。然后为我们的Flask应用实现缓存。
我们同样将会看到应用如何支持发送e-mail。然后将看到如何实现应用异步。通常,WSGI应用是同步和阻塞的,不能同时处理多个同步请求。我们将看到如何通过一个简单的例子解决这个问题。我们还会集成Celery到我们的应用,看一个任务队列对应用带来的好处。
Whoosh是使用Python完成的一个快速全文索引和搜索库。它是一个完全Pythonic的API,使得开发者为他们的应用增加搜索功能非常容易和高效。这一节,我们将使用一个叫做Flask-WhooshAlchemy的包,它集成了Whoosh文本搜索功能和SQLAlchemy,以便用于Flask应用中。
使用下面命令安装Flask-WhooshAlchemy:
$ pip install flask_whooshalchemy
它将安装需要的包和依赖。
flask_whooshalchemy不支持Python3,另外对中文支持也不好,不推荐使用,可以使用jieba。
使用SQLAlchemy集成Whoosh和Flask是非常简单的。首先,我们需要提供一个Whoosh目录的路径,这个目录下将创建模型索引。这应该在应用配置里完成,即my_app/__init__.py
:
app.config['WHOOSH_BASE'] = '/tmp/whoosh'
你可以选择任意你喜欢的路径,可以是绝对路径也可以是相对路径。
接下来,我们需要改变models.py文件,使得一些string/text字段可搜索:
import flask.ext.whooshalchemy as whooshalchemy
from my_app import app
class Product(db.Model):
__searchable__ = ['name', 'company']
# … Rest of code as before … #
whooshalchemy.whoosh_index(app, Product)
class Category(db.Model):
__searchable__ = ['name']
# … Rest of code as before … #
whooshalchemy.whoosh_index(app, Category)
注意每个模型添加的__searchable__
语句。它告诉Whoosh去创建这些字段的索引。记住这些字段应该是text或者是string类型的。whoosh_index语句告诉应用为这些模型创建索引,如果他们还不存在的话。
做好这些之后,添加一个handler使用Whoosh进行搜索。可以在views.py处理这些:
@catalog.route('/product-search-whoosh')
@catalog.route('/product-search-whoosh/')
def product_search_whoosh(page=1):
q = request.args.get('q')
products = Product.query.whoosh_search(q)
return render_template(
'products.html', products=products.paginate(page, 10)
)
这里,通过q获取URL参数,然后传递它的值到whoosh_search()方法里。这个方法将会对Product模型的name和company字段进行全文搜索。我们前面已经进行了设置,使得模型里的name和company变得可搜索了。
在第四章基于SQL搜索一节中我们实现了一个基于基本字段搜索的方法。但是在使用Whoosh情况下,搜索时我们不需要指定任何字段。我们可以输入任何字段,如何匹配上可搜索字段的话,将会返回结果,并按照相关性进行排序。
首先,在应用创建一些商品。现在,打开http://127.0.0.1:5000/product-search-whoosh?q=iPhone
,结果页将显示商品名包含iPhone的列表。
Whoosh提供了一些高级选项,我们可以控制哪些字段可以搜索或者结果是如何排序的。你可以根据应用的需要自行探索。
https://pythonhosted.org/Whoosh/
https://pypi.python.org/pypi/Flask-WhooshAlchemy
Elasticsearch是一个基于Lucene的搜索服务,是一个开源信息检索库。ElasticSearch提供了一个分布式全文搜索引擎,它具有RESTful Web接口和schema-free JSON文档。这一小节,我们将使用Elasticsearch为我们的Flask应用完成全文搜索。
我们将使用一个叫做pyelasticsearch的Python库,它使得处理Elasticsearch很容易:
$ pip install pyelasticsearch
我们同样需要安装Elasticsearch服务本身。可以从http://www.elasticsearch.org/download/
下载。解压文件,然后运行下面命令:
$ bin/elasticsearch
默认情况下,将在http://localhost:9200/
上运行Elasticsearch服务。
为了演示集成,我们将从向应用配置添加Elasticsearch开始,即my_app/__init__.py
:
from pyelasticsearch import ElasticSearch
from pyelasticsearch.exceptions import IndexAlreadyExistsError
es = ElasticSearch('http://localhost:9200/')
try:
es.create_index('catalog')
except IndexAlreadyExistsError, e:
pass
这里,我们从ElasticSearch类创建了一个es对象,它接收了服务器URL。然后创建了一个叫做catalog的索引。他们是在try-except块中处理的,因为如果索引已经存在 ,将会抛出IndexAlradyExistsError,通过捕捉异常,可以忽略这个错误。
接下来,我们需要往Elasticsearch索引里添加文档(document)。可以在视图和模型里完成这些,但是最好是将它添加在模型层。所以,我们将在models.py里完成这些:
from my_app import es
class Product(db.Model):
def add_index_to_es(self):
es.index('catalog', 'product', {
'name': self.name,
'category': self.category.name
})
es.refresh('catalog')
class Category(db.Model):
def add_index_to_es(self):
es.index('catalog', 'category', {
'name': self.name,
})
es.refresh('catalog')
这里,在每个模型里,我们添加了一个叫做add_index_to_es()的方法,这个方法将添加与当前Product或者Category对象对应的文档到catalog索引里,并伴随相关的文件类型,即product或category。最后,我们刷新索引以保证新建的索引可以被搜索到。
add_index_to_es()
方法可以在我们创建,更新,删除商品时被调用。为了演示,我们仅仅在views.py创建商品时添加了这个方法:
from my_app import es
def create_product():
#... normal product creation as always ...#
db.session.commit()
product.add_index_to_es()
#... normal process as always ...#
@catalog.route('/product-search-es')
@catalog.route('/product-search-es/')
def product_search_es(page=1):
q = request.args.get('q')
products = es.search(q)
return products
同时,添加一个product_search_es()
方法,允许在刚刚创建的Elasticsearch上进行搜索。对create_category()方法做同样的处理。
假设已经在每个类别里面创建一些商品。现在,如果打开http://127.0.0.1:5000/product-search-es?q=galaxy
,我们将看到类似于下面截图的回复:
Signals可以理解为应用里发生的事件。这些事件可以被一些特定接收方订阅,当事件发生的时候会触发一个函数。事件的发生由发送方广播,发送方可以指定接收方触发函数里可以使用的参数。
您应该避免修改信号中的任何应用程序数据,因为信号不是按指定的顺序执行的,而且很容易导致数据损坏。
我们将使用一个叫做binker的Python库,它提供了一些信号特性。Flask内建了对blinker的支持,可以很大程度上使用信号。其中一些核心信号是由Flask提供的。
这一小节,我们将使用上一小节的应用,通过信号添加额外的product和category文档(documents)到索引里。
首先,我们创建新建商品和类别时的信号。可以在models.py中处理这个。也可以在任何我们希望的文件里处理,因为信号是在全局范围创建的:
from blinker import Namespace
catalog_signals = Namespace()
product_created = catalog_signals.signal('product-created')
category_created = catalog_signals.signal('category-created')
我们使用Namespace去创建信号,因为这将在自定义命名空间创建他们,而不是在全局空间,这将有助于管理信号。我们创造了两个信号,可以从他们的名字明白他们的意思。
之后,我们需要去为这些信号创建订阅者,给他们绑定函数。为了这个目的,add_index_to_es()
需要被移除,在全局范围需要创建新的函数:
def add_product_index_to_es(sender, product):
es.index('catalog', 'product', {
'name': product.name,
'category': product.category.name
})
es.refresh('catalog')
product_created.connect(add_product_index_to_es, app)
def add_category_index_to_es(sender, category):
es.index('catalog', 'category', {
'name': category.name,
})
es.refresh('catalog')
category_created.connect(add_category_index_to_es, app)
前面的代码中,我们使用.connect()为信号创建了订阅者。这个方法接收一个函数,这个函数会在事件发生时调用。它同样接收一个发送方做为可选参数。app对象作为发送者被提供,因为,我们不希望我们的函数在任何应用任何地方触发的时候都会被调用。这在扩展的情况下尤其适用,可以被多个应用程序使用。接收方调用的函数接收发送者作为第一个参数,如果发送方没提供的话,通常情况下是None。我们提供product/category
作为第二个参数,为了将这条记录添加进Elasticsearch索引。
现在,我们仅仅需要触发可以被接收方捕捉的信号。可以在views.py处理这些。为了达到这个目的,需要移除add_index_to_es()
方法,使用.send()方法替换他们:
from my_app.catalog.models import product_created, category_created
def create_product():
#... normal product creation as always ...#
db.session.commit()
product_created.send(app, product=product)
# product.add_index_to_es()
#... normal process as always ...#
对create_category()做同样的处理。
当一个商品被创建的时候,product_created
信号被触发了,app做为发送方,商品做为关键参数。这个会在models.py中被捕捉,当add_product_index_to_es()
函数被调用的时候,会向目录索引添加文档。
https://pypi.python.org/pypi/blinker
http://flask.pocoo.org/docs/0.10/signals/#core-signals
https://pythonhosted.org/Flask-SQLAlchemy/signals.html
找到当应用程序的响应时间增加成为一个问题时,缓存成为了任何Web应用程序的重要组成部分。Flask本身默认不提供任何缓存支持,但是Werkzeug支持。Werkzeug对缓存提供了基本的支持,并可以使用多种后端(backend),比如Memcached和Redis。
我们将安装一个叫做Flask-Cache的扩展,这会在很大程度上简化缓存的使用:
$ pip install Flask-Cache
首先,需初始化Cache。将在应用配置里进行处理,my_app/__init__.py
:
from flask.ext.cache import Cache
cache = Cache(app, config={'CACHE_TYPE': 'simple'})
这里,使用simple做为Cache的类型,缓存存储在内存里。不建议在生产环境这样做。对于生产环境,需使用Redis,Memcached,文件系统等等这些。Flask-Cache对这些全部支持。
接下来,需在方法里增加缓存;这是非常容易实现的。我们仅仅需要对视图方法增加@cache.cached(timeout=)装饰器。列出所有的商品类别可以这样做(在views.py中处理):
from my_app import cache
@catalog.route('/categories')
@cache.cached(timeout=120)
def categories():
# Fetch and display the list of categories
这种缓存方式以键值对的形式进行存储,键为请求路径,值为这个方法的输出值。
在添加了前面的代码后,检查缓存是否生效。首先访问http://127.0.0.1:5000/categories
获取类别列表。在将会在缓存里为这个URL存储一个键值对。现在,快速的创建一个新的类别,然后再次访问商品类别列表页面。你将看到新建的类别没有被列出来。等几分钟,然后刷新页面。新的类别才会被展示出来。这是因为第一次访问的时候类别列表被缓存了,有效时间是2分钟,即120秒。
这可能看起来像是应用的一个错误,但是在大型应用程序中,减少对数据库的访问次数是一种恩惠,并且整个应用程序体验也会得到改善。缓存通常用于那些结果不经常更新的处理程序。
我们中的许多人可能认为,在单个类别或产品页面中,这种缓存将失败,因为每个记录都有一个单独的页面。解决这个问题的方法是memoization。当一个方法用同样参数进行多次调用时,结果应该是从缓存中加载而不是访问数据库。实现memoization非常简单:
@catalog.route('/product/')
@cache.memoize(120)
def product(id):
# Fetch and display the product
现在,如果我们在浏览器输入http://127.0.0.1:5000/product/1
,第一次请求将从数据库读取数据。但是,第二次,如果做相同的访问,将从缓存加载数据。如果我们打开另一个商品页面http://127.0.0.1:5000/product/2
,将从数据库获取商品细节。
https://pythonhosted.org/Flask-Cache/
http://en.wikipedia.org/wiki/Memoization
发送邮件功能是任何web应用最基础功能中的一个。基于Python的应用,可以使用smptblib非常容易的完成这一功能。在Flask中,使用Flask-Mail扩展更加简化了这一过程。
Flask-Mail通过pip安装:
$ pip install Flask-Mail
让我们以一个简单的例子为例,每当添加新类别时,将向应用管理者发送一封e-mail。
首先,需要在应用配置里实例化Mail对象,即my_app/__init__.py
:
from flask_mail import Mail
app.config['MAIL_SERVER'] = 'smtp.gmail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = 'gmail_username'
app.config['MAIL_PASSWORD'] = 'gmail_password'
app.config['MAIL_DEFAULT_SENDER'] = ('Sender name', 'sender email')
mail = Mail(app)
同时,我们需一些配置去创建e-mail服务和发送者账号。前面的代码是一个配置Gmail账户的简单示例。任何SMTP服务可以像这样建立。还有一些其他选项可以选择,可以参见Flask-Mail文档https://pythonhosted.org/Flask-Mail
。
在类别创建的时候发送e-mail,我们需要对view.py做下面修改:
from my_app import mail
from flask_mail import Message
@catalog.route('/category-create', methods=['GET', 'POST'])
def create_category():
# … Create category … #
db.session.commit()
message = Message(
"New category added",
recipients=['[email protected]']
)
message.body = 'New category "%s" has been created' % category.name
mail.send(message)
# … Rest of the process … #
这里,一个新的e-mail将从我们创建的发送方发送给接收列表方。
现在,假设需要发送一封非常大的邮件,包含非常多的HTML文本。将这些全都写在Python文件里会使得代码丑陋和难以管理。一个简单的方法是创建模板,然后在发送邮件的时候渲染。我创建了两个模板,一个是HTML文本,一个纯文本。
category-create-email-text.html模板看起来像这样:
A new category has been added to the catalog.
The name of the category is {{ category.name }}.
Click on the URL below to access the same:
{{ url_for('catalog.category', id=category.id, _external = True) }}
This is an automated email. Do not reply to it.
category-create-email-html.html模板看起来像这样:
A new category has been added to the catalog.
The name of the category is
{{ category.name }}
.
This is an automated email. Do not reply to it.
之后,我们需修改之前在views.py里创建e-mail的代码;
message.body = render_template(
"category-create-email-text.html", category=category
)
message.html = render_template(
"category-create-email-html.html", category=category
)
应用中有一些操作可能是耗时的,会使用应用变慢,即使这并不是真正意义上的慢。但这降低了用户体验。为了解决这个问题,最简单的方式是使用线程进行异步操作。这一小节,我们将使用Python的thread和threading库来完成这一功能。threading库是thread的一个简单接口;它提供了更多功能和隐藏了用户不常使用的一些东西。
我们将使用上一小节的应用代码。我们中的许多人可能会注意到当邮件在发送的时候,应用在等待这一过程完成,事实上这是不必要的。E-mail发送可以在后台处理,这样我们的应用对用户来说响应就变得及时。
使用thread库处理异步是非常简单。仅仅需要在views.py里增加这些代码:
import thread
def send_mail(message):
with app.app_context():
mail.send(message)
# Replace the line below in create_category()
#mail.send(message)
# by
thread.start_new_thread(send_mail, (message,))
你可以看到,在一个新的线程里发生邮件,这个线程接收一个叫message的参数。我们需要去创建一个新的send_mail()
方法,因为我们的e-mail模板包含url_for
,所以send_mail
方法仅能运行在应用上下文中,默认情况下在一个新建的线程里不可用。
同时,发送e-mail同样可以使用threading库:
from threading import Thread
# Replace the previously added line in create_category() by
new_thread = Thread(target=send_mail, args=[message])
new_thread.start()
实际上,效果和前面一样,但是threading库提供了在需要的时候启动线程的灵活性,而不是同时创建和启动线程。
很容易观察上面代码的效果。比较这个应用的执行效果和前一小节的应用效果。你会发现这个应用响应性更强。其他一种方式是监测日志输出,在e-mail发送之前,新建的类别页面将会加载。
Celery是为Python准备的任务队列。早期的时候有一个扩展是集成了Flask和Celery,但是在Celery3.0的时候,这个扩展被废弃了。现在,Celery可以直接在Flask里使用,而仅仅需做一些配置。前面小节,我们实现了异步发送邮件,这一小节将使用Celery完成同样的功能。
安装Celery:
$ pip install celery
在Flask下使用Celery,我们仅仅需要修改一点Flask app配置。这里,使用Redis做为代理(broker)。
我们将使用前一小节的应用,然后使用Celery完成它。
首先要做的是,在应用配置文件里做一些配置,即my_app/__init__.py
:
from celery import Celery
app.config.update(
CELERY_BROKER_URL='redis://localhost:6379',
CELERY_RESULT_BACKEND='redis://localhost:6379'
)
def make_celery(app):
celery = Celery(
app.import_name, broker=app.config['CELERY_BROKER_URL']
)
celery.conf.update(app.config)
TaskBase = celery.Task
class ContextTask(TaskBase):
abstract = True
def __call__(self, *args, **kwargs):
with app.app_context():
return TaskBase.__call__(self, *args, **kwargs)
celery.Task = ContextTask
return celery
前面的代码直接来自于Flask网站,大部分情况下可以在应用里这样使用:
celery = make_celery(app)
运行Celery进程,需执行下面命令:
$ celery worker -b redis://localhost:6379 --app=my_app.celery -l INFO
确保Redis运行在broker URL上,如配置中所指定的那样。
这里,-b指定了broker,-app指定了在配置文件里创建的celery对象。
现在,仅仅需要在views.py里使用celery对象去异步发送邮件:
from my_app import celery
@celery.task()
def send_mail(message):
with app.app_context():
mail.send(message)
# Add this line wherever the email needs to be sent
send_mail.apply_async((message,))
如果我们希望一个方法作为Celery任务运行只需增加@celery.task装饰器即可。Celery进程会自动检测到这些方法。
现在,我们创建了一个商品,并且一封邮件发送了,我们可以在Celery进程日志里看到一个任务正在运行,看起来像这样:
[2014-08-28 01:16:47,365: INFO/MainProcess] Received task: my_app.catalog.views.send_mail[d2ca07ae-6b47-4b76-9935-17b826cdc340]
[2014-08-28 01:16:55,695: INFO/MainProcess] Task my_app.catalog.views.send_mail[d2ca07ae-6b47-4b76-9935-17b826cdc340] succeeded in 8.329121886s: None
http://docs.celeryproject.org/en/latest/index.html