9.2 添加国际化和本地化
Django提供了完整的国际化和本地化支持。它允许你把应用翻译为多种语言,它会处理特定区域日期,时间,数字和时区。让我们弄清楚国际化和本地化的区别。国际化(通常缩写为i18n)是让软件适用于潜在的不同语言和地区的过程,让软件不会硬编码为特定语言和地区。本地化(缩写为l10n)是实际翻译软件和适应特定地区的过程。使用Django自己的国际化框架,它本身被翻译为超过50中语言。
9.2.1 使用Django国际化
国际化框架让你很容易的在Python代码和模板中编辑需要翻译的字符串。它依赖GNU gettext工具集生成和管理信息文件。一个信息文件是一个代表一种语言的普通文本文件。它包括你应用中的部分或全部需要翻译的字符串,以及相应的单种语言的翻译。信息文件的扩展名是.po
。
一旦完成翻译,信息文件就会被编译,用来快速访问翻译后的字符串。被编译的翻译文件扩展名是.mo
。
9.2.1.1 国际化和本地化设置
Django为国际化提供了一些设置。以下是最相关的设置:
-
USE_I18N
:指定Django的翻译系统是否可用的布尔值。默认为True
。 -
USE_L10N
:表示本地格式是否可用的布尔值。可用时,用本地格式表示日期和数字。默认为False
。 -
USE_TZ
:指定日期和时间是否时区感知的布尔值。当你用startproject
创建项目时,该值设置为True
。 -
LANGUAGE_CODE
:项目的默认语言代码。它使用标准的语言ID格式,比如en-us
表示美式英语,en-gb
表示英式英语。这个设置需要USE_I18N
设为True
才生效。你可以在这里查看有效地语言ID列表。 -
LANGUAGES
:一个包括项目可用语言的元组。它由包括语言代码和语言名称的双元组构成。你可以在django.conf.global_settings
中查看可用语言列表。当你选择你的网站将使用哪些语言时,你可以设置LANGUAGES
为这个列表的一个子集。 -
LOCALE_PATHS
:Django查找项目中包括翻译的信息文件的目录列表。 -
TIME_ZONE
:表示项目时区的字符串。当你使用startproject
命令创建新项目时,它设置为UTC
。你可以设置它为任何时区,比如Europe/Madrid
。
这是一些可用的国际化和本地化设置。你可以在这里查看完整列表。
9.2.1.2 国际化管理命令
使用manage.py
或者django-admin
工具管理翻译时,Django包括以下命令:
-
makemessages
:它在源代码树上运行,查找所有标记为需要翻译的字符串,并在locale
目录中创建或更新.po
信息文件。每种语言创建一个.po
文件。 -
compilemessages
:编译存在的.po
信息文件为.mo
文件,用于检索翻译。
你需要gettext
工具集创建,更新和编译信息文件。大部分Linux发行版都包括了gettext
工具集。如果你使用的是Mac OS X,最简单的方式是用brew install gettext
命令安装。你可能还需要用brew link gettext --force
强制链接到它。对于Windows安装,请参考这里的步骤。
9.2.1.3 如果在Django项目中添加翻译
让我们看下国际化我们项目的流程。我们需要完成以下工作:
- 我们标记Python代码和目录中需要编译的字符串。
- 我们运行
makemessages
命令创建或更新信息文件,其中包括了代码中所有需要翻译的字符串。 - 我们翻译信息文件中的字符串,然后用
compilemessages
管理命令编辑它们。
9.2.1.4 Django如何决定当前语言
Django自带一个中间件,它基于请求的数据决定当前语言。位于django.middleware.locale.LocaleMiddleware
的LocaleMiddleware
中间件执行以下任务:
- 如果你使用
i18_patterns
,也就是你使用翻译后的URL模式,它会在被请求的URL中查找语言前缀,来决定当前语言。 - 如果没有找到语言前缀,它会在当前用户会话中查询
LANGUAGE_SESSION_KEY
。 - 如果没有在会话中设置语言,它会查找带当前语言的cookie。这个自定义的cookie名由
LANGUAGE_COOKIE_NAME
设置提供。默认情况下,该cookie名为django-language
。 - 如果没有找到cookie,它会查询请求的
Accept-Language
头。 - 如果
Accept-Language
头没有指定语言,Django会使用LANGUAGE_CODE
设置中定义的语言。
默认情况下,Django会使用LANGUAGE_CODE
设置中定义的语言,除非你使用LocaleMiddleware
。以上描述的过程只适用于使用这个中间件。
9.2.2 为国际化我们的项目做准备
让我们为我们的项目使用不同语言。我们将创建商店的英语和西拔牙语版本。编辑项目的settings.py
文件,在LANGUAGE_CODE
设置之后添加LANGUAGES
设置:
LANGUAGES = (
('en', 'English'),
('es', 'Spanish'),
)
LANGUAGES
设置中包括两个元组,每个元组包括语言代码和名称。语言代码可以指定地区,比如en-us
或en-gb
,也可以通用,比如en
。在这个设置中,我们指定我们的应用只对英语和西班牙可用。如果我们没有定义LANGUAGES
设置,则网站对于Django的所有翻译语言都可用。
如下修改LANGUAGE_CODE
设置:
LANGUAGE_CODE = 'en'
在MIDDLEWARE
设置中添加django.middleware.locale.LocaleMiddleware
。确保这个中间件在SessionMiddleware
之后,因为LocaleMiddleware
需要使用会话数据。它还需要在CommonMiddleware
之前,因为后者需要一个激活的语言解析请求的URL。MIDDLEWARE
设置看起来是这样的:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
# ...
]
中间件的顺序很重要,因为每个中间件都依赖前面其它中间件执行后的数据集。中间件按
MIDDLEWARE
中出现的顺序应用在请求上,并且反序应用在响应上。
在项目主目录中穿件以下目录结构,与manage.py
同级:
locale/
en/
es/
locale
目录是应用的信息文件存储的目录。再次编辑settings.py
文件,在其中添加以下设置:
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale/'),
)
LOCALE_PATHS
设置指定了Django查找翻译文件的目录。最先出现的路径优先级最高。
当你在项目目录中使用makemessages
命令时,信息文件会在我们创建的locale/
路径中生成。但是,对于包括locale/
目录的应用来说,信息文件会在这个应用的locale/
目录中生成。
9.2.3 翻译Python代码
要翻译Python代码中的字面量,你可以用django.utils.translation
中的gettext()
函数标记要翻译的字符串。这个函数翻译信息并返回一个字符串。惯例是把这个函数导入为短别名_
。
你可以在这里查看所有关于翻译的文档。
9.2.3.1 标准翻译
以下代码展示了如何标记一个需要翻译的字符串:
from django.utils.translation import gettext as _
output = _('Text to be translated.')
9.2.3.2 惰性翻译
Django的所有翻译函数都包括惰性(lazy)版本,它们的后缀都是_lazy()
。使用惰性函数时,当值被访问时翻译字符串,而不是惰性函数被调用时翻译(这就是为什么它们被惰性翻译)。当标记为翻译的字符串位于加载模式时执行的路径中,这些惰性翻译函数非常方便。
使用
gettext_lazy()
代替gettext()
时,当值被访问时翻译字符串,而不是翻译函数调用时翻译。Django为所有翻译函数提供了惰性版本。
9.2.3.3 带变量的翻译
标记为翻译的字符串的字符串可以包括占位符来在翻译中引入变量。以下代码是翻译带占位符的字符串:
from django.utils.translation import gettext as _
month = _('April')
day = '14'
output = _('Today is %(month)s %(day)s') % {'month': month, 'day': day}
通过使用占位符,你可以重新排列文本变量。比如,上个例子中的英文防疫可能是Today is April 14
,而西拔牙语翻译时Hoy es 14 de Abiril
。当需要翻译的字符串中包括一个以上的参数时,总是使用字符串插值代替位置插值。这样就可以重新排列站位文本。
9.2.3.4 翻译中的复数形式
对于复数形式,你可以使用ngettext()
和ngettext_lazy()
。这些函数根据一个表示对象数量的参数翻译单数和复数形式。下面的代码展示了如何使用它们:
output = ngettext('there is %(count)d product',
'there are %(count)d products',
count) % {'count': count}
现在你已经学会了翻译Python代码中字面量的基础,是时候翻译我们的项目了。
9.2.3.5 翻译你的代码
编辑项目的settings.py
文件,导入gettext_lazy()
函数,并如下修改LANGUAGES
设置来翻译语言名称:
from django.utils.translation import gettext_lazy as _
LANGUAGES = (
('en', _('English')),
('es', _('Spanish')),
)
我们在这里使用gettext_lazy()
函数代替gettext()
,来避免循环导入,所以当语言名称被访问时翻译它们。
打开终端,并在项目目录中执行以下命令:
django-admin makemessages --all
你会看到以下输出:
processing locale en
processing locale es
看一眼locale/
目录,你会看这样的文件结构:
en/
LC_MESSAGES/
django.po
es/
LC_MESSAGES/
django.po
为每种语言创建了一个.po
信息文件。用文本编辑器打开es/LC_MESSAGES/django.po
文件。在文件结尾,你会看到以下内容:
#: myshop/settings.py:122
msgid "English"
msgstr ""
#: myshop/settings.py:123
msgid "Spanish"
msgstr ""
每个需要翻译的字符串前面都有一条注释,显示它位于的文件和行数。每个翻译包括两个字符串:
-
msgid
:源代码中需要翻译的字符串。 -
msgstr
:对应语言的翻译,默认为空。你需要在这里输入给定字符串的实际翻译。
为给的msgid
字符串填入msgstr
翻译:
#: myshop/settings.py:122
msgid "English"
msgstr "Inglés"
#: myshop/settings.py:123
msgid "Spanish"
msgstr "Español"
保存修改的信息文件,打开终端,执行以下命令:
django-admin compilemessages
如果一切顺利,你会看到类似这样的输出:
processing file django.po in /Users/lakerszhy/Documents/GitHub/Django-By-Example/code/Chapter 9/myshop/locale/en/LC_MESSAGES
processing file django.po in /Users/lakerszhy/Documents/GitHub/Django-By-Example/code/Chapter 9/myshop/locale/es/LC_MESSAGES
输出告诉你信息文件已经编译。再看一眼myshop
项目的locale
目录,你会看到以下文件:
en/
LC_MESSAGES/
django.mo
django.po
es/
LC_MESSAGES/
django.mo
django.po
你会看到为每种语言生成了一个编译后的.mo
信息文件。
我们已经翻译了语言名本身。现在让我们翻译在网站中显示的模型字段名。编辑orders
应用的models.py
文件,为Order
模型字段添加需要翻译的名称标记:
from django.utils.translation import gettext_lazy as _
class Order(models.Model):
first_name = models.CharField(_('first name'), max_length=50)
last_name = models.CharField(_('last name'), max_length=50)
email = models.EmailField(_('email'))
address = models.CharField(_('address'), max_length=250)
postal_code = models.CharField(_('postal code'), max_length=20)
city = models.CharField(_('city'), max_length=100)
# ...
我们为用户下单时显示的字段添加了名称,分别是first_name
,last_name
,email
,address
,postal_code
和city
。记住,你也可以使用verbose_name
属性为字段命名。
在orders
应用中创建以下目录结构:
locale/
en/
es/
通过创建locale/
目录,这个应用中需要翻译的字符串会存储在这个目录的信息文件中,而不是主信息文件。通过这种方式,你可以为每个应用生成独立的翻译文件。
在项目目录打开终端,执行以下命令:
django-admi makemessages --all
你会看到以下输出:
processing locale en
processing locale es
用文本编辑器开大es/LC_MESSAGES/django.po
文件。你会看到Order
模型需要翻译的字符串。为给的msgid
字符串填入msgstr
翻译:
#: orders/models.py:10
msgid "first name"
msgstr "nombre"
#: orders/models.py:11
msgid "last name"
msgstr "apellidos"
#: orders/models.py:12
msgid "email"
msgstr "e-mail"
#: orders/models.py:13
msgid "address"
msgstr "dirección"
#: orders/models.py:14
msgid "postal code"
msgstr "código postal"
#: orders/models.py:15
msgid "city"
msgstr "ciudad"
填完之后保存文件。
除了文本编辑器,你还可以使用Poedit编辑翻译。Poedit是一个编辑翻译的软件,它使用gettext
。它有Linux,Windows和Mac OS X版本。你可以在这里下载。
让我们再翻译项目中的表单。orders
应用的OrderCreateForm
不需要翻译,因为它是一个ModelForm
,它的表单字段标签使用了Order
模型字段的verbose_name
属性。我们将翻译cart
和coupons
应用的表单。
编辑cart
应用中的forms.py
文件,为CartAddProductForm
的quantity
字段添加一个lable
属性,然后标记为需要翻译:
from django import forms
from django.utils.translation import gettext_lazy as _
PRODUCT_QUANTITY_CHOICES = [(i, str(i)) for i in range(1, 21)]
class CartAddProductForm(forms.Form):
quantity = forms.TypedChoiceField(
choices=PRODUCT_QUANTITY_CHOICES,
coerce=int,
label=_('Quantity'))
update = forms.BooleanField(
required=False,
initial=False,
widget=forms.HiddenInput)
编辑coupons
应用的forms.py
文件,如下翻译CouponApplyForm
表单:
from django import forms
from django.utils.translation import gettext_lazy as _
class CouponApplyForm(forms.Form):
code = forms.CharField(label=_('Coupon'))
我们为code
字段添加了label
属性,并标记为需要翻译。
9.2.4 翻译模板
Django为翻译模板中的字符串提供了{% trans %}
和{% blocktrans %}
模板标签。要使用翻译模板标签,你必须在模板开头添加{% load i18n %}
加载它们。
9.2.4.1 模板标签{% trans %}
{% trans %}
模板标签允许你标记需要翻译的字符串,常量或者变量。在内部,Django在给定的文本上执行gettext()
。以下是在模板中标记需要翻译的字符串:
{% trans "Text to be translated" %}
你可以使用as
在变量中存储翻译后的内容,然后就能在整个模板中使用这个变量。下面这个例子在greeting
变量中存储翻译后的文本:
{% trans "Hello!" as greeting %}
<h1>{{ greeting }}</h1>
{% trans %}
标签对简单的翻译字符串很有用,但它不能处理包括变量的翻译内容。
9.2.4.2 模板标签{% blocktrans %}
{% blocktrans %}
模板标签允许你标记包括字面量的内容和使用占位符的变量内容。下面这个例子展示了如何使用{% blocktrans %}
标签标记一个包括name
变量的翻译内容:
{% blocktrans %}Hello {{ name }}!{% endblocktrans %}
你可以使用with
引入模板表达式,比如访问对象属性,或者对变量使用模板过滤器。这时,你必须总是使用占位符。你不能在blocktrans
块中访问表达式或者对象属性。下面的例子展示了如何使用with
,其中引入了一个对象属性,并使用capfirst
过滤器:
{% blocktrans with name=user.name|capfirst %}
Hello {{ name }}!
{% endblocktrans %}
当需要翻译的字符串中包括变量内容时,使用
{% blocktrans %}
代替{% trans %}
。
9.2.4.3 翻译商店的模板
编辑shop
应用的shop/base.html
模板。在模板开头加载i18n
标签,并标记需要翻译的字符串:
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>
{% block title %}{% trans "My shop" %}{% endblock %}
</title>
<link href="{% static "css/base.css" %}" rel="stylesheet">
</head>
<body>
<div id="header">
<a href="/" class="logo">{% trans "My shop" %}</a>
</div>
<div id="subheader">
<div class="cart">
{% with total_items=cart|length %}
{% if cart|length > 0 %}
{% trans "Your cart:" %}
<a href="{% url "cart:cart_detail" %}">
{% blocktrans with total_items_plural=otal_items|pluralize total_price=cart.get_total_price %}
{{ total_items }} item{{ total_items_plural }},
${{ total_price }}
{% endblocktrans %}
</a>
{% else %}
{% trans "Your cart is empty." %}
{% endif %}
{% endwith %}
</div>
</div>
<div id="content">
{% block content %}
{% endblock %}
</div>
</body>
</html>
记住,我们用{% blocktrans %}
标签显示购物车汇总。之前购物车汇总是这样的:
{{ total_items }} item{{ total_items|pluralize }},
${{ cart.get_total_price }}
我们利用{% blocktrans with ... %}
为total_items|pluralize
(在这里使用模板标签)和cart.get_total_price
(在这里访问对象方法)使用占位符,结果是:
{% blocktrans with total_items_plural=otal_items|pluralize total_price=cart.get_total_price %}
{{ total_items }} item{{ total_items_plural }},
${{ total_price }}
{% endblocktrans %}
接着编辑shop
应用的shop/product/detai.html
模板,在{% extends %}
标签(它必须总是第一个标签)之后加载i18n
标签:
{% load i18n %}
然后找到这一行:
<input type="submit" value="Add to cart">
替换为:
<input type="submit" value="{% trans "Add to cart" %}">
现在翻译orders
应用的模板。编辑orders
应用的orders/order/create.html
模板,如下标记需要翻译的文本:
{% extends "shop/base.html" %}
{% load i18n %}
{% block title %}
{% trans "Checkout" %}
{% endblock title %}
{% block content %}
<h1>{% trans "Checkout" %}</h1>
<div class="order-info">
<h3>{% trans "Your order" %}</h3>
<ul>
{% for item in cart %}
<li>
{{ item.quantity }}x {{ item.product.name }}
<span>${{ item.total_price }}</span>
</li>
{% endfor %}
{% if cart.coupon %}
<li>
{% blocktrans with code=cart.coupon.code discount=cart.coupon.discount %}
"{{ code }}" ({{ discount }}% off)
{% endblocktrans %}
<span>- ${{ cart.get_discount|floatformat:"2" }}</span>
</li>
{% endif %}
</ul>
<p>{% trans "Total" %}: ${{ cart.get_total_price_after_discount|floatformat:"2" }}</p>
</div>
<form action="." method="post" class="order-form">
{{ form.as_p }}
<p><input type="submit" value="{% trans "Place order" %}"></p>
{% csrf_token %}
</form>
{% endblock content %}
在本章示例代码中查看以下文件是如何标记需要翻译的字符串:
-
shop
应用:shop/product/list.hmtl
模板 -
orders
应用:orders/order/created.html
模板 -
cart
应用:cart/detail.html
模板
让我们更新信息文件来引入新的翻译字符串。打开终端,执行以下命令:
django-admin makemessages --all
.po
翻译文件位于myshop
项目的locale
目录中,orders
应用现在包括了所有我们标记过的翻译字符串。
编辑项目和orders
应用的.po
翻译文件,并填写西班牙语翻译。你可以参考本章示例代码的.po
文件。
从项目目录中打开终端,并执行以下命令:
cd orders/
django-admin compilemessages
cd ../
我们已经编译了orders
应用的翻译文件。
执行以下命令,在项目的信息文件中包括没有locale
目录的应用的翻译。
django-admin compilemessage
9.2.5 使用Rosetta的翻译界面
Rosetta是一个第三方应用,让你可以用跟Django管理站点一样的界面编辑翻译。Rosetta可以很容易的编辑.po
文件,并且它会更新编译后的编译文件。让我们把它添加到项目中。
使用pip
命令安装Rosetta:
pip install django-rosetta
然后把rosetta
添加到项目settings.py
文件中的INSTALLED_APP
设置中:
你需要把Rosetta的URL添加到主URL配置中。编辑项目的urls.py
文件,并添加下URL模式:
url(r'^rosetta/', include('rosetta.urls')),
确保把它放在shop.urls
模式之后,避免错误的匹配。
在浏览器中打开http://127.0.0.1:8000/admin/
,并用超级用户登录。然后导航到http://127.0.0.1:8000/rosetta/
。你会看到已经存在的语言列表,如下图所示:
点击Filter
中的All
显示所有可用的信息文件,包括属于orders
应用的信息文件。在Spanish
中点击Myshop
链接来编辑西班牙语翻译。你会看到一个需要翻译的字符串列表,如下图所示:
你可以在Spanish
列中输入翻译。Occurrences
列显示每个需要翻译的字符串所在的文件和行数。
包括占位符的翻译是这样的:
Rosetta用不同的颜色显示占位符。当你翻译内容时,确保不要翻译占位符。比如这一行字符串:
%(total_items)s item%(total_items_plural)s, $%(total_price)s
翻译为西班牙语后是这样的:
%(total_items)s producto%(total_items_plural)s, $%(total_price)s
你可以参考本章的示例代码,用同样的西班牙语翻译你的项目。
当你完成翻译后,点击Save and translate next block
按钮,把翻译保存到.po
文件。保存翻译时,Rosseta会编译信息文件,所以不需要执行compilemessages
命令。但是Rosetta需要写入locale
目录的权限来写入信息文件。确保这些目录有合理的权利。
如果你希望其他用户也可以编辑翻译,在浏览器中打开http://127.0.0.1:8000/admin/auth/group/add/
,并创建一个translators
组。然后访问http://127.0.0.1:8000/admin/auth/user/
,编辑你要授予翻译权限的用户。编辑用户时,在Premissions
中,把translators
组添加到每个用户的Chosen Groups
中。Resetta只对超级用户和属于translators
组的用户可用。
你可以在这里阅读Rosetta的文档。
当你在生产环境添加新翻译时,如果你的Django运行在一个真实的web服务器上,你必须在执行
compilemessages
命令或者用Rosetta保存翻译之后重启服务器,才能让修改生效。
9.2.6 不明确的翻译
你可能已经注意到了,在Rosetta中有一个Fuzzy
列。这不是Rosetta的特征,而是有gettext
提供的。如果启用了翻译的fuzzy
标记,那么它就不会包括在编译后的信息文件中。这个标记用于需要翻译者修改的翻译字符串。当用新的翻译字符串更新.po
文件中,可能有些翻译字符串自动标记为fuzzy
。当gettext
发现某些msgid
变动不大时,它会匹配为旧的翻译,并标记为fuzzy
,以便复核。翻译者应该复核不明确的翻译,然后移除fuzzy
标记,并在此编译信息文件。
9.2.7 URL模式的国际化
Django为URL提供了国际化功能。它包括两种主要的国际化URL特性:
- URL模式中的语言前缀:把语言前缀添加到URL中,在不同的基础URL下提供每种语言的版本
- 翻译后的URL模式:标记需要翻译的URL模式,因此同一个URL对于每种语言是不同的
翻译URL的其中一个原因是为搜索引擎优化你的网站。通过在模式中添加语言前缀,你就可以为每种语言提供索引URL,而不是为所有语言提供一个索引URL。此外,通过翻译URL为不同语言,你可以为搜索引擎提供对每种语言排名更好的URL。
9.2.7.1 添加语言前缀到URL模式中
Django允许你在URL模式中添加语言前缀。例如,网站的英语版本可以/en/
起始路径下,而西班牙语版本在/es/
下。
要在URL模式中使用语言,你需要确保settings.py
文件的MIDDLEWARE
设置中包括django.middleware.locale.LocaleMiddleware
。Django将用它从请求URL中识别当前语言。
让我们在URL模式中添加语言前缀。编辑myshop
项目的urls.py
文件,添加以下导入:
from django.conf.urls.i18n import i18n_patterns
然后添加i18n_patterns()
,如下所示:
urlpatterns = i18n_patterns(
url(r'^admin/', admin.site.urls),
url(r'^cart/', include('cart.urls', namespace='cart')),
url(r'^orders/', include('orders.urls', namespace='orders')),
url(r'^paypal/', include('paypal.standard.ipn.urls')),
url(r'^payment/', include('payment.urls', namespace='payment')),
url(r'^coupons/', include('coupons.urls', namespace='coupons')),
url(r'^rosetta/', include('rosetta.urls')),
url(r'^', include('shop.urls', namespace='shop')),
)
你可以在patterns()
和i18n_patterns()
中结合URL模式,这样有些模式包括语言前缀,有些不包括。但是,最好只使用翻译后的URL,避免不小心把翻译后的URL匹配到没有翻译的URL模式。
启动开发服务器,并在浏览器中打开http://127.0.0.1:8000/
。因为你使用了LocaleMiddleware
中间件,所以Django会执行Django如何决定当前语言
中描述的步骤,决定当前的语言,然后重定义到包括语言前缀的同一个URL。看一下眼浏览器中的URL,它应该是http://127.0.0.1:8000/en/
。如果浏览器的Accept-Language
头是西班牙语或者英语,则当前语言是它们之一;否则当前语言是设置中定义的默认LANGUAGE_CODE
(英语)。
9.2.7.2 翻译URL模式
Django支持URL模式中有翻译后的字符串。对应单个URL模式,你可以为每种语言使用不同的翻译。你可以标记需要翻译的URL模式,方式与标记字面量一样,使用gettext_lazy()
函数。
编辑myshop
项目的主urls.py
文件,把翻译字符串添加到cart
,orders
,payment
和coupons
应用的URL模式的正则表达式中:
urlpatterns = i18n_patterns(
url(r'^admin/', admin.site.urls),
url(_(r'^cart/'), include('cart.urls', namespace='cart')),
url(_(r'^orders/'), include('orders.urls', namespace='orders')),
url(r'^paypal/', include('paypal.standard.ipn.urls')),
url(_(r'^payment/'), include('payment.urls', namespace='payment')),
url(_(r'^coupons/'), include('coupons.urls', namespace='coupons')),
url(r'^rosetta/', include('rosetta.urls')),
url(r'^', include('shop.urls', namespace='shop')),
)
编辑orders
应用的urls.py
文件,编辑需要翻译的URL模式:
from django.utils.translation import gettext_lazy as _
urlpatterns = [
url(_(r'^create/$'), views.order_create, name='order_create'),
# ..
]
编辑payment
应用的urls.py
文件,如下修改代码:
from django.utils.translation import gettext as _
urlpatterns = [
url(_(r'^process/$'), views.payment_process, name='process'),
url(_(r'^done/$'), views.payment_done, name='done'),
url(_(r'^canceled/$'), views.payment_canceled, name='canceled'),
]
我们不要翻译shop
应用的URL模式,因为它们由变量构建,不包括任何字面量。
打开终端,执行以下命令更新信息文件:
django-admin makemessages --all
确保开发服务器正在运行。在浏览器中打开http://127.0.0.1:8000/en/rosetta/
,然后点击Spanish
中的Myshop
链接。你可以使用Display
过滤器只显示没有翻译的字符串。在URL翻译中,一定要保留正则表达式中的特殊字符。翻译URL是一个精细的任务;如果你修改了正则表达式,就会破坏URL。
9.2.8 允许用户切换语言
因为我们现在提供了多种语言,所以我们应该让用户可以切换网站的语言。我们会在网站中添加一个语言选择器。语言选择器用链接显示可用的语言列表。
编辑shop/base.html
模板,找到以下代码:
<div id="header">
<a href="/" class="logo">{% trans "My shop" %}</a>
</div>
替换为以下代码:
<div id="header">
<a href="/" class="logo">{% trans "My shop" %}</a>
{% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %}
<div class="languages">
<p>{% trans "Languages" %}:</p>
<ul class="languages">
{% for language in languages %}
<li>
<a href="/{{ language.code }}" {% if language.code == LANGUAGE_CODE %} class="selected"{% endif %}>
{{ language.name_local }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
我们是这样构建语言选择器的:
- 我们首先用
{% load i18n %}
加载国际化标签。 - 我们用
{% get_current_language %}
标签查询当前语言。 - 我们用
{% get_available_languages %}
模板标签获得LANGUAGES
设置中定义的语言。 - 我们用
{% get_language_info_list %}
标签提供访问语言属性的便捷方式。 - 我们构建HTML列表显示所有可用的语言,并在当前激活语言上添加
selected
类属性。
我们用i18n
提供的模板标签,根据项目设置提供可用的语言。现在打开http://127.0.0.1:8000/
。你会看到网站右上角有语言选择器,如下图所示:
用户现在可以很容易的切换语言。
9.2.9 用django-parler翻译模型
Django没有为翻译模型提供好的解决方案。你必须实现自己的解决方案来管理不同语言的内容,或者使用第三方模块翻译模型。有一些第三方应用允许你翻译模型字段。每种采用不同的方法存储和访问翻译。其中一个是django-parler
。这个模块提供了一种非常高效的翻译模型的方式,并且它和Django管理站点集成的非常好。
django-parler
为每个模型生成包括翻译的独立的数据库表。这张表包括所有翻译后的字段,以及一个翻译所属的原对象的外键。因为每行存储单个语言的内容,所以它还包括一个语言字段。
9.2.9.1 安装django-parler
使用pip
命令安装django-parler
:
pip install django-parler
然后编辑项目的settings.py
文件,把parler
添加到INSTALLED_APPS
设置中。并在设置文件中添加以下代码:
PARLER_LANGUAGES = {
None: (
{'code': 'en', },
{'code': 'es', },
),
'default': {
'fallback': 'en',
'hide_untranslated': False,
}
}
这个设置定义了django-parler的可用语言en
和es
。我们指定默认语言是en
,并且指定django-parler不隐藏没有翻译的内容。
9.2.9.2 翻译模型字段
让我们为商品目录添加翻译。django-parler提供了一个TranslatableModel
模型类和一个TranslatedFields
包装器(wrapper)来翻译模型字段。编辑shop
应用的models.py
文件,添加以下导入:
from parler.models import TranslatableModel, TranslatedFields
然后修改Category
模型,让name
和slug
字段可翻译。我们现在还保留非翻译字段:
class Category(TranslatableModel):
name = models.CharField(max_length=200, db_index=True)
slug = models.SlugField(max_length=200, db_index=True, unique=True)
translations = TranslatedFields(
name = models.CharField(max_length=200, db_index=True),
slug = models.SlugField(max_length=200, db_index=True, unique=True)
)
现在Category
模型继承自TranslatableModel
,而不是models.Model
。并且name
和slug
字段都包括在TranslatedFields
包装器中。
编辑Product
模型,为name
,slug
和description
字段添加翻译。同样保留非翻译字段:
class Product(TranslatableModel):
name = models.CharField(max_length=200, db_index=True)
slug = models.SlugField(max_length=200, db_index=True)
description = models.TextField(blank=True)
translations = TranslatedFields(
name = models.CharField(max_length=200, db_index=True),
slug = models.SlugField(max_length=200, db_index=True),
description = models.TextField(blank=True)
)
category = models.ForeignKey(Category, related_name='products')
image = models.ImageField(upload_to='products/%Y/%m/%d', blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.PositiveIntegerField()
available = models.BooleanField(default=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
django-parler为每个TranslatableModel
模型生成另一个模型。图9.9中,你可以看到Product
模型的字段和生成的ProductTranslation
模型:
django-parler生成的ProductTranslation
模型包括name
,slug
和description
可翻译字段,一个language_code
字段,以及指向Product
对象的外键master
字段。从Product
到ProductTranslation
是一对多的关系。每个Product
对象会为每种语言生成一个ProductTranslation
对象。
因为Django为翻译使用了单独的数据库表,所以有些Django特性不能使用了。一个翻译后的字段不能用作默认的排序。你可以在查询中用翻译后的字段过滤,但你不能再ordering
元选项中包括翻译后的字段。编辑shop
应用的models.py
文件,注释Category
类中Meta
类的ordering
属性:
class Meta:
# ordering = ('name', )
verbose_name = 'category'
verbose_name_plural = 'categories'
我们还必须注释Product
类中Meta
类的index_together
属性,因为当前django-parler版本不提供验证它的支持:
class Meta:
ordering = ('-created', )
# index_together = (('id', 'slug'), )
你可以在这里阅读更多关于django-parler和Django兼容性的信息。
9.2.9.3 创建自定义数据库迁移
当你为翻译创建了新模型,你需要执行makemigrations
命令为模型生成数据库迁移,然后同步到数据库中。但是当你将已存在字段变为可翻译后,你的数据库中可能已经存在数据了。我们将把当前数据迁移到新的翻译模型中。因此,我们添加了翻译后的字段,但暂时保留了原来的字段。
为已存在字段添加翻译的流程是这样的:
- 我们为新的可翻译模型字段创建数据库迁移,并保留原来的字段。
- 我们构建一个自定义数据库迁移,从已存在字段中拷贝数据到翻译模型中。
- 我们从原来的模型中移除已存在的字段。
执行以下命令,为添加到Category
和Product
模型中的翻译字段创建数据库迁移:
python manage.py makemigrations shop --name "add_translation_model"
你会看到以下输出:
Migrations for 'shop':
shop/migrations/0002_add_translation_model.py
- Create model CategoryTranslation
- Create model ProductTranslation
- Change Meta options on category
- Alter index_together for product (0 constraint(s))
- Add field master to producttranslation
- Add field master to categorytranslation
- Alter unique_together for producttranslation (1 constraint(s))
- Alter unique_together for categorytranslation (1 constraint(s))
现在我们需要创建一个自定义数据库迁移,把已存在的数据拷贝到新的翻译模型中。使用以下命令创建一个空的数据库迁移:
python manage.py makemigrations --empty shop --name "migrate_translatable_fields"
你会看到以下输出:
Migrations for 'shop':
shop/migrations/0003_migrate_translatable_fields.py
编辑shop/migrations/0003_migrate_translatable_fields.py
文件,并添加以下代码:
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-05-17 01:18
from __future__ import unicode_literals
from django.db import models, migrations
from django.apps import apps
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
translatable_models = {
'Category': ['name', 'slug'],
'Product': ['name', 'slug', 'description'],
}
def forwards_func(apps, schema_editor):
for model, fields in translatable_models.items():
Model = apps.get_model('shop', model)
ModelTranslation = apps.get_model('shop', '{}Translation'.format(model))
for obj in Model.objects.all():
translation_fields = {field: getattr(obj, field) for field in fields}
translation = ModelTranslation.objects.create(
master_id=obj.pk,
language_code=settings.LANGUAGE_CODE,
**translation_fields
)
def backwards_func(apps, shcema_editor):
for model, fields in translatable_models.items():
Model = apps.get_model('shop', model)
ModelTranslation = apps.get_model('shop', '{}Translation'.format(model))
for obj in Model.objects.all():
translation = _get_translation(obj, ModelTranslation)
for field in fields:
setattr(obj, field, getattr(translation, field))
obj.save()
def _get_translation(obj, MyModelTranslation):
translation = MyModelTranslation.objects.filter(master_id=obj.pk)
try:
# Try default translation
return translation.get(language_code=settings.LANGUAGE_CODE)
except ObjectDoesNotExist:
# Hope there is a single translation
return translations.get()
class Migration(migrations.Migration):
dependencies = [
('shop', '0002_add_translation_model'),
]
operations = [
migrations.RunPython(forwards_func, backwards_func)
]
这个迁移包括forwards_func()
和backwards_func()
函数,其中包含要执行数据库同步和反转的代码。
迁移流程是这样的:
- 我们在
translatable_models
字典中定义模型和可翻译的字段。 - 要同步迁移,我们用
app.get_model()
迭代包括翻译的模型,来获得模型和它可翻译的模型类。 - 我们迭代数据库中所有存在的对象,并为项目设置中定义的
LANGUAGE_CODE
创建一个翻译对象。我们包括了一个指向原对象的ForeignKey
,以及从原字段中拷贝的每个可翻译字段。
backwards_func()
函数执行相反的操作,它查询默认的翻译对象,并把可翻译字段的值拷贝回原对象。
我们已经创建了一个数据库迁移来添加翻译字段,以及一个从已存在字段拷贝内容到新翻译模型的迁移。
最后,我们需要删除不再需要的原字段。编辑shop
应用的models.py
文件,移除Category
模型的name
和slug
字段。现在Category
模型字段是这样的:
class Category(TranslatableModel):
translations = TranslatedFields(
name = models.CharField(max_length=200, db_index=True),
slug = models.SlugField(max_length=200, db_index=True, unique=True)
)
移除Product
模型的name
,slug
和description
字段。它现在是这样的:
class Product(TranslatableModel):
translations = TranslatedFields(
name = models.CharField(max_length=200, db_index=True),
slug = models.SlugField(max_length=200, db_index=True),
description = models.TextField(blank=True)
)
category = models.ForeignKey(Category, related_name='products')
image = models.ImageField(upload_to='products/%Y/%m/%d', blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.PositiveIntegerField()
available = models.BooleanField(default=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
现在我们需要创建最后一个迁移,让修改生效。但是,如果我们尝试执行manage.py
工具,我们会看到一个错误,因为我们还没有让管理站点适配可翻译模型。让我们先修改管理站点。
9.2.9.4 在管理站点集成翻译
Django管理站点可以很好的跟django-parler集成。django-parler包括一个TranslatableAdmin
类,它覆写了Django提供的ModelAdmin
类,来管理模型翻译。
编辑shop
应用的admin.py
文件,添加以下导入:
from parler.admin import TranslatableAdmin
修改CategoryAdmin
和ProductAdmin
类,让它们从TranslatableAdmin
继承。django-parler还不知道prepopulated_fields
属性,但它支持相同功能的get_ prepopulated_fields()
方法。让我们相应的修改,如下所示:
from django.contrib import admin
from parler.admin import TranslatableAdmin
from .models import Category, Product
class CategoryAdmin(TranslatableAdmin):
list_display = ('name', 'slug')
def get_prepopulated_fields(self, request, obj=None):
return {'slug': ('name', )}
admin.site.register(Category, CategoryAdmin)
class ProductAdmin(TranslatableAdmin):
list_display = ('name', 'slug', 'price', 'stock', 'available', 'created', 'updated')
list_filter = ('available', 'created', 'updated')
list_editable = ('price', 'stock', 'available')
def get_prepopulated_fields(self, request, obj=None):
return {'slug': ('name', )}
admin.site.register(Product, ProductAdmin)
我们已经让管理站点可以与新的翻译模型一起工作了。现在可以同步模型修改到数据库中。
9.2.9.5 为模型翻译同步数据库迁移
适配管理站点之前,我们已经从模型中移除了旧的字段。现在我们需要为这个修改创建一个迁移。打开终端执行以下命令:
python manage.py makemigrations shop --name "remove_untranslated_fields"
你会看到以下输出:
Migrations for 'shop':
shop/migrations/0004_remove_untranslated_fields.py
- Change Meta options on product
- Remove field name from category
- Remove field slug from category
- Remove field description from product
- Remove field name from product
- Remove field slug from product
通过这次迁移,我们移除了原字段,保留了可翻译字段。
总结一下,我们已经创建了以下迁移:
- 添加可翻译字段到模型中
- 从原字段迁移已存在字段到可翻译字段
- 从模型中移除原字段
执行以下命令,同步我们创建的三个迁移:
python manage.py migrate shop
你会看到包括以下行的输出:
Applying shop.0002_add_translation_model... OK
Applying shop.0003_migrate_translatable_fields... OK
Applying shop.0004_remove_untranslated_fields... OK
现在模型已经跟数据库同步了。让我们翻译一个对象。
使用python manage.py runserver
启动开发服务器,然后在浏览器中打开http://127.0.0.1:8000/en/admin/shop/category/add/
。你会看到Add category
页面包括两个标签页,一个英语和一个西班牙语翻译:
现在你可以添加一个翻译,然后点击Save
按钮。确保切换标签页之前保存修改,否则输入的信息会丢失。
9.2.9.6 为翻译适配视图
我们必须让shop
的视图适配翻译的QuerySet
。在命令行中执行python manage.py shell
,看一眼如何检索和查询翻译字段。要获得当前语言的字段内容,你只需要与访问普遍模型字段一样访问该字段:
>>> from shop.models import Product
>>> product = Product.objects.first()
>>> product.name
'Black tea'
当你访问翻译后的字段时,它们已经被当前语言处理了。你可以在对象上设置另一个当前语言,来访问指定的翻译:
>>> product.set_current_language('es')
>>> product.name
'Té negro'
>>> product.get_current_language()
'es'
当使用filter()
执行QeurySet
时,你可以在相关的翻译对象上用translations__
语法过滤:
>>> Product.objects.filter(translations__name='Black tea')
[<Product: Black tea>]
你也可以用language()
管理器为对象检索指定语言:
>>> Product.objects.language('es').all()
[<Product: Té negro>, <Product: Té en polvo>, <Product: Té rojo>, <Product: Té verde>]
正如你所看到的,访问和查询翻译字段非常简单。
让我们适配商品目录视图。编辑shop
应用的views.py
文件,在product_list
视图中找到这一行代码:
category = get_object_or_404(Category, slug=category_slug)
替换为以下代码:
language = request.LANGUAGE_CODE
category = get_object_or_404(
Category,
translations__language_code=language,
translations__slug=category_slug)
接着编辑product_detail
视图,找到这一行代码:
product = get_object_or_404(Product, id=id, slug=slug, available=True)
替换为以下代码:
language = request.LANGUAGE_CODE
product = get_object_or_404(
Product,
id=id,
translations__language_code=language,
translations__slug=slug,
available=True)
现在product_list
和product_detail
视图已经适配了用翻译字段检索对象。启动开发服务器,并在浏览器中打开http://127.0.0.1:8000/es/
。你会看到商品列表页面,所有商品都已经翻译为西班牙语:
现在用slug
字段构建的每个商品的URL已经翻译为当前语言。例如,一个商品的西班牙语URL是http://127.0.0.1:8000/es/1/te-negro/
,而英语的URL是http://127.0.0.1:8000/en/1/black-tea/
。如果你导航到一个商品的详情页面,你会看到翻译后的URL和选中语言的内容,如下图所示:
如果你想进一步了解django-parler,你可以在这里找到所有文档。
你已经学习了如何翻译Python代码,模板,URL模式和模型字段。要完成国际化和本地化过程,我们还需要显示本地化格式的日期,时间和数组。
9.2.10 格式的本地化
根据用户的地区,你可能希望以不同格式显示日期,时间和数字。修改项目的settings.py
文件中的USE_L10N
设置为True
,可以启动本地化格式。
启用USE_L10N
后,当Django在模板中输出值时,会尝试使用地区特定格式。你可以看到,你的英文版网站中的十进制用点号分隔小数部分,而不在西班牙版本中显示为逗号。这是因为Django为es
地区指定了地区格式。你可以在这里查看西班牙格式配置。
通常你会设置USE_L10N
为True
,让Django为每个地区应用本地化格式。但是,有些情况下你可能不想使用地区化的值。当输出必须提供机器可读的JavaScript或JSON时,这一点尤其重要。
Django提供了{% localize %}
模板标签,运行你在模板块中开启或关闭本地化。这让你可以控制格式的本地化。要使用这个模板标签,你必须加载l10n
标签。下面这个例子展示了如何在模板中开启或关闭本地化:
{% load l10n %}
{% localize on %}
{{ value }}
{% endlocalize %}
{% localize off %}
{{ value }}
{% endlocalize %}
Django还提供了localize
和unlocalize
模板过滤器,强制或避免本地化一个值,如下所示:
{{ value|localize }}
{{ value|unlocalize }}
你还可以创建自定义格式过滤器来指定本地格式。你可以在这里查看更多关于格式本地化的信息。
9.2.11 用django-localflavor验证表单字段
django-localflavor是一个第三方模板,其中包含一组特定用途的功能,比如每个国家特定的表单字段或模型字段。验证本地区域,本地电话号码,身份证,社会安全号码等非常有用。这个包由一系列以ISO 3166国家代码命名的模块组成。
用以下命令安装django-localflavor:
pip install django-localflavor
编辑项目的settings.py
文件,把localflavor
添加到INSTALLED_APPS
设置中。
我们会添加一个美国(U.S)邮政编码字段,所以创建新订单时需要一个有效的美国邮政编码。
编辑orders
应用的forms.py
文件,如下修改:
from django import forms
from .models import Order
from localflavor.us.forms import USZipCodeField
class OrderCreateForm(forms.ModelForm):
postal_code = USZipCodeField()
class Meta:
model = Order
fields = ['first_name', 'last_name', 'email',
'address', 'postal_code', 'city']
我们从localflavor
的us
包中导入了USZipCodeField
字段,并把它用于OrderCreateForm
表单的postal_code
字段。在浏览器中打开http://127.0.0.1:8000/en/orders/create/
,尝试输入一个3个字母的邮政编码。你会看USZipCodeField
抛出的验证错误:
Enter a zip code in the format XXXXX or XXXXX-XXXX.
这只是一个简单的例子,说明如何在你的项目中使用localflavor
的自定义字段进行验证。localflavor
提供的本地组件对于让你的应用适应特定国家非常有用。你可以在这里阅读django-localflavor
文档,查看每个国家所有可用的本地组件。
接下来,我们将在商店中构建一个推荐引擎。
9.3 构建推荐引擎
推荐引擎是一个预测用户对商品的偏好或评价的系统。系统根据用户行为和对用户的了解选择商品。如今,很多在线服务都使用推荐系统。它们帮助用户从大量的可用数据中选择用户可能感兴趣的内容。提供良好的建议可以增强用户参与度。电子商务网站还可以通过推荐相关产品提高销量。
我们将创建一个简单,但强大的推荐引擎,来推测用户通常会一起购买的商品。我们将根据历史销售确定通常一起购买的商品,来推荐商品。我们将在两个不同的场景推荐补充商品:
- 商品详情页面:我们将显示一个通常与给定商品一起购买的商品列表。它会这样显示:购买了这个商品的用户还买了X,Y,Z。我们需要一个数据结构,存储每个商品与显示的商品一起购买的次数。
- 购物车详情页面:根据用户添加到购物车中的商品,我们将推荐通常与这些商品一起购买的商品。这种情况下,我们计算的获得相关商品的分数必须汇总。
我们将使用Redis存储一起购买的商品。记住,你已经在第六章中使用了Redis。如果你还没有安装Redis,请参考第六章。
9.3.1 根据之前的购买推荐商品
现在,我们将根据用户已经添加到购物车中的商品来推荐商品。我们将在Redis中为网站中每个出售的商品存储一个键。商品键会包括一个带评分的Redis有序集。每次完成一笔新的购买,我们为每个一起购买的商品的评分加1。
当一个订单支付成功后,我们为购买的每个商品存储一个键,其中包括属于同一个订单的商品有序集。这个有序集让我们可以为一起购买的商品评分。
编辑项目的settings.py
文件,编辑以下设置:
REDIS_HOST = 'localhost'
REDIS_PORT = 6379
REDIS_DB = 1
这是建立一个Redis服务器连接必须的设置。在shop
应用目录中创建一个recommender.py
文件,添加以下代码:
import redis
from django.conf import settings
from .models import Product
# connect to Redis
r = redis.StrictRedis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
db=settings.REDIS_DB
)
class Recommender:
def get_product_key(self, id):
return 'product:{}:purchased_with'.format(id)
def products_bought(self, products):
product_ids = [p.id for p in products]
for product_id in product_ids:
for with_id in product_ids:
# get the other products bought with each product
if product_id != with_id:
# increment score for product purchased together
r.zincrby(self.get_product_key(product_id), with_id, amount=1)
Recommender
类允许我们存储购买的商品,以及为给定的商品检索商品推荐。get_product_key()
方法接收一个Product
对象的ID,然后为存储相关商品的有序集构建Redis键,看起来是这样的:product:[id]:purchased_with
。
product_bought()
方法接收一个一起购买(也就是属于同一个订单)的Product
对象列表。我们在这个方法中执行以下任务:
- 我们获得给定的
Product
对象的商品ID。 - 我们迭代商品ID。对于每个ID,我们迭代商品ID,并跳过同一个商品,这样我们获得了与每个商品一起购买的商品。
- 我们用
get_product_id()
方法获得每个购买的商品的Redis商品键。对于一个ID是33的商品,这个方法返回的键是product:33:purchased_with
。这个键用于包括与这个商品一起购买的商品ID的有序集。 - 我们将ID包含在有序集中的商品评分加1。这个评分表示其它商品与给定商品一起购买的次数。
因此这个方法可以保存一起购买的商品,并对它们评分。现在,我们需要一个方法检索与给定的商品列表一起购买的商品。在Recommender
类中添加suggest_products_for()
方法:
def suggest_products_for(self, products, max_results=6):
product_ids = [p.id for p in products]
if len(products) == 1:
# only 1 product
suggestions = r.zrange(
self.get_product_key(product_ids[0]),
0, -1, desc=True
)[:max_results]
else:
# generate a temporary key
flat_ids = ''.join([str(id) for id in product_ids])
tmp_key = 'tmp_{}'.format(flat_ids)
# multiple products, combine scores of all products
# store the resulting sored set in a temporary key
keys = [self.get_product_key(id) for id in product_ids]
r.zunionstore(tmp_key, keys)
# remove ids for the products the recommendation is for
r.zrem(tmp_key, *product_ids)
# get the product ids by their score, descendant sort
suggestions = r.zrange(tmp_key, 0, -1, desc=True)[:max_results]
# remove the temporary key
r.delete(tmp_key)
suggested_products_ids = [int(id) for id in suggestions]
# get suggested products and sort by order of appearance
suggested_products = list(Product.objects.filter(id__in=suggested_products_ids))
suggested_products.sort(key=lambda x: suggested_products_ids.index(x.id))
return suggested_products
suggest_products_for()
方法接收以下参数:
-
products
:要获得推荐商品的商品列表。它可以包括一个或多个商品。 -
max_results
:一个整数,表示返回的推荐商品的最大数量。
在这个方法中,我们执行以下操作:
- 我们获得给定商品对象的商品ID。
- 如果只给定了一个商品,我们检索与该商品一起购买的商品ID,并按它们一起购买的总次数排序。我们用Redis的
ZRANGE
命令进行排序。我们限制结果数量为max_results
参数指定的数量(默认是6)。 - 如果给定的商品多余1个,我们用商品ID生成一个临时的Redis键。
- 我们组合每个给定商品的有序集中包括的商品,并求和所有评分。通过Redis的
ZUNIONSTORE
命令实现这个操作。ZUNIONSTORE
命令用给定的键执行有序集的并集,并在新的Redis键中存储元素的评分总和。你可以在这里阅读更多关于这个命令的信息。我们在一个临时键中存储评分和。 - 因为我们正在汇总评分,所以我们得到的有可能是正在获得推荐商品的商品。我们用
ZREM
命令从生成的有序集中移除它们。 - 我们从临时键中检索商品ID,并用
ZRANGE
命令根据评分排序。我们限制结果数量为max_results
参数指定的数量。然后我们移除临时键。 - 最后,我们用给定的ID获得
Product
对象,并按ID同样的顺序进行排序。
为了更实用,让我们再添加一个清除推荐的方法。在Recommender
类中添加以下方法:
def clear_purchases(self):
for id in Product.objects.values_list('id', flat=True):
r.delete(self.get_product_key(id))
让我们试试推荐引擎。确保数据库中包括几个Product
对象,并在终端使用以下命令初始化Redis服务:
src/redis-server
打开另一个终端,执行python manage.py shell
,输入下面代码检索商品:
from shop.models import Product
black_tea = Product.objects.get(translations__name='Black tea')
red_tea = Product.objects.get(translations__name='Red tea')
green_tea = Product.objects.get(translations__name='Green tea')
tea_powder = Product.objects.get(translations__name='Tea powder')
然后添加一些测试购买到推荐引擎中:
from shop.recommender import Recommender
r = Recommender()
r.products_bought([black_tea, red_tea])
r.products_bought([black_tea, green_tea])
r.products_bought([red_tea, black_tea, tea_powder])
r.products_bought([green_tea, tea_powder])
r.products_bought([black_tea, tea_powder])
r.products_bought([red_tea, green_tea])
我们已经存储了以下评分:
black_tea: red_tea (2), tea_powder (2), green_tea (1)
red_tea: black_tea (2), tea_powder (1), green_tea (1)
green_tea: black_tea (1), tea_powder (1), red_tea(1)
tea_powder: black_tea (2), red_tea (1), green_tea (1)
让我们看一眼单个商品的推荐商品:
>>> r.suggest_products_for([black_tea])
[<Product: Tea powder>, <Product: Red tea>, <Product: Green tea>]
>>> r.suggest_products_for([red_tea])
[<Product: Black tea>, <Product: Tea powder>, <Product: Green tea>]
>>> r.suggest_products_for([green_tea])
[<Product: Black tea>, <Product: Tea powder>, <Product: Red tea>]
>>> r.suggest_products_for([tea_powder])
[<Product: Black tea>, <Product: Red tea>, <Product: Green tea>]
正如你所看到的,推荐商品的顺序基于它们的评分排序。让我们用多个商品的评分总和获得推荐商品:
>>> r.suggest_products_for([black_tea, red_tea])
[<Product: Tea powder>, <Product: Green tea>]
>>> r.suggest_products_for([green_tea, red_tea])
[<Product: Black tea>, <Product: Tea powder>]
>>> r.suggest_products_for([tea_powder, black_tea])
[<Product: Red tea>, <Product: Green tea>]
你可以看到,推荐商品的顺序与评分总和匹配。例如,black_tea
和red_tea
的推荐商品是tea_powder(2+1)
和green_tea(1+1)
。
我们已经确认推荐算法如期工作了。让我们为网站的商品显示推荐。
编辑shop
应用的views.py
文件,并添加以下导入:
from .recommender import Recommender
在product_detail()
视图的render()
函数之前添加以下代码:
r = Recommender()
recommended_products = r.suggest_products_for([product], 4)
我们最多获得4个推荐商品。现在product_detail
视图如下所示:
from .recommender import Recommender
def product_detail(request, id, slug):
language = request.LANGUAGE_CODE
product = get_object_or_404(
Product,
id=id,
translations__language_code=language,
translations__slug=slug,
available=True)
cart_product_form = CartAddProductForm()
r = Recommender()
recommended_products = r.suggest_products_for([product], 4)
return render(
request,
'shop/product/detail.html',
{
'product': product,
'cart_product_form': cart_product_form,
'recommended_products': recommended_products
}
)
现在编辑shop
应用的shop/product/detail.html
模板,在{{ product.description|linebreaks }}
之后添加以下代码:
{% if recommended_products %}
<div class="recommendations">
<h3>{% trans "People who bought this also bought" %}</h3>
{% for p in recommended_products %}
<div class="item">
<a href="{{ p.get_absolute_url }}">
![]({% if p.image %}{{ p.image.url }}{% else %}{% static )
</a>
<p><a href="{{ p.get_absolute_url }}">{{ p.name }}</a></p>
</div>
{% endfor %}
</div>
{% endif %}
使用python manage.py runserver
启动开发服务器,并在浏览器中打开http://127.0.0.1:8000/en/
。点击任何一个商品显示详情页面。你会看到商品下面的推荐商品,如下图所示:
接下来我们在购物车中包括商品推荐。基于用户添加到购物车中的商品生成推荐商品。编辑cart
应用的views.py
文件,添加以下导入:
from shop.recommender import Recommender
然后编辑cart_detail
视图,如下所示:
def cart_detail(request):
cart = Cart(request)
for item in cart:
item['update_quantity_form'] = CartAddProductForm(
initial={'quantity': item['quantity'], 'update': True})
coupon_apply_form = CouponApplyForm()
r = Recommender()
cart_products = [item['product'] for item in cart]
recommended_products = r.suggest_products_for(cart_products, max_results=4)
return render(
request, 'cart/detail.html',
{
'cart': cart,
'coupon_apply_form': coupon_apply_form,
'recommended_products': recommended_products
}
)
编辑cart
应用的cart/detail.html
模板,在</table>
标签之后添加以下代码:
{% if recommended_products %}
<div class="recommendations cart">
<h3>{% trans "People who bought this also bought" %}</h3>
{% for p in recommended_products %}
<div class="item">
<a href="{{ p.get_absolute_url }}">
![]({% if p.image %}{{ p.image.url }}{% else %}{% static )
</a>
<p><a href="{{ p.get_absolute_url }}">{{ p.name }}</a></p>
</div>
{% endfor %}
</div>
{% endif %}
在浏览器中打开http://127.0.0.1:8000/en/
,并添加一些商品到购物车中。当你导航到http://127.0.0.1:8000/en/cart/
,你会看到购物车中商品合计的推荐商品,如下图所示:
恭喜你!你已经用Django和Redis构建了一个完整的推荐引擎。
9.4 总结
在本章中,你使用会话创建了优惠券系统。你学习了如何进行国际化和本地化。你还用Redis构建了一个推荐引擎。
在下一章中,你会开始一个新的项目。你会通过Django使用基于类的视图构建一个在线学习平台,你还会创建一个自定义的内容管理系统。
网友评论