最近在Flask Web Development作者博客看到第二版Flask Mega-Tutorial已在2017年底更新,现翻译给大家参考,希望帮助大家学习flask。
这是Flask Mega-Tutorial系列的第十四章,其中我将使用Microsoft翻译服务和一点点JavaScript添加实时语言翻译功能。
供您参考,以下是本系列文章的列表。
- 第1章:Hello, World!
- 第2章:模板
- 第3章:Web表单
- 第4章:数据库
- 第5章:用户登录
- 第6章:配置文件页面和头像
- 第7章:错误处理
- 第8章:关注与被关注
- 第9章:分页
- 第10章:电子邮件支持
- 第11章:整容
- 第12章:日期和时间
- 第13章:I18n和L10n
- 第14章:Ajax(本文)
- 第15章:大型应用程序结构
- 第16章:全文搜索
- 第17章:在Linux上部署
- 第18章:在Heroku上部署
- 第19章:Docker容器上的部署
- 第20章:一些JavaScript Magic
- 第21章:用户通知
- 第22章:后台工作
- 第23章:应用程序编程接口(API)
注意1:如果您正在寻找本教程的旧版本,请在此处。
注意2:如果您想在此博客上支持我的工作,或者只是没有耐心等待每周的文章,我将提供完整的本教程版本,打包成电子书或视频集。欲了解更多信息,请访问courses.miguelgrinberg.com。
在本文中,我将偏离服务器端开发的“安全区域”,并致力于具有同等重要的服务器和客户端组件的功能。您是否看到某些网站在用户生成的内容旁边显示的“翻译”链接?这些链接可触发实时自动翻译非用户本国语言的内容。通常将翻译后的内容插入原始版本的下方。Google将其显示为外语搜索结果。Facebook将其发布。Twitter将其用于推文。今天,我将向您展示如何向Microblog添加相同的功能!
服务器端与客户端
到目前为止,在我遵循的传统服务器端模型中,有一个客户端(由用户命令的Web浏览器)向应用程序服务器发出HTTP请求。请求可以简单地要求一个HTML页面,例如单击“个人主页”链接时,也可以触发操作,例如在编辑你的个人信息之后单击提交按钮。在这两种类型的请求中,服务器通过直接发送新的网页或通过发送重定向来完成请求。 然后客户端用新的页面替换当前页面。 只要用户停留在应用的网站上,该周期就会重复。 在这种模式下,服务器完成所有工作,而客户端只显示网页并接受用户输入。
在另一种模式中,客户端扮演着更积极的角色。在此模式中,客户端向服务器发出一个请求,并且服务器响应一个页面,但是与前一种情况不同,并非所有页面数据都是HTML,页面中也有部分代码,通常用Javascript编写。一旦客户端收到页面后,它将显示HTML部分,并执行代码。从那时起,您将拥有一个可以独立工作的活动客户端,可以在不与服务器接触很少或没有接触的情况下自行完成工作。在严格的客户端应用程序中,整个应用程序会在初始页面请求的情况下下载到客户端,然后该应用完全在客户端上运行,只有在查询或者变更数据时才与服务器联系。这种类型的应用称为单页应用程序或SPAs。
大多数应用是这两种模式之间的混合,并且结合了两种技术特点。我的Microblog应用主要是服务器端应用,但是今天我将向它添加一些客户端操作。为了实时翻译用户动态,客户端浏览器将异步请求发送到服务器,服务器将响应该请求而不会导致页面刷新。然后客户端将动态地将翻译插入当前页面。这种技术被称为Ajax,它是异步JavaScript和XML的缩写(尽管如今XML经常被JSON取代)。
实时翻译的工作流程
借助Flask-Babel,本应用对外语提供了良好的支持,可以支持尽可能多的语言,只要我找到了对应的译文。但是遗漏一个要素,用户将以自己的语言撰写博客文章,因此用户很有可能会遇到用未知语言撰写的文章。自动翻译的质量并不总是很好,但是在大多数情况下,如果您只想对另一种语言的文字有一个基本的了解,就已经足够了。
这正是Ajax大展身手的好机会! 设想主页或发现页面可能会显示若干用户动态,其中一些可能是外语。 如果我使用传统的服务器端技术实现翻译,则翻译请求会导致原始页面被替换为新页面。 事实是,要求翻译诸多用户动态中的一条,并不是一个足够大的动作来要求整个页面的更新,如果翻译文本可以被动态地插入到原始文本下方,而剩下的页面保持原样,则用户体验更加出色。
实施实时自动翻译需要一些步骤。首先,我需要一种方法来识别要翻译的文本的源语言。我还需要了解每个用户的首选语言,因为我想仅为使用其他语言发表的动态显示“翻译”链接。当提供翻译链接并且用户单击它时,我需要将Ajax请求发送到服务器,服务器将与第三方翻译API联系。服务器将包含翻译后的文本的响应发送回去后,客户端javascript代码将把该文本动态地插入到页面中。您一定会注意到,这里有一些特殊的问题。我将逐一看看这些问题。
语言识别
第一个问题是确定帖子使用的语言。这并不是一门精确的科学,因为并非总是可以明确地检测一种语言,但是在大多数情况下,自动检测效果很好。在Python中,有一个称为guess_language
的语言检测库,还算好用。该软件包的原始版本相当旧,并且从未移植到Python 3,因此我将安装一个支持Python 2和3的派生版本:
(venv) $ pip install guess-language_spirit
计划是将每个博客帖子都提供给这个包,以尝试确定语言。由于进行这种分析比较耗时,因此我不想在每次将帖子渲染到页面时重复进行此工作。我要做的是在帖子提交时,就设置帖子的源语言。然后检测到的语言将存储在posts表中。
第一步,向Post
模型添加language
字段:
app / models.py:将检测到的语言添加到Post模型。
class Post(db.Model):
# ...
language = db.Column(db.String(5))
您还记得,每当数据库模型发生变化时,都需要生成数据库迁移:
(venv) $ flask db migrate -m "add language to posts"
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added column 'post.language'
Generating migrations/versions/2b017edaa91f_add_language_to_posts.py ... done
然后需要将迁移应用于数据库:
(venv) $ flask db upgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Upgrade ae346256b650 -> 2b017edaa91f, add language to posts
现在,我可以在提交帖子时检测并存储该语言类型:
app / routes.py:保存新帖子的语言字段。
from guess_language import guess_language
@app.route('/', methods=['GET', 'POST'])
@app.route('/index', methods=['GET', 'POST'])
@login_required
def index():
form = PostForm()
if form.validate_on_submit():
language = guess_language(form.post.data)
if language == 'UNKNOWN' or len(language) > 5:
language = ''
post = Post(body=form.post.data, author=current_user,
language=language)
# ...
进行此更改后,每次提交帖子时,都会通过guess_language
函数测试文本来尝试确定语言。如果该语言以未知的形式返回,或者得到意想不到的长字符串的结果,我会将一个空字符串保存到数据库中以安全地使用它。我将采用这样的约定:语言字段设置为空字符串的任何帖子,都假定为未知的语言。
显示“翻译”链接
第二步很简单。我现在要做的是在所有非当前用户首选语言的帖子旁边,添加一个“翻译”链接。
app / templates / _post.html:为帖子添加翻译链接。
{% if post.language and post.language != g.locale %}
<br><br>
<a href="#">{{ _('Translate') }}</a>
{% endif %}
我正在_post.html
子模板中执行此操作,因此此功能将显示在显示博客文章的任何页面上。翻译链接只会出现在检测到该语言的帖子上,并且该语言与用Flask-Babel装饰器localeselector
装饰的函数选择的语言不匹配。回顾第13章,所选语言环境存储为g.locale
。链接的文本需要以Flask-Babel可以翻译的方式添加,因此我在定义链接时使用了_()
函数。
请注意,我尚未与此链接关联任何操作。首先,我想弄清楚如何进行实际翻译。
使用第三方翻译服务
两种主要的翻译服务是Google Cloud Translation API和Microsoft Translator Text API。两者都是付费服务,但是Microsoft提供的入门级选项可免费提供少量翻译。Google过去曾提供免费的翻译服务,但今天,即使是最低的服务也要付费。因为我希望能够在不花费任何费用的情况下尝试翻译,所以我将采用Microsoft解决方案。
在使用Microsoft Translator API之前,您需要获得Microsoft的云服务Azure的帐户。您可以选择免费套餐,虽然在注册过程中会要求您提供信用卡号,但是当您停留在该级别的服务中时,将不会从您的卡中扣除费用。
拥有Azure帐户后,转到Azure门户并单击左上方的“New”按钮,然后在搜索框中键入“Translator Text API”。从搜索中选择Translator Text API,然后单击“Create”按钮。现在,您将看到一个表单,您可以在其中定义一个新的Translator Text API,该资源将添加到您的帐户中。您可以在下面看到我如何填写表格:
单击“Review + Create”按钮,然后单击“Create”后,翻译器API资源将添加到您的帐户中。如果您等待几秒钟,则会在顶部栏中收到有关已部署翻译器资源的通知。单击“Go to resource”按钮,然后在左侧栏中找到“Keys and Endpoint”选项。现在,您将看到两个键,分别标记为“Key 1”和“Key 2”。将任一键复制到剪贴板,然后将其输入到终端的环境变量中(如果使用的是Microsoft Windows,请替换export
为set
):
(venv) $ export MS_TRANSLATOR_KEY=<paste-your-key-here>
该密钥用于与翻译服务进行身份验证,因此需要将其添加到应用配置中:
config.py:将Microsoft Translator API密钥添加到配置中。
class Config(object):
# ...
MS_TRANSLATOR_KEY = os.environ.get('MS_TRANSLATOR_KEY')
与很多配置值一样,我更喜欢将它们安装在环境变量中,然后从那里将其导入Flask配置中。这对于允许访问第三方服务的敏感信息(例如密钥或密码)尤其重要。您绝对不希望在代码中显式地编写这些信息。
Microsoft Translator API是一种接受HTTP请求的Web服务。Python中有一些HTTP客户端,但是最受欢迎和易于使用的是requests
包。因此,我们将其安装到虚拟环境中:
(venv) $ pip install requests
在下面,您可以看到我使用Microsoft Translator API编写翻译文本的功能。新建一个app / translate.py模块:
app / translate.py:文本翻译函数。
import json
import requests
from flask_babel import _
from app import app
def translate(text, source_language, dest_language):
if 'MS_TRANSLATOR_KEY' not in app.config or \
not app.config['MS_TRANSLATOR_KEY']:
return _('Error: the translation service is not configured.')
auth = {'Ocp-Apim-Subscription-Key': app.config['MS_TRANSLATOR_KEY']}
r = requests.get('https://api.microsofttranslator.com/v2/Ajax.svc'
'/Translate?text={}&from={}&to={}'.format(
text, source_language, dest_language),
headers=auth)
if r.status_code != 200:
return _('Error: the translation service failed.')
return json.loads(r.content.decode('utf-8-sig'))
该函数将要翻译的文本、源语言和目标语言代码作为参数,并返回带有已翻译文本的字符串。首先检查配置中是否有翻译服务的key,如果没有,则返回错误。错误也是一个字符串,因此从外部看,它看起来像翻译后的文本。这样可以确保在出现错误的情况下,用户将看到有意义的错误消息。
requests
包中的get()
方法向作为第一个参数给定的URL发送一个带有GET方法的HTTP请求。在https://api.cognitive.microsofttranslator.com/这个URL上的Translator Text API页面,我正在使用“Keys and Endpoin”选项。转换端点的路径为/ translate,如文档所述。需要在URL中将源语言和目标语言作为查询字符串参数,分别命名为from和
to
。API还要求在查询字符串中提供api-version=3.0
参数。需要在请求的正文中以JSON格式给出要翻译的文本,格式为{"Text": "the text to translate here"}
。
要通过服务进行身份验证,我需要传递添加到配置中的密钥。通过自定义HTTP标头Ocp-Apim-Subscription-Key,
可以传递此密钥。还需要HTTP标头Ocp-Apim-Subscription-Region
中提供部署翻译器资源的区域。该区域提供的名称显示在“Keys and Endpoint”页面中,在我的案例中是westus2
,该名称是我选择区域West US 2
的名称。我创建了auth
字典,然后将它通过headers
参数传递给requests
。
requests.post()
方法返回一个响应对象,其中包含服务提供的所有详细信息。我首先需要检查状态码是否为200,200表示请求成功。如果我得到任何其他代码,则表明存在错误,因此在这种情况下,我将返回错误字符串。如果状态码为200,则响应的主体具有转换后的JSON编码字符串,因此我所要做的就是使用响应对象中的json()
函数将JSON解码为我可以使用的Python字符串。返回JSON响应是翻译的列表,但是由于我们正在翻译单个文本,因此我可以获取第一个元素并在翻译结构中找到实际的翻译文本。
在下面,您可以看到一个使用新translate()
函数的Python控制台会话:
>>> from app.translate import translate
>>> translate('Hi, how are you today?', 'en', 'es') # English to Spanish
'Hola, ¿cómo estás hoy?'
>>> translate('Hi, how are you today?', 'en', 'de') # English to German
'Are Hallo, how you heute?'
>>> translate('Hi, how are you today?', 'en', 'it') # English to Italian
'Ciao, come stai oggi?'
>>> translate('Hi, how are you today?', 'en', 'fr') # English to French
"Salut, comment allez-vous aujourd'hui ?"
太酷了吧?现在是时候将该功能与应用集成了。
服务器上的Ajax
我将从实现服务器端部分开始。当用户单击帖子下方显示的“翻译”链接时,将向服务器发出异步HTTP请求。在下一个会话中,我将向您展示如何执行此操作,因此,我现在将专注于实现服务器对此请求的处理。
异步(或Ajax)请求类似于我在应用中创建的路由和视图函数,唯一的区别在于,它不返回HTML或重定向,而只是返回数据,格式为XML或更常见的JSON。在下面,您可以看到翻译视图函数,该功能调用Microsoft Translator API,然后以JSON格式返回翻译后的文本:
app / routes.py:文本翻译视图函数。
from flask import jsonify
from app.translate import translate
@app.route('/translate', methods=['POST'])
@login_required
def translate_text():
return jsonify({'text': translate(request.form['text'],
request.form['source_language'],
request.form['dest_language'])})
如您所见,这很简单。我以POST
请求的形式实现了这条路由。关于何时使用GET
或POST
(或您尚未看到的其他请求方法),实际上并没有绝对的规则。由于客户端将发送数据,因此我决定使用一个POST
请求,因为该请求类似于提交表单数据的请求。request.form
属性是Flask暴露的字典,其中包含提交中的所有数据。当我使用Web表单时,我不需要查看request.form
,因为Flask-WTF可以为我完成所有工作,但是在这种情况下,实际上没有Web表单,因此我必须直接访问数据。
因此,我在此函数中所做的就是从上一节中调用translate()
函数,直接从通过请求提交的数据中传递三个参数。结果被合并到key下的单键text
字典中,并且该字典作为参数传递给Flask的jsonify()
函数,该函数将字典转换为JSON的有效数据格式。jsonify()
的返回值是将发送回客户端的HTTP响应。
例如,如果客户端要将字符串Hello, World!
转换为西班牙语,则此请求的响应将具有以下有效数据格式:
{ "text": "Hola, Mundo!" }
来自客户端的Ajax
因此,既然服务器能够通过/ translate URL提供翻译,那么当用户单击上面添加的“ Translate”链接时,我需要调用此URL,传递需要翻译的文本、源语言和目标语言。如果您不熟悉在浏览器中使用JavaScript的话,这将是一个很好的学习机会。
在浏览器中使用JavaScript时,当前显示的页面在内部表示为“文档对象模型”或仅表示为DOM。这是引用页面中存在的所有元素的层次结构。在此上下文中运行的JavaScript代码可以对DOM进行更改以触发页面中的更改。
首先让我们讨论一下,我在浏览器中运行的JavaScript代码如何获取我需要发送给服务器中运行的翻译函数的三个参数。要获取文本,我需要在DOM中找到包含博客文章正文的节点,并读取其内容。为了易于识别包含博客文章的DOM节点,我将为其添加唯一的ID。如果您查看_post.html模板,则呈现帖子正文的行将显示为{{ post.body }}
。我要做的就是将这些内容包装在一个<span>
元素中。这不会在视觉上进行任何更改,但是它为我提供了一个可以插入标识符的位置:
app / templates / _post.html:为每个博客帖子添加一个ID。
<span id="post{{ post.id }}">{{ post.body }}</span>
这将为每个博客帖子分配一个唯一的标识符,格式为post1
,post2
等等,其中数字与每个帖子的数据库标识符匹配。现在,每个博客帖子都有一个唯一的标识符,给定ID值,我可以使用jQuery定位该<span>
帖子的元素并提取其中的文本。例如,如果我想获取ID为123的帖子的文本,则可以这样做:
$('#post123').text()
$
符号是jQuery库提供的函数的名称。该库供Bootstrap使用,因此Flask-Bootstrap已包含该库。#
是通过jQuery的所使用的“选择器”的语法,这意味着接下来是元素的ID。
从服务器接收到翻译后的文本后,我还想在其中插入翻译后的文本。我要做的是用翻译后的文本替换“翻译”链接,因此我还需要为该节点具有唯一的标识符:
app / templates / _post.html:为翻译链接添加ID。
<span id="translation{{ post.id }}">
<a href="#">{{ _('Translate') }}</a>
</span>
因此,现在对于给定的帖子ID,我有一个博客帖子节点post<ID>
,以及一个相应的translation<ID>
节点,一旦有了它,我将需要用翻译后的文本替换“翻译”链接。
下一步是编写一个可以完成所有翻译工作的函数。此函数将使用输入和输出DOM节点以及源语言和目标语言,使用所需的三个参数向服务器发出异步请求,并在服务器响应后最终用翻译后的文本替换Translate链接。这听起来很麻烦,但是实现起来却很简单:
app / templates / base.html:客户端翻译函数。
{% block scripts %}
...
<script>
function translate(sourceElem, destElem, sourceLang, destLang) {
$(destElem).html('<img src="{{ url_for('static', filename='loading.gif') }}">');
$.post('/translate', {
text: $(sourceElem).text(),
source_language: sourceLang,
dest_language: destLang
}).done(function(response) {
$(destElem).text(response['text'])
}).fail(function() {
$(destElem).text("{{ _('Error: Could not contact server.') }}");
});
}
</script>
{% endblock %}
前两个参数是发布和翻译链接节点的唯一ID。最后两个参数是源语言代码和目标语言代码。
该函数的界面很友好:它添加了一个加载器来替换“翻译”链接,以便用户知道翻译正在进行中。这是使用jQuery的$(destElem).html()
函数完成的,它用基于<img>
元素的新HTML内容替换定义为翻译链接的原始HTML。对于加载器,我将使用小动画GIF,我将使用一个小的动画GIF(loading.gif),它已添加到Flask为静态文件保留的app/static目录中。为了生成引用该图像的URL,我使用了url_for()
函数,传递了特殊的路由名称,static
并给出了图像的文件名作为参数。您可以在本章的下载包中找到loading.gif图像。
所以现在我有了一个不错的加载器,它取代了Translate链接,因此用户知道等待翻译出现。下一步是将POST
请求发送到上一节中定义的/ translate URL。为此,在这种情况下,我还将使用jQuery的 $.post()
。此函数以类似于浏览器提交Web表单的方式,将数据提交到服务器,这很方便,因为这允许Flask将这些数据合并到request.form
字典中。$.post()
参数为两个,首先是发送请求的URL,然后是包含服务器期望的三个数据项的字典(或对象,在JavaScript中称为对象)。
您可能知道JavaScript与回调函数(或更高级的回调形式promises)一起工作。我现在要做的是,一旦请求完成,浏览器接收到响应,就指出我想做什么。在JavaScript中,没有等待的东西,一切都是异步的。我需要做的是提供一个回调函数,当收到响应时浏览器将调用该回调函数。另外,为了使所有内容尽可能健壮,我想指出发生错误的情况下的处理方法,这将是处理错误的第二个回调函数。有几种方法可以指定这些回调,但是在这种情况下,使用promise使代码相当清晰。语法如下:
$.post(<url>, <data>).done(function(response) {
// success callback
}).fail(function() {
// error callback
})
promise语法允许将$ .post()
调用的返回值“传入”回调函数作为参数。在成功回调中,我要做的就是调用$(destElem).text()
翻译后的文本,该文本位于text
键下的字典中。在发生错误的情况下,我会执行相同的操作,但是我显示的文本是通用的错误消息,我确保将其作为可翻译文本输入到基本模板中。
所以现在唯一剩下的就是通过用户点击翻译链接来触发具有正确参数的translate()
函数。还有一些方法可以做到这一点,我要做的就是将对函数的调用嵌入到链接的href
属性中:
app / templates / _post.html:翻译链接处理程序。
<span id="translation{{ post.id }}">
<a href="javascript:translate(
'#post{{ post.id }}',
'#translation{{ post.id }}',
'{{ post.language }}',
'{{ g.locale }}');">{{ _('Translate') }}</a>
</span>
如果链接的href
元素带有前缀,则可以接受任何JavaScript代码,因此这是调用翻译函数的便捷方法。因为当客户端请求页面时此链接将在服务器中渲染,所以我可以使用{{ }}
表达式生成该函数的四个参数。每个帖子都会有自己的翻译链接,并带有其唯一生成的参数。post <ID>
和translation <ID>
需要渲染具体的ID,它们都需要在被使用时加上#
前缀。
现在,实时翻译功能已完成!如果您在环境中设置了有效的Microsoft Translator API密钥,则现在应该能够触发翻译。假设您将浏览器设置为喜欢英语,则需要用另一种语言撰写一篇文章,以查看“翻译”链接。在下面您可以看到一个示例:
在本章中,我介绍了一些新文本,需要将其翻译成应用支持的所有语言,因此有必要更新翻译目录:
(venv) $ flask translate update
对于您自己的项目,您将需要在每个语言存储库中编辑messages.po文件,以包括这些新测试的翻译,但是我已经在本章或GitHub存储库的下载包中创建了西班牙语翻译。
要发布新的翻译,需要对其进行编译:
(venv) $ flask translate compile
原文链接:https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xiv-ajax