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

来源:互联网 发布:长春南关网络花店 编辑:程序博客网 时间:2024/05/18 03:57

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, urljoinexcept 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)
@propertydef 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@propertydef 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 0