【问题标题】:Properly format multipart/form-data body正确格式化 multipart/form-data 正文
【发布时间】:2012-11-10 23:17:24
【问题描述】:

简介

背景

我正在编写一个脚本来上传内容,包括使用RFC 2388 中定义的multipart/form-data 内容类型的文件。从长远来看,我正在尝试提供一个简单的 Python 脚本来执行 uploads of binary packages for github,这涉及将类似表单的数据发送到 Amazon S3。

相关

This question 已经询问过如何做到这一点,但到目前为止还没有一个公认的答案,并且它目前有两个答案中的the more useful 指向these recipes,这反过来又手动构建了整个消息。我有点担心这种方法,尤其是在字符集和二进制内容方面。

还有this question,其currently highest-scoring answer 暗示MultipartPostHandler 模块。但这与我提到的食谱没有太大区别,因此我的担忧也适用于此。

疑虑

二进制内容

RFC 2388 Section 4.3 明确指出,除非另有声明,否则内容应为 7 位,因此可能需要 Content-Transfer-Encoding header。这是否意味着我必须对二进制文件内容进行 Base64 编码?或者Content-Transfer-Encoding: 8bit 对于任意文件是否足够?还是应该读为Content-Transfer-Encoding: binary

标题字段的字符集

一般的标头字段,尤其是filename 标头字段,默认情况下仅为 ASCII。我希望我的方法也能够传递非 ASCII 文件名。我知道对于我当前为 github 上传内容的应用程序,我可能不需要它,因为文件名是在单独的字段中给出的。但我希望我的代码是可重用的,所以我宁愿以一种一致的方式对文件名参数进行编码。 RFC 2388 Section 4.4 建议在 RFC 2231 中引入的格式,例如filename*=utf-8''t%C3%A4st.txt.

我的方法

使用 python 库

由于multipart/form-data 本质上是一种MIME 类型,我认为应该可以使用标准python 库中的email package 来撰写我的帖子。特别是对非 ASCII 标头字段的相当复杂的处理是我想委托的。

工作至今

于是我写了如下代码:

#!/usr/bin/python3.2

import email.charset
import email.generator
import email.header
import email.mime.application
import email.mime.multipart
import email.mime.text
import io
import sys

class FormData(email.mime.multipart.MIMEMultipart):

    def __init__(self):
        email.mime.multipart.MIMEMultipart.__init__(self, 'form-data')

    def setText(self, name, value):
        part = email.mime.text.MIMEText(value, _charset='utf-8')
        part.add_header('Content-Disposition', 'form-data', name=name)
        self.attach(part)
        return part

    def setFile(self, name, value, filename, mimetype=None):
        part = email.mime.application.MIMEApplication(value)
        part.add_header('Content-Disposition', 'form-data',
                        name=name, filename=filename)
        if mimetype is not None:
            part.set_type(mimetype)
        self.attach(part)
        return part

    def http_body(self):
        b = io.BytesIO()
        gen = email.generator.BytesGenerator(b, False, 0)
        gen.flatten(self, False, '\r\n')
        b.write(b'\r\n')
        b = b.getvalue()
        pos = b.find(b'\r\n\r\n')
        assert pos >= 0
        return b[pos + 4:]

fd = FormData()
fd.setText('foo', 'bar')
fd.setText('täst', 'Täst')
fd.setFile('file', b'abcdef'*50, 'Täst.txt')
sys.stdout.buffer.write(fd.http_body())

结果如下:

--===============6469538197104697019==
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: form-data; name="foo"

YmFy

--===============6469538197104697019==
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: form-data; name*=utf-8''t%C3%A4st

VMOkc3Q=

--===============6469538197104697019==
Content-Type: application/octet-stream
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: form-data; name="file"; filename*=utf-8''T%C3%A4st.txt

YWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJj
ZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVm
YWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJj
ZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVm
YWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJjZGVmYWJj
ZGVmYWJjZGVmYWJjZGVm

--===============6469538197104697019==--

它似乎可以很好地处理标题。二进制文件内容将得到 base64 编码,这可能是可以避免的,但应该可以很好地工作。让我担心的是介于两者之间的文本字段。它们也是 base64 编码的。我认为根据标准,这应该足够好,但我宁愿在那里有纯文本,以防一些愚蠢的框架必须处理中间级别的数据并且不知道 Base64 编码的数据。

