django解析POST过来的json时,Unterminated string starting

1.bug产生背景

handsontable插件实现线上Excel, 在其基础上封装,当插件操作完后产生一个较复杂的json对象( 属性是列表或者 字典[map,键值对]的嵌套,结构十分复杂 ),然后使用 JSON.stringify(excel_data)转成 json字符串,连同其他形如“&a=1&b=2”的QueryString字符串一起 POST给django后端。当Excel中某一个单元格,填写有分号(;),等号(=)时就会500错误,打印出异常信息如下:
Unterminated string starting at line 1.....

2.错误定位
查看firebug网络标签页,发现post的数据正确无误,到了django后端view层,却在request.POST里取不出post数据,在request.POST.get("excel_data_key")一句引发异常。毫无疑问,问题出在django后端,firebug已经证明前端post提交无误。
在Django里 request是一个WSGIRequest(site-packages/django/core/handlers/下面),而 request.POST是一个QueryDict。将这个QueryDict 打印出来,发现post数据被分号(;)截断了。举个例子示意(项目中json串太复杂了,举个简单例子):
post过来有一项是这样......{“advertisement_size”: "400X200 px;JPG", .......} .......有一个value字符串("400X200 px;JPG")中带分号,
结果 request.POST打印出来......{“advertisement_size”: "400X200 px“,"JPG":.......} .......
很明显 分号分割了字符串, ”JPG“成了下一个key,这样显然会导致字符串引号不匹配,从而会Unterminated string starting 异常。那么为什么会有这个分割呢?


当我们 使用request.POST时,实际上 会调用WSGIRequest._get_post()
def _get_post(self):
    if not hasattr(self, '_post'):
        self._load_post_and_files()
    return self._post

_get_post()中会调用_load_post_and_files()  ps:定义在HttpRequest父类中
def _load_post_and_files(self):
    # Populates self._post and self._files
    if self.method == 'POST':
        if self.environ.get('CONTENT_TYPE', '').startswith('multipart'):
            header_dict = dict([(k, v) for k, v in self.environ.items() if k.startswith('HTTP_')])
            header_dict['Content-Type'] = self.environ.get('CONTENT_TYPE', '')
            self._post, self._files = http.parse_file_upload(header_dict, self.raw_post_data)
        else:
            self._post, self._files = http.QueryDict(self.raw_post_data, encoding=self._encoding), datastructures.MultiValueDict()
    else:
        self._post, self._files = http.QueryDict('', encoding=self._encoding), datastructures.MultiValueDict()
到这一步看不出任何问题,于是把self.raw_post_data打印了一下,发现这个还是正确的,很明显 问题就出在构造QueryDict这一步。QueryDict的构造函数如下:
def __init__(self, query_string, mutable=False, encoding=None):
    MultiValueDict.__init__(self)
    if not encoding:
        # *Important*: do not import settings any earlier because of note
        # in core.handlers.modpython.
        from django.conf import settings
        encoding = settings.DEFAULT_CHARSET
    self.encoding = encoding
    self._mutable = True
    for key, value in parse_qsl((query_string or ''), True): # keep_blank_values=True
        self.appendlist(force_unicode(key, encoding, errors='replace'),
                        force_unicode(value, encoding, errors='replace'))
    self._mutable = mutable

有一个外部函数的调用 parse_qsl,跟踪进去:
这里有点需要说明, django不同版本使用的parse_qsl不一样,优先使用高版本的新的
django1.5对于parse_qsl函数引入是这样写的
try:
    from urllib.parse import parse_qsl, urlencode, quote, urljoin
except ImportError:
    from urllib import urlencode, quote
    from urlparse import parse_qsl, urljoin
django1.4大同小异,只不过为了兼顾低版本Python,对最后一句再次使用try, 当从from urlparse import parse_qsl失败时,改为从 cgi 模块引入。
cgi模块的parse_qsl函数如下:(可以看到, 其实cgi的parse_qsl也是调用了 urlparse下的parse_qsl)
def parse_qsl(qs, keep_blank_values=0, strict_parsing=0):
    """Parse a query given as a string argument."""
    warn("cgi.parse_qsl is deprecated, use urlparse.parse_qsl instead",
         PendingDeprecationWarning, 2)
    return urlparse.parse_qsl(qs, keep_blank_values, strict_parsing)

而关于 cgi.parse_qsl 在Python2.7文档中有这样一段:(貌似2.6就废弃了)
cgi.parse_qsl(qs[, keep_blank_values[, strict_parsing]]) 
    This function is deprecated in this module. Use urlparse.parse_qsl() instead. It is maintained here only for backward compatiblity.


不管是cgi还是urlparse, 最终跟进去,看到的parse_qsl函数最终的实现是这样的
def parse_qsl(qs, keep_blank_values=0, strict_parsing=0):
    """Parse a query given as a string argument.

    Arguments:

    qs: URL-encoded query string to be parsed

    keep_blank_values: flag indicating whether blank values in
        URL encoded queries should be treated as blank strings.  A
        true value indicates that blanks should be retained as blank
        strings.  The default false value indicates that blank values
        are to be ignored and treated as if they were  not included.

    strict_parsing: flag indicating what to do with parsing errors. If
        false (the default), errors are silently ignored. If true,
        errors raise a ValueError exception.

    Returns a list, as G-d intended.
    """
    pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
    r = []
    for name_value in pairs:
        if not name_value and not strict_parsing:
            continue
        nv = name_value.split('=', 1)
        if len(nv) != 2:
            if strict_parsing:
                raise ValueError, "bad query field: %r" % (name_value,)
            # Handle case of a control-name with no equal sign
            if keep_blank_values:
                nv.append('')
            else:
                continue
        if len(nv[1]) or keep_blank_values:
            name = urllib.unquote(nv[0].replace('+', ' '))
            value = urllib.unquote(nv[1].replace('+', ' '))
            r.append((name, value))
    return r

看到第一行文档字符串就大概明白了, 这玩意儿,把POST过来的数据当作QueryString解析了。
看源码果不其然,
与号( &), 分号( ;), 等号( =)都会成为字符串分割符。POST过来的数据含有分号也正是在这里被分割的。

3.怎么解决呢?
3.1 对于比较简单的post数据,比如例子中举的,改法很简单。不要直接从POST字典里取值,把request.POST.get('excel_data')
改为simplejson.loads( request.raw_post_data)或者simplejson.loads( request.body)

request.raw_post_data   也被django1.4废弃了,但是仍然可用,最好写为 request.body
详见源码(http包下http.HttpRequest,可以看到 row_post_data其实就是指向body)
@property
def body(self):
    if not hasattr(self, '_body'):
        if self._read_started:
            raise Exception("You cannot access body after reading from request's data stream")
        try:
            self._body = self.read()
        except IOError, e:
            raise UnreadablePostError, e, sys.exc_traceback
        self._stream = StringIO(self._body)
    return self._body

@property
def raw_post_data(self):
    warnings.warn('HttpRequest.raw_post_data has been deprecated. Use HttpRequest.body instead.', PendingDeprecationWarning)
    return self.body

3.2很不幸,项目中 post json数据结构过于复杂,有些QuerySyting(= & 惨杂的字符串)也在post数据之中(post既有复杂json又有QueryString格式的其他数据),Python的json , django的simplejson只能处理json字符串,不会拿着&, =字符串分割,不能直接处理QueryString 。这种QueryString混杂json的字符串3.1方法不好使,问题到这里已经很清楚了,就是parse_qsl根据 与号& ,分号;分割了字符串,根本原因在于 QueryString字符串的存在,会使Django用解析QueryString的字符串分割方式处理整个post数据,但是post数据中json字符串部分没有URL编码,把特殊字符转成%xx形式。于是在从Excel插件取数据时,把取出来的数据全部URL编码,于是,不论QueryString还是excel产生的复杂json对象,其任意属性(key)的值(value)不会有分号;  加号+等号=等特殊字符。所以不会被parse_qsl截断。问题解决。

至于javascript url编码就不多说了,百度谷歌一大把,几个函数之间有啥区别,也很详细。不赘述。


4.问题解决了,但是仍然不理解,为什么django把post数据当作QueryString去解析,仅仅因为post数据中存在形如“a=1&b=2” 格式的串?说不清是个什么设计或者是bug?

这个问题其实也可以通过规范代码来避免:要么完全使用“a=1&b=2”   这种QueryString格式的字符串,要么完全使用JSON.stringIfy()产生的json字符串。二者不要混杂字符串拼接,实在要拼接,记得URL编码. 关于post json, 移步 正确post json数据











  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值