Supervisor引起的UnicodeEncodeError

网站之前的文件上传流程会把上传文件的文件名统一修改成UUID字符串,这样做避免了一些文件名造成的麻烦不过也让文件名不易被识别(搜索引擎喜欢有含义的文件名)。考虑到文件上传部分只向管理员开放,所以修改了代码让上传后的文件保持原有名称。

在本机上修改代码后可以正常运行,但是同步到服务器上后就一直出现

UnicodeEncodeError: 'ascii' codec can't encode character
的错误(在上传文件时出现)

问题分析

一开始认为GIT在合并分支过程中出问题,代码没有正确的合并到Master分支——这个猜想很快被排除了;

然后又排除了系统编码(我开发是在MAC OS或是WINDOWS系统上,服务器是UBUNTU)的问题;

后来尝试直接在服务器上用python manage.py runserver lcfcn.com:8080把网站运行起来,结果错误消失了

看来问题不在Django身上,我用django UnicodeEncodeError with nginx, django UnicodeEncodeError with nginx gunicorn作关键词搜索相关信息(服务器上Django运行环境是Nginx+Gunicorn+Supervisor),结果也没找到解决问题的办法,后来看到这篇文章里面提到了Supervisor,赶紧在关键词中加上“Supervisor”又Google了一下,发现果然是它的问题。

解决方案

Step One 第一步

/etc/supervisor/supervisord.conf(推荐)或者项目Supervisor配置文件的[program:program_name]中加上:
environment=LANG=en_US.UTF-8,LC_ALL=en_US.UTF-8

Step Two 第二步

运行如下命令重新启动Supervisor
$ /etc/init.d/supervisord stop
$ /etc/init.d/supervisord start
WARNING!!! 仅运行/etc/init.d/supervisord restart或者简单地重起项目是不会生效的!

完成这两步后,(在你的网站通过manage.py runserver能正常运作的前提下,)问题应该能解决掉。

另外,在查阅资料过程中也看到不少值得注意的类似问题,例如Nginx中的Charset设定、系统的编码设定等都有可能造成类似的错误。

参考资料

http://tech.barszcz.info/2012/05/17/django-unicodeencodeerror-when-uploading-files/

http://albertoconnor.ca/blog/2012/Jul/21/unicodeencodeerror-when-uploading-files-django-usi

http://stackoverflow.com/a/10986010

Homebrew和Homebrew cask

Homebrew和Homebrew cask的关系

Homebrew是Mac下的软件包管理工具,就像CentOS下yum或者Ubuntu中的apt一样;

Homebrew Cask是Homebrew的一个扩展;

Homebrew一般用来安装命令行界面(CLI)的程序,例如mysql, php等;Homebrew Cask一般来用安装图行界面(GUI)的程序,例如QQ, Chrome之类;


说点题外话,感觉Mac App Store实在是多余的东西,版本更新慢,很多著名软件都不愿意在上面上架,除非安装XCODE这样的官方产品不然实在没有什么存在的必要。MAC的软件安装方式本来就不是很统一,多一个MAS后想装个软件都要动脑子想是从MAS上装还是去官网装,很不爽。

再记一点和MAS有关的事:如何判断一个软件是从MAS安装的还是从网上下载后安装的?

方法一:
进入Mac App Store后搜索已软件名称,如果是从MAS安装的,那软件的下方显示的是“打开”,如果不是从MAS安装的,那显示的是“获取”;

方法二:
进入Launchpad, 按住某个APP(触控板三指按住),等所有APP开始抖动后(像IPHONE上删除软件一样),如果APP图标左上角显示[X]号,那就是从MAS安装的。

Django(Python) 中按屏幕显示宽度截取字符串

Django 的模板中默认提供了两个常用的 filter: truncatecharstruncatechars_html, 用于在字符串中截取指定数量的字符(详见 Django 文档说明). 如果是纯英文(或其它半角字符)组成的字符串, 使用这两个 filter 不会感觉有什么不对. 但对于中文网站来讲, 就有很大的局限性.

中文是全角字符, 而西方文字多是半角字符, 在大多数字体中, 全角和半角字符的宽度相差很大, 全角字符在屏幕上显示的宽度大概是半角字符的两倍(注意: 只是大概而以), 如果使用 truncatechars 来截取混合有全角和半角字符的字符串时, 那截取结果在屏幕上的显示宽度就很难控制. 而很多情况下, 我们希望的结果就是要截取一定宽度的字符串, 而不是一定的字符个数. 例如本站首页的讨论版新贴最近回复两块内容中(因网站改版现在的首页已经有所不同), 我就希望显示指定宽度的内容而不是显示指定的字符个数.

