我想发送附带文件的POST请求,但有些字段名称中包含Unicode字符。但是服务器没有正确接收它们,如下所示:
>>> # normal, without unicode
>>> resp = requests.post('http://httpbin.org/post', data={'snowman': 'hello'}, files={('kitten.jpg', open('kitten.jpg', 'rb'))}).json()['form']
>>> resp
{u'snowman': u'hello'}
>>>
>>> # with unicode, see that the name has become 'null'
>>> resp = requests.post('http://httpbin.org/post', data={'☃': 'hello'}, files={('kitten.jpg', open('kitten.jpg', 'rb'))}).json()['form']
>>> resp
{u'null': u'hello'}
>>>
>>> # it works without the image
>>> resp = requests.post('http://httpbin.org/post', data={'☃': 'hello'}).json()['form']
>>> resp
{u'\u2603': u'hello'}
我如何解决这个问题?
答案 0 :(得分:22)
来自wireshark评论,看起来python-requests做错了,但可能没有“正确答案”。
最初使用非ASCII字符集的字段名称可以使用RFC 2047中描述的标准方法在“name”参数的值内编码。
RFC 2047反过来说
通常,“编码字”是一系列可打印的ASCII字符,以“=?”开头,以“?=”结尾,中间有两个“?”。它指定了一个字符集和一种编码方法,并且还包括根据该编码方法的规则编码为图形ASCII字符的原始文本。
继续描述“Q”和“B”编码方法。使用“Q”(quoted-printable)方法,名称为:
=?utf-8?q?=E2=98=83?=
但是,正如RFC 6266明确指出:
“编码字”绝不能用于MIME内容类型或内容处置字段的参数,也不能用于任何结构化字段正文,除非在“评论”或“短语”中。
所以我们不允许这样做。 (感谢@Lukasa这次捕获!)
RFC 2388也说也可以提供原始本地文件名 “filename”参数中的任何一个“content-disposition:form-data” 标题,或者在多个文件的情况下,在“内容处理: 文件“子部分的标题。发送应用程序可以提供一个 文件名;如果发件人的操作系统的文件名不是 在US-ASCII中,文件名可能是近似的,或使用编码 RFC 2231的方法。
RFC 2231描述了一种看起来更像你所看到的方法。在其中,
星号(“*”)被重复用于提供语言和语言的指示符 存在字符集信息并且正在使用编码。一个 单引号(“'”)用于分隔字符集和语言 参数值开头的信息。百分号 (“%”)用作编码标志,与RFC 2047一致。
具体来说,参数名称末尾的星号用作 字符集和语言信息可能出现的指示符 参数值的开头。单引号用于 将字符集,语言和实际值信息分开 参数值字符串,百分号用于标记 以十六进制编码的八位字节。
也就是说,如果使用此方法(并且两端都支持),则名称应为:
name*=utf-8''%E2%98%83
幸运的是,RFC 5987将基于RFC 2231 的编码添加到HTTP标头中! (感谢@bobince这个发现)它说你可以(任何可能应该)包含RFC 2231风格的值和一个简单的值:
然而,在他们的例子中,他们“愚弄”了“遗留客户”的普通价值。对于表单字段名称,这不是一个真正的选项,因此似乎最好的方法可能包括标题字段规范需要定义是否有多个实例 允许使用具有相同parmname组件的参数,以及如何使用 他们应该被处理。该规范建议a 使用扩展语法的参数优先。这个会 允许生产者使用这两种格式而不破坏收件人 还不了解扩展语法。
示例:
foo:bar; title =“欧元汇率”; 标题* = UTF-8 '' %E2%82%AC%20exchange%20rates
name=
和name*=
版本,其中普通值是(正如@bobince描述的那样)“只是以与表单相同的编码发送引用的字节”,如:
Content-Disposition: form-data; name="☃"; name*=utf-8''%E2%98%83
另见:
最后,请参阅http://larry.masinter.net/1307multipart-form-data.pdf(还有https://www.w3.org/Bugs/Public/show_bug.cgi?id=16909#c8),其中建议您坚持使用ASCII表单字段名称来避免此问题。
答案 1 :(得分:4)
字段值显示为form-data; Wireshark中的name * = utf-8''%5Cu2603
这里有两件事。
这不适合我,我得name*=utf-8''%E2%98%83
。 %5Cu2603
是我在非Unicode字符串中意外键入\u
转义符所期望的,即如上所述写'\u2603'
而不是'☃'
。
正如所讨论的那样,这是扩展Unicode标头的RFC 2231形式:
RFC 2231格式以前在HTTP中无效(HTTP不是RFC 822系列中的邮件标准)。它现在已经被RFC 5987带到了HTTP,但因为这是最近几乎没有任何服务器端支持它。
绝对urllib3
不应该依赖它;它应该做什么浏览器做,只是发送引用的字节,与表单相同的编码。如果它必须使用2231表格,它应该是组合的,如section 4.2。
例如在urllib3.fields.format_header_param
中,而不是:
value = email.utils.encode_rfc2231(value, 'utf-8')
你可以说:
value = '%s="%s"; %s*=%s' % (
name, value, name,
email.utils.encode_rfc2231(value, 'utf-8')
)
但是,包括2231表单可能仍然会混淆一些旧服务器。
答案 2 :(得分:1)
我想我应该责怪urllib3因此Requests产生它的格式。在I wrote that code时,我主要考虑了文件名,RFC 2388 section 4.5建议在那里使用RFC 2231格式。
关于字段名称,RFC 2388 section 3引用RFC 2047,forbids依次and others使用Content-Disposition
字段中的编码字。所以在我看来RFC 2338 should take precedence这两个标准相互矛盾。但也许made aware,所以使用RFC 2047编码的单词可能会更正确。
最近我the current draft for the HTML 5 standard已经a section on the encoding of multipart/form-data an issue for urllib3 {{3}}。它与其他几个标准相矛盾,但它可能是未来。关于字段名称(不是文件名),它描述了一种编码,该编码将字符转换为十进制XML实体,例如, ☃
雪人☃
。但是,只有在为提交而建立的编码不包含相关字符时才应用该编码,而在您的设置中不应该这样。
我已提交{{3}}来讨论此问题的后果,并可能在实施过程中解决这些问题。
答案 3 :(得分:0)
Rob Starling的回答非常有见地,并且证明在字段名称中使用非ASCII字符是兼容性方面的错误(所有那些RFC!),但我设法让python请求坚持使用最多(来自我所能看到的)处理事物的方法。
在site-packages/requests/packages/urllib3/fields.py
内,删除此(第50行):
value = email.utils.encode_rfc2231(value, 'utf-8')
然后将它下面的线改为:
value = '%s="%s"' % (name, value.decode('utf-8'))
这使得服务器(我已经测试过)接收字段并正确处理它。