在Python 3 CGI脚本中设置编码

在Python 3 CGI脚本中设置编码,python,unicode,python-3.x,cgi,Python,Unicode,Python 3.x,Cgi,在编写Python 3.1 CGI脚本时,我遇到了可怕的UnicodeDecodeErrors。但是,在命令行上运行脚本时,一切正常 似乎open()和print()使用了locale.getpreferredencoding()的返回值来了解默认情况下要使用的编码。在命令行上运行时,该值应为“UTF-8”。但当通过浏览器运行脚本时,编码会神秘地被重新定义为“ANSI_X3.4-1968”,这似乎只是普通ASCII的一个花哨名称 我现在需要知道如何使cgi脚本在所有情况下都以“utf-8”作为默

在编写Python 3.1 CGI脚本时,我遇到了可怕的UnicodeDecodeErrors。但是,在命令行上运行脚本时,一切正常

似乎
open()
print()
使用了
locale.getpreferredencoding()
的返回值来了解默认情况下要使用的编码。在命令行上运行时,该值应为“UTF-8”。但当通过浏览器运行脚本时,编码会神秘地被重新定义为“ANSI_X3.4-1968”,这似乎只是普通ASCII的一个花哨名称


我现在需要知道如何使cgi脚本在所有情况下都以“utf-8”作为默认编码运行。我的设置是在DebianLinux上的Python 3.1.3和Apache2。系统范围的语言环境是en_GB.utf-8。

最好的办法是使用您想要使用的编码将Unicode字符串显式编码为字节。依赖隐式转换将导致类似这样的问题


顺便说一句:如果错误真的是Unicode DecodeError,那么它不会发生在输出上,而是试图将字节流解码为Unicode,这可能发生在其他地方。

我用以下代码解决了我的问题:

import locale                                  # Ensures that subsequent open()s 
locale.getpreferredencoding = lambda: 'UTF-8'  # are UTF-8 encoded.

import sys                                     
sys.stdin = open('/dev/stdin', 'r')       # Re-open standard files in UTF-8 
sys.stdout = open('/dev/stdout', 'w')     # mode.
sys.stderr = open('/dev/stderr', 'w') 
这个解决方案并不漂亮,但似乎暂时可行。实际上,我选择了Python 3而不是更普通的v。2.6作为我的开发平台,由于宣传良好的Unicode处理,但是
cgi
包似乎破坏了一些简单性


我相信
/dev/std*
文件可能不存在于没有procfs的旧系统上。但是,它们在最近的Linux上是受支持的。

您不应该将IO流作为CGI/WSGI的字符串来读取;它们不是Unicode字符串,而是显式的字节序列

(假设
内容长度
是以字节而非字符为单位测量的;想象一下,尝试读取
多部分/表单数据
二进制文件上传提交压缩成UTF-8解码字符串,或返回二进制文件下载…)

因此,使用
sys.stdin.buffer
sys.stdout.buffer
获取stdio的原始字节流,并用它们读/写二进制文件。在适当的情况下,由表单读取层使用网页上的编码将这些字节转换为Unicode字符串参数

不幸的是,标准库CGI和WSGI接口在Python3.1中没有做到这一点:相关模块是使用
2to3
从Python2原始版本粗略转换而来的,因此有许多bug最终会出现在UnicodeError中


可用于web应用程序的Python 3的第一个版本是3.2。使用3.0/3.1简直是浪费时间。令人遗憾的是,花了很长时间才解决了这个问题,并通过了PEP33333。

为后来者回答这个问题,因为我不认为发布的答案能够找到问题的根源,即CGI上下文中缺少区域设置环境变量。我正在使用Python 3.2

  • open()以文本(字符串)或二进制(字节)模式打开文件对象进行读取和/或写入;在文本模式下,可在调用中指定用于编码写入文件的字符串和解码从文件读取的字节的编码;如果不是,则由locale.getPreferredEncode()确定,它在linux上使用您的locale环境设置中的编码,通常是utf-8(例如LANG=en_US.utf-8)

  • sys.stdout实际上是一个以文本模式打开的文件,其编码基于locale.getPreferredEncode();您可以很好地向它写入字符串,它们将根据sys.stdout的编码编码为字节;print()默认情况下写入sys.stdout-print()本身没有编码,而是它写入的文件有编码

    >>> sys.stdout.encoding
    'UTF-8'                          # encoding is from the environment
    >>> exit()
    user@host:~$ python3 -c 'print("€")' > foo
    user@host:~$ hd foo
    00000000  e2 82 ac 0a   |....|   # data is UTF-8 encoded; \n is from print()
    
    );不能将字节写入sys.stdout-为此使用sys.stdout.buffer.write();如果您尝试使用sys.stdout.write()将字节写入sys.stdout,那么它将返回一个错误;如果您尝试使用print(),print()将简单地将字节对象转换为字符串对象,像
    \xff
    这样的转义序列将被视为四个字符\,x,f,f

    user@host:~$ python3 -c 'print(b"\xe2\xf82\xac")' > foo
    user@host:~$ hd foo
    00000000  62 27 5c 78 65 32 5c 78  66 38 32 5c 78 61 63 27  |b'\xe2\xf82\xac'|
    00000010  0a                                                |.|
    
  • 在CGI脚本中,您需要写入sys.stdout,您可以使用print()来完成;但是Apache中的CGI脚本进程没有语言环境设置——它们不是CGI规范的一部分;因此,sys.stdout编码默认为ANSI_X3.4-1968-换句话说,ASCII;如果尝试将()包含非ASCII字符的字符串打印到sys.stdout,将得到“UnicodeEncodeError:'ASCII'编解码器无法对字符进行编码…”:序号不在范围内(128)”

  • 一个简单的解决方案是在服务器或虚拟主机配置中使用Apache的mod_env PassEnv命令将Apache进程的LANG环境变量传递给CGI脚本:PassEnv LANG;在Debian/Ubuntu上,确保在/etc/apache2/envvars中取消了“/etc/default/locale”行的注释,以便Apache使用系统默认语言环境而不是C(Posix)语言环境运行(这也是ASCII编码);在Python 3.2中运行以下CGI脚本时应无错误:

    #!/usr/bin/env python3
    import sys
    print('Content-Type: text/html; charset=utf-8')
    print()
    print('<html><body><pre>' + sys.stdout.encoding + '</pre>h€lló wörld<body></html>')
    
    #/usr/bin/env蟒蛇3
    导入系统
    打印('Content-Type:text/html;charset=utf-8')
    打印()
    打印(''+sys.stdout.encoding+'h€llówörld')
    


  • 总结@cercatrova的答案:

    • PassEnv LANG
      行添加到
      /etc/apache2/apache2.conf
      .htaccess
      的末尾
    • 取消注释
      /etc/default/locale
      中的行
      /etc/apache2/envvars

    • 确保类似于
      LANG=“en_US.UTF-8”
      的行出现在
      /etc/default/locale
    • sudo服务apache2重启

      • 我遇到了同样的问题。我的环境是Windows10+Apache 2.4+Python 3.8。
        我正在为Google Earth Pro开发一个覆盖图,它只访问
        #!/usr/bin/env python3
        import sys
        print('Content-Type: text/html; charset=utf-8')
        print()
        print('<html><body><pre>' + sys.stdout.encoding + '</pre>h€lló wörld<body></html>')
        
        sys.stdout = codecs.getwriter('utf8')(sys.stdout.buffer)
        
        SetEnv PYTHONIOENCODING utf8
        
        Options +ExecCGI
        AddHandler cgi-script .py