我GOOGLE了一些资料,暂时找到以下两种比较可行的解决办法:

方案一: urwid的解决方案

我参考了这篇文章, http://likang.me/blog/2012/04/13/calculate-character-width-in-python/, 里面讲的很详细.

最核心的原理在这句话:

首先根据 unicode 的官方 EastAsianWidth 文档整理出字符宽度的范围表,然后使用unicode代码查表。
我根据他提出的方案, 照着写了一个函数并包装成 Django 的 custom template filter(这也是本站现在使用的方案), 核心函数如下:
WIDTHS = [
    (126,    1), (159,    0), (687,     1), (710,   0), (711,   1),
    (727,    0), (733,    1), (879,     0), (1154,  1), (1161,  0),
    (4347,   1), (4447,   2), (7467,    1), (7521,  0), (8369,  1),
    (8426,   0), (9000,   1), (9002,    2), (11021, 1), (12350, 2),
    (12351,  1), (12438,  2), (12442,   0), (19893, 2), (19967, 1),
    (55203,  2), (63743,  1), (64106,   2), (65039, 1), (65059, 0),
    (65131,  2), (65279,  1), (65376,   2), (65500, 1), (65510, 2),
    (120831, 1), (262141, 2), (1114109, 1),
]

def get_screen_width(input_str, max_width=None, tail='.', tail_length=3):
    """
    获取输入字符串input_str在屏幕上的显示宽度,全角字符宽度计算为2,半角字符宽度计算为1
    注意这个宽度并不能保证字符串能在屏幕上完美对齐,因为字体的原因,全角字符的宽度并不一定是半角字符的2倍
    如果仅需要获取字符串的宽度,只需提供input_str参数即可
    如果需要截取字符串,需提供最大截取宽度(max_width)和省略替代符号(tail, 可选)及其最大个数(tail_length, 可选)
    例如,最大截取宽度(max_width)为3,输入的字符串为 u"测试字符串"(长度为10)
    那截取结果是:
    u"..."
    如果截取宽度为4,那结果是:
    u"测.."(会自动少用一个表示省略的字符)
    如果截取宽度为5,那结果是:
    u"测..."
    """

    def get_char_width(char):
        """
        查表(WIDTHS)获取单个字符的宽度
        """
        char = ord(char)
        if char == 0xe or char == 0xf:
            return 0

        for num, wid in WIDTHS:
            if char <= num:
                return wid

        return 1

    if max_width and max_width > tail_length*get_char_width(tail):
        # 最大宽度应该至少和表示省略的字符串一样长
        # str_max_width和max_width的区别在于:
        # max_width表示的是返回结果的最大宽度,包括了最后表示省略的点
        # str_max_width表示的是除去表示省略的符号后在输入字符串中截取部分的最大长度
        str_max_width = max_width - tail_length*get_char_width(tail)
    elif max_width and max_width == tail_length*get_char_width(tail):
        # 如果最大宽度刚好和表示省略的字符串宽度一样,那就直接返回表示省略的字符串
        return tail*tail_length
    elif max_width:
        # 如果出现提供了最大宽度但最大宽度还不如结尾表示省略的字符宽度大的时候就抛出异常
        raise AttributeError

    total_width = 0
    result = input_str

    for i in range(0, len(input_str)):
        total_width += get_char_width(input_str[i])

        if not max_width:
            continue

        # 当接近str_max_width时有几种情况:
        # 一种最离str_max_width还有一个半角字符,这种情况就继续循环
        # 另一种是截完当前字符总长度刚好为str_max_width,这种情况就停止分析下面的字符,
        # 直接在当前字符后面加上表示省略的符号后返回,这时总的长度刚好为max_width
        # 最后一种情况是截取完上一个字符后总宽度刚好和str_max_width差一个半角字符,
        # 刚好当前读取的字符的宽度是2(全角字符),那从输入字符串中截取的长度不可能和
        # str_max_width完全相同,会比str_max_width大一个半角宽度,这种情况就把表示
        # 省略的字符少显示一个,加到结尾,这样最后返回值的长度刚好也是max_width.
        if total_width < str_max_width:
            continue
        elif total_width == str_max_width:
            result = input_str[0:i+1] + tail*tail_length
            break
        else:
            result = input_str[0:i+1] + tail*(tail_length-1)
            break

    return result if max_width else total_width