问题

  • 我可以在我的文本字段中使用 8 位数据并且仍然符合规范吗?
  • 我能否获取电子邮件包以将我的文本字段序列化为 8 位数据而无需额外编码?
  • 如果我必须坚持一些 7 位编码,我是否可以让实现为那些编码短于 base64 的文本部分使用带引号的可打印?
  • 我是否也可以避免对二进制文件内容进行 base64 编码?
  • 如果可以避免,我应该将Content-Transfer-Encoding 写成8bit 还是binary
  • 如果我必须自己序列化正文,我怎么能单独使用 email.header package 来格式化标题值?email.utils.encode_rfc2231 这样做。)
  • 是否有一些实现已经完成了我想做的所有事情?

这些问题密切相关,可以概括为“您将如何实施”。在许多情况下,回答一个问题要么回答要么废弃另一个问题。所以我希望你同意为所有这些人发一篇帖子是合适的。

【问题讨论】:

  • HTTP 中没有 Content-Transfer-Encoding 头域。只是不要发送它。

标签: python http amazon-s3 mime multipartform-data


【解决方案1】:

这是一个占位符答案,描述了我在等待对我的一些问题的权威输入时所做的事情。如果它表明这种方法在至少一个设计决策中是错误的或不适合的,我将很乐意接受不同的答案。

Here 是我现在根据我的喜好用来完成这项工作的代码。 我做了以下决定:

我可以在我的文本字段中使用 8 位数据并且仍然符合规范吗?

我决定这样做。至少对于这个应用程序,它确实有效。

我能否让电子邮件包将我的文本字段序列化为 8 位数据而无需额外编码?

我没找到办法,所以我自己做序列化,就像我看到的所有other recipes一样。

我是否也可以避免对二进制文件内容进行 base64 编码?

至少在我的单个应用程序中,仅以二进制形式发送文件内容似乎就足够了。

如果可以避免,我应该将 Content-Transfer-Encoding 写成 8bit 还是二进制?

正如RFC 2045 Section 2.8 所说,8bit 数据受到 CRLF 对之间 998 个八位字节的行长限制,我认为binary 更通用,因此这里的描述更合适。

如果我必须自己序列化正文,如何单独使用 email.header 包来格式化标头值?

正如我已经在我的问题中编辑的那样,email.utils.encode_rfc2231 对此非常有用。我首先尝试使用 ascii 进行编码,但在非 ascii 数据或双引号字符串中禁止使用的 ascii 字符的情况下使用该方法。

是否有一些实现已经完成了我想做的所有事情?

我不知道。不过,请其他实现采纳来自my code 的想法。


编辑:

感谢this comment,我现在知道对标头使用 RFC 2231 并没有被普遍接受:HTML 5 的当前草案forbids its use。也有人看到cause problems in the wild。但由于 POST 标头并不总是对应于特定的 HTML 文档(例如考虑 Web API),我也不确定在这方面我是否会信任该草案。也许正确的方法是同时给出编码和未编码的名称,RFC 5987 Section 4.2 建议的方式。但是该 RFC 是针对 HTTP 标头的,而 multipart/form-data 标头在技术上是 HTTP 正文。因此,该 RFC 不适用,而且我不知道有任何 RFC 会明确允许(甚至鼓励)同时对 multipart/form-data 使用两种形式。

【讨论】:

    【解决方案2】:

    您可能想查看Send file using POST from a Python script 问题,该问题指向Requests 库,该库正在成为http 中最常用的Python 库。 如果您在那里找不到所有需要的功能并决定自己实现它,我鼓励您将它贡献给这个项目。

    【讨论】:

    • Requests 库听起来很有趣,但是查看the implementation 我发现它目前不能正确处理非ascii 文件名。它也不表示其文本字段的字符集。如果我想出更好的解决方案,我会告诉他们,但我想先整理好自己的东西。
    • 我想提出问题将是一个好的开始,并且可能会邀请某人帮助您。请注意,请求在下面使用 urllib3,因此应该在那里提出问题 - github.com/shazow/urllib3
    • 针对这些问题提交了 urllib3 问题 119120
    猜你喜欢
    • 2017-08-12
    • 2021-11-03
    • 2020-05-13
    • 1970-01-01
    • 2016-02-18
    • 1970-01-01
    • 1970-01-01
    • 2019-12-13
    • 2017-06-09
    相关资源
    最近更新 更多