Google App Engine 上的 Flask Webapp 仅在从 PC 访问时出错:'ascii'编解码器无法解码字节



我有一个托管在Google App Engine上的Flask网络应用程序,它要求用户上传文件。几年来它一直运行良好。网络应用程序支持广告,所以我不会链接到托管版本,但源代码在这里:https://github.com/n8henrie/icw

一位用户最近通知我,他收到一个文件500错误:'ascii' codec can't decode byte 0xc2 in position 108: ordinal not in range(128)

他通过电子邮件将文件发送给我,我无法在本地或OS X上的托管Web应用程序上复制错误。

后来,他通过电子邮件向我发送了几个导致错误的文件,所以我再次尝试,但这次是从 PC 上。在PC上,我确实收到了错误。出于好奇,我回到Mac,从Gmail下载相同的文件,并尝试 - 没有收到错误。

为什么会这样?我真的很想在我的Mac上重现此错误,以便我可以在家进行调试,但我只能从工作的PC上获取它 - 我没有代码并且无法调试。

为什么

  • 认为它可能与从Gmail下载后但在上传到Web应用程序之前的本地文件编码有关,因此在我的Mac上,我在TextWrangler中打开并尝试将编码更改为ascii。仍然没有错误
  • 在记事本中打开PC上的文件并将编码更改为UTF8。仍然会导致错误。
  • 添加了from __future__ import unicode_literals到网络应用程序。仍然通过OSX上的所有测试,仍然在PC上导致类似的错误('ascii' codec can't decode byte 0xef in position 0: ordinal not in range(128)(。

为什么相同的网络应用程序和相同的上传文件在PC上会出现错误,而在我的Mac上却没有?GAE 是否会根据检测客户端的操作系统以某种方式更改 Web 应用程序版本?

非常感谢任何帮助。

  • Chrome 46.0.2490.80 on Windows 7 v6.1
  • Chrome 46.0.2490.80 on OS X 10.11.1
  • 托管在 GAE 上的 Python 2.7
  • 烧瓶==0.10.1

更新20151111

能够在 GAE 上找到堆栈跟踪:

Exception on / [POST]
Traceback (most recent call last):
  File "/base/data/home/apps/s~icw-flask/2.386023698597365904/lib/flask/app.py", line 1817, in wsgi_app
    response = self.full_dispatch_request()
  File "/base/data/home/apps/s~icw-flask/2.386023698597365904/lib/flask/app.py", line 1477, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/base/data/home/apps/s~icw-flask/2.386023698597365904/lib/flask/app.py", line 1381, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/base/data/home/apps/s~icw-flask/2.386023698597365904/lib/flask/app.py", line 1475, in full_dispatch_request
    rv = self.dispatch_request()
  File "/base/data/home/apps/s~icw-flask/2.386023698597365904/lib/flask/app.py", line 1461, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/base/data/home/apps/s~icw-flask/2.386023698597365904/icw/views.py", line 38, in index
    links_title=links_title)
  File "/base/data/home/apps/s~icw-flask/2.386023698597365904/lib/flask/templating.py", line 128, in render_template
    context, ctx.app)
  File "/base/data/home/apps/s~icw-flask/2.386023698597365904/lib/flask/templating.py", line 110, in _render
    rv = template.render(context)
  File "/base/data/home/runtimes/python27/python27_lib/versions/third_party/jinja2-2.6/jinja2/environment.py", line 894, in render
    return self.environment.handle_exception(exc_info, True)
  File "/base/data/home/apps/s~icw-flask/2.386023698597365904/icw/templates/index.html", line 1, in top-level template code
    {% extends "base.html" %}
  File "/base/data/home/apps/s~icw-flask/2.386023698597365904/icw/templates/base.html", line 3, in top-level template code
    {% extends "bootstrap/base.html" %}
  File "/base/data/home/apps/s~icw-flask/2.386023698597365904/lib/flask_bootstrap/templates/bootstrap/base.html", line 1, in top-level template code
    {% block doc -%}
  File "/base/data/home/apps/s~icw-flask/2.386023698597365904/lib/flask_bootstrap/templates/bootstrap/base.html", line 4, in block "doc"
    {%- block html %}
  File "/base/data/home/apps/s~icw-flask/2.386023698597365904/lib/flask_bootstrap/templates/bootstrap/base.html", line 20, in block "html"
    {% block body -%}
  File "/base/data/home/apps/s~icw-flask/2.386023698597365904/icw/templates/base.html", line 40, in block "body"
    {{ utils.flashed_messages(messages=messages, container=False) }}
  File "/base/data/home/apps/s~icw-flask/2.386023698597365904/lib/flask_bootstrap/templates/bootstrap/utils.html", line 12, in template
    {% for cat, msg in messages %}      <div class="alert alert-{{cat}}" role="alert">{{msg|safe}}</div>{% endfor -%}
  File "/base/data/home/runtimes/python27/python27_lib/versions/third_party/jinja2-2.6/jinja2/filters.py", line 705, in do_mark_safe
    return Markup(value)
  File "/base/data/home/runtimes/python27/python27_lib/versions/third_party/markupsafe-0.15/markupsafe/__init__.py", line 71, in __new__
    return unicode.__new__(cls, base)
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc2 in position 108: ordinal not in range(128)

