Django 处处支持 Unicode 数据。
本文档告诉你,如果你要编写使用 ASCII 以外编码的数据或模板的应用程序,你需要知道什么。
确保你的数据库被配置成能够存储任意字符串数据。通常,这意味着给它一个 UTF-8 或 UTF-16 的编码。如果你使用更严格的编码——例如,latin1(iso8859-1)——你将无法在数据库中存储某些字符,信息将丢失。
所有 Django 的数据库后端都会自动将字符串转换为合适的编码来与数据库对话。它们也会自动将从数据库中获取的字符串转换为字符串。你甚至不需要告诉 Django 你的数据库使用什么编码:这都是透明的处理。
更多内容,请看下面的“数据库 API”一节。
当你在 Django 中使用字符串时——例如,在数据库查询、模板渲染或其他任何地方——你有两种选择来编码这些字符串。你可以使用普通字符串或字节字符串(以 'b' 开头)。
警告
一个字节字符串并没有携带任何关于其编码的信息。为此,我们必须做一个假设,Django 假设所有的字节字符串都是 UTF-8 编码。
如果你传递给 Django 的字符串是用其他格式编码的,那么事情会以有趣的方式出错。通常情况下,Django 会在某些时候引发一个 UnicodeDecodeError
。
如果你的代码只使用 ASCII 数据,那么使用普通的字符串是安全的,可以随意传递它们,因为 ASCII 是 UTF-8 的一个子集。
不要傻傻的认为如果你将 DEFAULT_CHARSET
设置为 'utf-8'
以外的其他编码,你就可以在你的字节字符串中使用其他编码。DEFAULT_CHARSET
只适用于模板渲染后生成的字符串(和电子邮件)。Django 对内部的字节字符串总是采用 UTF-8 编码。原因是 DEFAULT_CHARSET
的配置实际上并不在你的控制之下(如果你是应用开发者的话)。它是在安装和使用你的应用程序的人的控制之下——如果这个人选择了不同的配置,你的代码仍然必须继续工作。因此,它不能依赖该配置。
在大多数情况下,当 Django 处理字符串时,它会在做其他事情之前将它们转换为字符串。所以,一般来说,如果你传入一个字节字符串,要准备好在结果中收到一个字符串。
除了字符串和字节字符串之外,还有第三种类型的字符串类对象,你在使用 Django 时可能会遇到。框架的国际化特性引入了“惰性翻译”的概念——一个已经被标记为翻译的字符串,但其实际的翻译结果直到该对象被用于字符串时才被确定。这个功能在以下情况下非常有用:在使用字符串之前,翻译的 locale 是未知的,即使该字符串可能是在第一次导入代码时创建的。
通常情况下,你不必担心惰性翻译的问题。只是要注意,如果你检查一个对象,并且它声称是 django.utils.functional.__proxy__
对象,它就是一个惰性翻译。调用 str()
作为参数调用 str()
将生成一个当前语言环境下的字符串。
关于惰性翻译对象的更多细节,请参考 国际化 文档。
因为有些字符串操作会反复出现,所以 Django 提供了一些有用的函数,这些函数可以使处理字符串和字节字符串对象变得更加容易。
django.utils.encoding
模块包含了一些函数,这些函数可以方便地在字符串和字节字符串之间来回转换。
smart_str(s, encoding='utf-8', strings_only=False, errors='strict')
将输入转换为字符串。encoding
参数指定了输入的编码。(例如,Django 内部在处理表单输入数据时使用了这个参数,这些数据可能不是 UTF-8 编码的。) strings_only
参数,如果设置为 True,将导致 Python 数字、布尔值和 None
不会被转换为字符串(它们保持原来的类型)。errors
参数取 Python 的 str()
函数接受的任何值来处理错误。force_str(s, encoding='utf-8', strings_only=False, errors='strict')
几乎在所有情况下都和 smart_str()
相同。区别在于当第一个参数是 惰性翻译 实例时。smart_str()
保留了惰性翻译,而 force_str()
将这些对象强制为字符串(导致翻译执行)。通常,你会想使用 smart_str()
。然而,force_str()
在模板标签和过滤器中是有用的,它们绝对 必须 有一个字符串来工作,而不仅仅是可以转换为字符串的东西。smart_bytes(s, encoding='utf-8', strings_only=False, errors='strict')
本质上与 smart_str()
相反。它迫使第一个参数变成一个字节字符串。strings_only
参数的行为与 smart_str()
和 force_str()
相同。这与 Python 内置的 str()
函数的语义略有不同,但在 Django 内部的一些地方需要区分。通常,你只需要使用 force_str()
。在任何可能是字符串或字节字符串的输入数据上尽可能早地调用它,从那时起,你可以将结果视为始终是字符串。
网络框架必须处理 URL(是 IRI 的一种)。URL 的一个要求是,它们只能使用 ASCII 字符进行编码。然而,在国际环境中,您可能需要从 IRI 构建一个 URL —— 非常宽泛地讲,一个可以包含 Unicode 字符的 URI。使用这些函数来引用 IRI 并将其转换为 URI。
django.utils.encoding.iri_to_uri()
函数,它实现了 RFC 3987#section-3.1 所要求的从 IRI 到 URI 的转换。urllib.parse.quote()
和 urllib.parse.quote_plus()
函数。这两组函数的目的略有不同,因此必须将它们区分开来。通常,你会在 IRI 或 URI 路径的个别部分使用 quote()
,这样任何保留的字符如“&”或“%”都会被正确编码。然后,你将 iri_to_uri()
应用于整个 IRI,它将任何非 ASCII 字符转换为正确的编码值。
注解
从技术上讲,说 iri_to_uri()
实现了 IRI 规范中的全部算法是不正确的。它并不(尚未)执行算法的国际域名编码部分。
iri_to_uri()
函数不会改变 URL 中允许的 ASCII 字符。因此,例如,当传递给 iri_to_uri()
时,字符“%”不会被进一步编码。这意味着你可以将一个完整的 URL 传递给这个函数,它不会弄乱查询字符串或类似的东西。
举个例子可以说明这里的情况:
>>> from urllib.parse import quote
>>> from django.utils.encoding import iri_to_uri
>>> quote('Paris & Orléans')
'Paris%20%26%20Orl%C3%A9ans'
>>> iri_to_uri('/favorites/François/%s' % quote('Paris & Orléans'))
'/favorites/Fran%C3%A7ois/Paris%20%26%20Orl%C3%A9ans'
如果你仔细观察,你可以看到第二个例子中由 quote()
生成的部分在传递给 iri_to_uri()
时没有被双引号。这是一个非常重要和有用的功能。这意味着你可以构建你的 IRI,而不用担心它是否包含非 ASCII 字符,然后在最后调用 iri_to_uri()
对结果进行处理。
类似的,Django 提供了 django.utils.encoding.uri_to_iri()
,它按照 RFC 3987#section-3.2 实现了从 URI 到 IRI 的转换。
举例说明:
>>> from django.utils.encoding import uri_to_iri
>>> uri_to_iri('/%E2%99%A5%E2%99%A5/?utf8=%E2%9C%93')
'/♥♥/?utf8=✓'
>>> uri_to_iri('%A9hello%3Fworld')
'%A9hello%3Fworld'
在第一个例子中,UTF-8 字符没有被引用。在第二个例子中,百分比编码保持不变,因为它们位于有效的 UTF-8 范围之外或代表一个保留字符。
iri_to_uri()
和 uri_to_iri()
函数都是幂等的,这意味着以下内容始终为真:
iri_to_uri(iri_to_uri(some_string)) == iri_to_uri(some_string)
uri_to_iri(uri_to_iri(some_string)) == uri_to_iri(some_string)
所以你可以安全地在同一个 URI/IRI 上多次调用它,而不会有重复引用问题的风险。
因为所有的字符串都是以 str
对象的形式从数据库中返回的,所以当 Django 从数据库中检索数据时,基于字符的模型字段(CharField、TextField、URLField 等)将包含 Unicode 值。这 总是 这样的情况,即使数据可以放入 ASCII 字节字符串中。
你可以在创建模型或填充字段时传入字节字符串,Django 会在需要时将其转换为字符串。
get_absolute_url()
中注意¶URL 只能包含 ASCII 字符。如果你从可能是非 ASCII 码的数据中构建一个 URL,要注意将结果编码成适合 URL 的方式。reverse()
函数会自动为你处理这个问题。
如果你是手动构建一个 URL(即 不 使用 reverse()
函数),你就需要自己进行编码。在这种情况下,请使用 上面 记载的 iri_to_uri()
和 quote()
函数。例如:
from urllib.parse import quote
from django.utils.encoding import iri_to_uri
def get_absolute_url(self):
url = '/person/%s/?x=0&y=0' % quote(self.location)
return iri_to_uri(url)
即使 self.location
是类似于“Jack visited Paris & Orléans”,这个函数也会返回一个正确编码的 URL。(事实上,在上面的例子中,iri_to_uri()
的调用并不是绝对必要的,因为所有非 ASCII 字符都会在第一行的引号中被删除。)
手动创建模板时使用字符串:
from django.template import Template
t2 = Template('This is a string template.')
但常见的情况是从文件系统中读取模板。如果你的模板文件没有采用 UTF-8 编码存储,请调整 TEMPLATES
配置。内置的 django
后端提供了 'file_charset'
选项,可以改变从磁盘读取文件时使用的编码。
DEFAULT_CHARSET
配置控制了渲染模板的编码。默认配置为 UTF-8。
在编写自己的模板标签和过滤器时,要记住几个小技巧:
render()
方法和模板过滤器中返回字符串:force_str()
,而不是 smart_str()
。标签渲染和过滤器调用是在模板渲染时发生的,所以推迟将懒惰翻译对象转换为字符串没有好处。这时只用字符串工作更容易。如果你打算允许用户上传文件,你必须确保用于运行 Django 的环境被配置成可以使用非 ASCII 文件名。如果你的环境配置不正确,当保存文件名包含非 ASCII 字符的文件时,你会遇到 UnicodeEncodeError
异常。
文件系统对 UTF-8 文件名的支持有所不同,可能取决于环境。在交互式 Python shell 中运行检查你当前的配置:
import sys
sys.getfilesystemencoding()
这应该输出“UTF-8”。
LANG
环境变量负责设置 Unix 平台上的预期编码。请查阅操作系统和应用服务器的文档,了解设置这个变量的适当语法和位置。
在你的开发环境中,你可能需要在你的 ~.bashrc
中添加一个类似于这样的设置:
export LANG="en_US.UTF-8"
HTML 表单提交是一个棘手的领域。无法保证提交的数据会包含编码信息,这意味着框架可能不得不猜测提交数据的编码。
Django 采用了一种“惰性”的方式来解码表单数据。HttpRequest
对象中的数据只有在你访问它时才会被解码。事实上,大部分数据根本没有被解码。只有 HttpRequest.GET
和 HttpRequest.POST
数据结构有任何解码应用。这两个字段将作为 Unicode 数据返回其成员。HttpRequest
的所有其他属性和方法将完全按照客户端提交的数据返回。
默认情况下,DEFAULT_CHARSET
配置被用作表单数据的假定编码。如果你需要为一个特定的表单改变这个配置,你可以在一个 HttpRequest
实例上设置 encoding
属性。例如:
def some_view(request):
# We know that the data must be encoded as KOI8-R (for some reason).
request.encoding = 'koi8-r'
...
你甚至可以在访问了 request.GET
或 request.POST
之后改变编码,所有后续的访问都将使用新的编码。
大多数开发人员不需要担心更改表单编码,但对于那些与编码无法控制的传统系统对话的应用程序来说,这是一个有用的功能。
Django 不会对文件上传的数据进行解码,因为这些数据通常被视为字节的集合,而不是字符串。任何自动解码都会改变字节流的含义。
5月 26, 2021