具体如何在template中使用这里不讨论, 详见官方文档相关章节.

方案二: 使用Kitchen

Kitchen 是一个python的模板, 可以通过 PIP 安装. 它提供了不少功能, 其中就包括了我们需要的计算字符串宽度的函数.

具体的文档在这里: https://pythonhosted.org/kitchen/api-text-display.html

它的使用也非常简单, 也可以很方便地包装为Django的custom template filter:

>>> from kitchen.text.display import textual_width
>>> test_str = u"abc字符串123.-/"
>>> textual_width(test_str)
15
我测试了一下, 两种方案的结果基本相同, 我目前采用的是第一种方法.

值得注意的是, 不管采用哪种方法, 都不能确保精确地控制字符串的宽度, 因为字符串的宽度还和字体有关, 半角字符的宽度并不总是全角字符的一半。以上方案只能说比起 Django 自带的 truncatechars 截取效果会好很多。

Django实现文件上传功能的笔记

给网站增加了上传文件的功能,以便发布内容的时候可以插入图片和附件。翻了一下Django的文档,把Django对文件上传的处理流程做个笔记总结一下。(这里的文件指的是用户上传的文件,和CSS, JS这些静态文件有所不同)

向网站上传文件的流程其实是很简单的,无非就是用户打开一个网页,里面有一个表单,表单里有一个(或多个)文件上传控件,通过上传控件选择了要上传的文件后提交,然后浏览器会向服务器发送POST请求,把要上传的文件和表单的其它内容一起发送给服务器,然后在服务器端进行处理。

用Django做的网站也是这套流程,关键在于服务器对接收到的文件的处理这块上,不同的网站程序有不同的处理方法。Django也有自己的处理流程,我把它总结成两大块:一是预处理(Upload Handlers),二是在View中处理

Upload process of Django

这个网站的文件上传功能设计是这样:文件上传这块是独立的,不和文章有固定的联系。如果发布文章时要上传文件或者图片,那就到文件上传的页面上传文件,然后会得到上传文件的URL,一次上传可以多次使用,只要到已上传文件的列表中复制一个URL即可。

实现步骤如下:

1.定义Model

Django中的信息都是用Model来表示的,文件也不例外,这里把每个附件也用一个model来表示:
# -*- coding: utf-8 -*-
# forum/models.py
# ...

class Attachment(models.Model):
# 上传文件的用户
user = models.ForeignKey('User')
# 被上传的文件
file = models.FileField(upload_to='uploads')
#...


在Model中,文件是用FileFieldImageField来表示的,ImageField是对FileField的扩展,在FileField的基础上增加了一些属性(图像的长度和宽度等)。

2.设定在View中的处理流程

上传的文件是通过表单提交一起提交的,事实上,对它们的处理也和其它表单内容的处理基本一样,可以通过Form, ModelForm等处理后保存为Model的实例。

区别在于:表单中的一般内容是通过request.POST来访问的,而上传的文件要通过request.FILES来访问,这个request.FILES是一个Python的字典对象。 一个表单一次可能上传多个文件,每个文件都是request.FILES这个字典中的元素,我们可以通过request.FILES['key']操作它们, 这里的键名key是指表单中上传控件的名称(name属性值)。1

文档中的示例:

# -*- coding: utf-8 -*-
# forum/views.py

from django.http import HttpResponseRedirect
from django.shortcuts import render
from .forms import ModelFormWithFileField

# ...

def upload_file(request):
if request.method == 'POST':
form = ModelFormWithFileField(request.POST, request.FILES)
if form.is_valid():
# file is saved
form.save()
return HttpResponseRedirect('/success/url/')
else:
form = ModelFormWithFileField()
return render(request, 'upload.html', {'form': form})

# ...

3.上传文件的存储

文件在View中的处理逻辑也写好了,那上传的文件到底存在哪里呢?——当然是想存在哪里就存在哪里。

可以通过settings.py中的MEDIA_ROOT属性指定文件在服务器上的存储路径绝对路径。例如:

MEDIA_ROOT = "/var/www/example.com/upload/"
或者不要写得这么绝对,比如想把上传的文件放在Django项目目录下的upload/文件夹中,可以写成:
# project/settings.py