我认为我的项目中最相关的代码是读取文件,问题可能出在werkzeug对Unicode的处理上。

icw/converter.py:139

def convert(upfile):
    reader_builder = csv.reader(upfile.read().splitlines(),
                                skipinitialspace=True)
    reader_list = list(reader_builder)

解决方案最终不是特别简单。我仍然不完全确定为什么在我的 Mac 上工作正常,但在 PC 上却不能。

然而,我庆幸地发现Microsoft提供了预制的、跨平台的、对VirtualBox友好的Internet Explorer图像,这使我可以轻松地在Macbook上进行测试。它们需要下载几GB,但之后我能够确认我在PC映像上收到IE错误,但在OS X上的FireFox,Safari或Chrome中没有。

它看起来像一个 unicode/ascii 问题,所以我认为尝试将所有内容转换为 unicode 将是解决方案。最终,我的代码中有几个特定的部分需要注意。

  • 首先是读取包含 unicode 字符的文件,意识到我需要upfile.read().decode('utf8')unicode(upfile.read(), 'utf8')使用 unicode 而不是 str 。(显然unicode()更快。
  • 接下来要记住 python2 csv 模块存在 unicode 问题,需要一种解决方法才能对 unicode 友好。
  • 接下来是记住我现在需要将所有字符串转换为 unicode 以处理读入数据,例如将print("foo: {}".format(bar))更新为 print(u"foo: {}".format(unicode_bar))
  • 还有一些地方我用类似map(str, myset)的东西打印套装,我把它改成了map(unicode, myset)
  • 最后一件事是找出带有字节顺序标记的错误

更详细地说,我首先阅读了 csv 模块的 unicode 问题,并使用 python2 文档中的unicode_reader示例将我的csv.reader转换为 unicode 友好版本。

接下来,我继续将# -*- coding: utf-8添加到文件的顶部,并在该from __future__ import unicode_literals下方添加,以使我不必手动将每个'example string'更改为u'example string注意:读者在实施之前应该了解使用unicode_literals难以解开错误的风险;您最好手动更改所有字符串。

然而,即使在这之后,我仍然收到 unicode 错误——尽管略有不同,但始终在文件的开头,尤其是u'ufeff' in position 0

关于这个问题有一些SO线程,但基本上该字符是一个"字节顺序标记"(BOM(,由PC经常添加(特别是如果使用记事本编辑(在文件的开头,以指示它是utf-8编码的。我认为这就是为什么我只在PC上遇到问题的原因。为了解决这个问题,我将unicode_reader更改为使用utf-8-sig编码。

我的最终代码如下所示:

def unicode_csv_reader(utf8_file, **kwargs):
    # splitlines lets us respect universal newlines
    utf8_data = utf8_file.read().splitlines()
    csv_reader = csv.reader(utf8_data, **kwargs)
    for row in csv_reader:
        yield [unicode(cell, 'utf-8-sig') for cell in row]
...
def convert(upfile):
    reader_builder = unicode_csv_reader(upfile, skipinitialspace=True)
    reader_list = list(reader_builder)

我可能会尝试只去除 BOM 而不是使用 utf-8-sig ,但至少我有一个工作版本,它似乎通过了所有测试并在 OS X 和 PC/IE 虚拟机中按预期工作。

希望这对其他人有所帮助!

更新20151115:

  • 似乎BOM确实是问题所在,当我在PC上的记事本中对文件进行简短编辑时,它可能入了。我发现我可以从上面使用虚拟机,从Gmail下载文件,在记事本中打开(包含在虚拟机上(并保存,然后使用Dropbox或其他东西传输回OS X,这样我就可以在OS X上复制错误。所以它与操作系统无关,可能只是 BOM。

最新更新