About
RSS

Bit Focus


多国语言环境下 Jinja2 与 strftime 互动异常退治纪要

Posted at 2013-04-13 09:21:26 | Updated at 2018-02-21 23:17:29

    不少语言或者库或多或少都遭遇过乱码问题. 然而这一点在 Python 里面表现得比较另类, 一般来说只要贯彻 UTF-8 到底的思路, 不怎么可能遇到一大波乱码袭来的情况. 只是, 偶尔会直接因为字符串类型设置不对而直接抛运行期异常.
    常在河边走, 湿鞋是顺理成章的事情. 这次遇到的情况简报如下
# encoding=utf-8

import jinja2
import datetime

now = datetime.datetime.utcnow()

print jinja2.Template(u'''当前月份 {{ date.strftime('%Y 年 %m 月') }}''').render(date=now)
运行这一段代码, Python 直接扔出来一坨
Traceback (most recent call last):
  File "test.py", line 8, in <module>
    print jinja2.Template(u'''当前月份 {{ date.strftime('%Y 年 %m 月') }}''').render(date=now)
  File "/usr/local/lib/python2.7/dist-packages/Jinja2-2.6-py2.7.egg/jinja2/environment.py", line 894, in render
    return self.environment.handle_exception(exc_info, True)
  File "<template>", line 1, in top-level template code
UnicodeEncodeError: 'ascii' codec can't encode character u'\u5e74' in position 3: ordinal not in range(128)
    简单地说, 就是 Jinja2 在刷什么东西的过程中挂了, 因为字符串里面混入了什么奇怪的东西.

    一番尝试并寻找之后, 首先发现第一个问题, datetime.datetime.strftime 函数接受奇怪的字符串会出问题, 比如先来试试看这个
now = datetime.datetime.utcnow()
print now.strftime('%Y 年 %m 月')
    结果什么问题都没有, 突然有人品爆了的感觉, 这个好像很不科学的样子啊. 再仔细对比代码找找茬才发现要这样才能挂掉程序
now = datetime.datetime.utcnow()
print now.strftime(u'%Y 年 %m 月')
    这前缀 uunicode 类型怎么说也坑太深了的感觉吧. (别扯什么 Python strunicode 类型异与同 21 天从入门到精通什么的, 别的语言都没这问题 Python 确有, 这应该就是 Python 的问题了吧.) 在这种情况下如果要用 unicode 还得转一道
now = datetime.datetime.utcnow()
print now.strftime(u'%Y 年 %m 月'.encode('utf-8'))
    之所以有这个搞法是因为可能拿到的格式化字符串是 unicode 类型, 所以加一下 encode.
    这看起来简直像是不明来历的野生 API, 连自家的 unicode 都不支持么. 可是注意 Jinja2 模板字符串里面那个格式化字符串并没有加任何前缀. 而貌似 Jinja2 也觉得 Python unicode 是个坑, 因此并不需要放字符串前缀, 相反放了还出错 (u'string' 会被认为是变量 u 与字符串 'string' 放一起中间没加二元运算符).

    搞了这么久问题还是没进展, 所以得采用其它方式更深入地调教一下. 那么, 把格式化函数外面套一层, 然后把套在外面的一层也搅进 Jinja2, 如下
# encoding=utf-8

import jinja2
import datetime

def strftime(dt, fmt):
    print '0'
    x = dt.strftime(fmt)
    print '1'
    return x

now = datetime.datetime.utcnow()

print jinja2.Template(u'''当前月份 {{ strftime(date, '%Y 年 %m 月') }}'''
                        ).render(date=now, strftime=strftime)
    简单诊断发现问题出在 x = dt.strftime(fmt) 这句. 好吧这 API 不知道适可而止一点么, 还是说喂过去的参数有什么问题? 那么看看从 Jinja2 传过来的东西到底是什么东西
def strftime(dt, fmt):
    print type(fmt)
    return dt.strftime(fmt)
    结果就被控制台上迎面一记 <type 'unicode'> 击倒. Jinja2 你这是要跟 strftime 合起来玩我啊. 好吧, 那只能这样了
def strftime(dt, fmt):
    print '0'
    x = dt.strftime(fmt.encode('utf-8'))
    print '1'
    return x
    好消息是这一来 0 跟 1 都打出来了, 而坏消息是, 还是挂. 这熊孩子模板库到底是要闹哪样啊!

    行了, 那一个一个地攻略, strftime 你先一边去
def strftime(dt, fmt):
    return '2013 年 4 月'
挂. 而
def strftime(dt, fmt):
    return u'2013 年 4 月'
正常.
    被抓到了尾巴了吧. Jinja2 不接受含有非 ASCII 的 str 而只能返回 unicode. 好吧, 那这么一来, 线索又回到 strftime 了, 这函数一定是得出了什么不对劲的东西. 可以如下做个简单试验
now = datetime.datetime.utcnow()
s = now.strftime('%Y 年 %m 月')
print type(s)
    结果竟然是 <type 'str'> 啊, 一瞬三观尽毁的感觉, 不带这么玩脱的吧. 行, 那么正确的打码方式是在返回的 str 上调用 decode 函数, 得到 unicode 再塞给 Jinja2. 完整代码如下
# encoding=utf-8

import jinja2
import datetime

def strftime(dt, fmt):
    return dt.strftime(fmt.encode('utf-8')).decode('utf-8')

now = datetime.datetime.utcnow()

print jinja2.Template(u'''当前月份 {{ strftime(date, '%Y 年 %m 月') }}'''
                        ).render(date=now, strftime=strftime)
    多谢 @kavinyao 同学给了一个更专业的解答: 弄一个 Jinja2 过滤器. 如下
# encoding=utf-8

import jinja2
import datetime

def strftime(dt, fmt):
    return dt.strftime(fmt.encode('utf-8')).decode('utf-8')

env = jinja2.Environment(loader=jinja2.DictLoader(
            dict(test=u'''当前月份 {{ date|strftime('%Y 年 %m 月') }}''')))
env.filters['strftime'] = strftime
t = env.get_template('test')
print t.render(date=datetime.datetime.utcnow())

Post tags:   Python  Jinja2  strftime  Unicode

Leave a comment:




Creative Commons License Your comment will be licensed under
CC-NC-ND 3.0


. Back to Bit Focus
NijiPress - Copyright (C) Neuron Teckid @ Bit Focus
About this site