Unicode 数据

Django 处处支持 Unicode 数据。

本文档告诉你,如果你要编写使用 ASCII 以外编码的数据或模板的应用程序,你需要知道什么。

创建数据库

确保你的数据库被配置成能够存储任意字符串数据。通常,这意味着给它一个 UTF-8 或 UTF-16 的编码。如果你使用更严格的编码——例如,latin1(iso8859-1)——你将无法在数据库中存储某些字符,信息将丢失。

  • MySQL 用户,请参考 MySQL 手册 ,了解如何设置或更改数据库字符集编码。
  • PostgreSQL 用户,请参考 PostgreSQL 手册 (PostgreSQL 9 中的 22.3.2 节),了解以正确编码创建数据库的细节。
  • 关于如何设置(第 2 节 )或改变(第 11 节 )数据库字符集编码,请参考 Oracle 手册
  • SQLite 用户,你不需要做什么。SQLite 一直使用 UTF-8 作为内部编码。

所有 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()。在任何可能是字符串或字节字符串的输入数据上尽可能早地调用它,从那时起,你可以将结果视为始终是字符串。

URI 和 IRI 处理

网络框架必须处理 URL(是 IRI 的一种)。URL 的一个要求是,它们只能使用 ASCII 字符进行编码。然而,在国际环境中,您可能需要从 IRI 构建一个 URL —— 非常宽泛地讲,一个可以包含 Unicode 字符的 URI。使用这些函数来引用 IRI 并将其转换为 URI。

这两组函数的目的略有不同,因此必须将它们区分开来。通常,你会在 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.GETHttpRequest.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.GETrequest.POST 之后改变编码,所有后续的访问都将使用新的编码。

大多数开发人员不需要担心更改表单编码,但对于那些与编码无法控制的传统系统对话的应用程序来说,这是一个有用的功能。

Django 不会对文件上传的数据进行解码,因为这些数据通常被视为字节的集合,而不是字符串。任何自动解码都会改变字节流的含义。