# ...

BASE_DIR = os.path.dirname(os.path.dirname(file))

# ...

MEDIA_ROOT = os.path.join(BASE_DIR, 'upload')

如果想把文件动态地放在upload文件夹下不同的子目录中也很容易做到,请参阅官方文档

上面讲的这些都是文件最终的储存位置,那在文件上传到服务器后,在View函数中被处理完成之前它们又是放在哪里呢?之前讲到上传的文件在服务器端的处理有两步,一个是预处理,二是View中的处理。在settings.py中有一个FILE_UPLOAD_HANDLERS属性指定了预处理的流程,它有两个默认值,当然也可以自己写处理流程。FILE_UPLOAD_HANDLERS的默认处理流程会把较小的文件暂时放在内存中,较大的文件放在某个临时文件夹中等待进一步处理,设置中的FILE_UPLOAD_TEMP_DIR指定了临时文件夹的位置,一般不用特别设置,会使用系统默认的/tmp/。2

4.访问已上传的文件

上传完成后就是对文件进行操作和使用了。对文件的操作可以分成两类情况:一种是在Python程序内部使用,另外一种是在WEB页面中访问

先说如何在程序内部操作文件。

Internally, Django uses a django.core.files.File instance any time it needs to represent a file. This object is a thin wrapper around Python’s built-in file object with some Django-specific additions.3

When you use a FileField or ImageField, Django provides a set of APIs you can use to deal with that file.4


一个文件在Django中始终是以django.core.files.File或者它的子类的一个实例的形态的存在的,而File又是对Python中的文件对象(File Objects)的一个简单封装。换句话说,你可以像操作Python原生文件对象一样操作Django中的文件。例如,在request.FILES中,文件的存在形式是UploadedFile的实例(UploadFile是File的一个子类)5,当通过Model的FileField访问文件时,你会得到一个FieldFile的实例(显然,FieldFile也是File的子类)6

再说一下在WEB页面或者是模板中对文件的访问。

在WEB页面中访问文件或者图片就更简单了,只要一个URL就可以了,前面讲到当通过Model的FileField访问文件时,你会得到一个FieldFile的实例,这个FieldFile扩展了默认的File类,提供了一些新的属性和方法,比如FieldFile.url——用来得到文件的URL。

这里问题又来了,通过FieldFile.url这样的属性得到的URL是怎样的呢?是lcfcn.com/aaa/file.exe还是lcf.cn/bbb/file.exe还是aaa.lcfcn.com/file.exe呢?settings.py中的MEDIA_URL就是解决这个问题的,MEDIA_URL指定了MEDIA_ROOT的URL。例如当MEDIA_URL = "/upload/"(MEDIA_URL值需要以/结尾),那生成的文件URL会类似lcfcn.com/upload/filename.ext

到这一步还没有结束,MEIDA_URL只是确定了附件的URL而以,如果想让URL生效,还需要在urls.py中或者WEB服务器上进行对应的设置。到这一步又分成两种情况:一是开发过程中,另一种是生产环境中。如果在是开发过程中,只需要在urls.py中增加对应的设置就好,Django提供了方便的函数,具体照抄文档中的这个例子即可,基本没有需要改的地方:

from django.conf import settings
from django.conf.urls.static import static

urlpatterns = patterns('',
# ... the rest of your URLconf goes here ...
) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

# 文档中说明了static()只有在DEBUG模式中才能生效

如果是在生产环境中,一般来说不会把静态文件交给Django来处理,WEB服务器处理这种情况更拿手。根据不同的WEB服务器,设置也略有不同,不过思路还大同小异的,例如以下的情况:

MEDIA_ROOT = "/home/user/www/site/upload/"

MEDIA_URL = "/upload/"

那在WEB服务器中只要把/upload/指向/home/user/www/site/upload/就好了。


最后再总结一下在Django中实现简单的文件上传需要的一些流程(不分先后顺序):

  • settings.py中设置好MEDIA_ROOTMEDIA_URL
  • urls.py中的设置(开发过程中)
  • WEB服务器(nginx, apache)中的设置(服务器上部署)
  • 在Model中使用FileField或者ImageField
  • 在View中像处理普通表单一样处理上传的文件(记得使用request.FILES)
  • 给表单设定enctype="multipart/form-data属性7