在Python中创建和解析多部分HTTP请求

9 投票
3 回答
13904 浏览
提问于 2025-04-16 08:29

我正在尝试写一些Python代码,目的是在客户端创建多部分的MIME HTTP请求,然后在服务器上正确解析这些请求。我觉得在客户端这部分我已经部分成功了,代码如下:

from email.mime.multipart import MIMEMultipart, MIMEBase
import httplib
h1 = httplib.HTTPConnection('localhost:8080')
msg = MIMEMultipart()
fp = open('myfile.zip', 'rb')
base = MIMEBase("application", "octet-stream")
base.set_payload(fp.read())
msg.attach(base)
h1.request("POST", "http://localhost:8080/server", msg.as_string())

不过唯一的问题是,邮件库还包含了内容类型(Content-Type)和MIME版本(MIME-Version)这些头信息,我不太确定它们和httplib包含的HTTP头信息之间的关系:

Content-Type: multipart/mixed; boundary="===============2050792481=="
MIME-Version: 1.0

--===============2050792481==
Content-Type: application/octet-stream
MIME-Version: 1.0

这可能就是为什么当我的web.py应用接收到这个请求时,我只看到一个错误信息。web.py的POST处理器:

class MultipartServer:
    def POST(self, collection):
        print web.input()

抛出了这个错误:

Traceback (most recent call last):
  File "/usr/local/lib/python2.6/dist-packages/web.py-0.34-py2.6.egg/web/application.py", line 242, in process
    return self.handle()
  File "/usr/local/lib/python2.6/dist-packages/web.py-0.34-py2.6.egg/web/application.py", line 233, in handle
    return self._delegate(fn, self.fvars, args)
  File "/usr/local/lib/python2.6/dist-packages/web.py-0.34-py2.6.egg/web/application.py", line 415, in _delegate
    return handle_class(cls)
  File "/usr/local/lib/python2.6/dist-packages/web.py-0.34-py2.6.egg/web/application.py", line 390, in handle_class
    return tocall(*args)
  File "/home/richard/Development/server/webservice.py", line 31, in POST
    print web.input()
  File "/usr/local/lib/python2.6/dist-packages/web.py-0.34-py2.6.egg/web/webapi.py", line 279, in input
    return storify(out, *requireds, **defaults)
  File "/usr/local/lib/python2.6/dist-packages/web.py-0.34-py2.6.egg/web/utils.py", line 150, in storify
    value = getvalue(value)
  File "/usr/local/lib/python2.6/dist-packages/web.py-0.34-py2.6.egg/web/utils.py", line 139, in getvalue
    return unicodify(x)
  File "/usr/local/lib/python2.6/dist-packages/web.py-0.34-py2.6.egg/web/utils.py", line 130, in unicodify
    if _unicode and isinstance(s, str): return safeunicode(s)
  File "/usr/local/lib/python2.6/dist-packages/web.py-0.34-py2.6.egg/web/utils.py", line 326, in safeunicode
    return obj.decode(encoding)
  File "/usr/lib/python2.6/encodings/utf_8.py", line 16, in decode
    return codecs.utf_8_decode(input, errors, True)
UnicodeDecodeError: 'utf8' codec can't decode bytes in position 137-138: invalid data

我的代码出错的那一行大概在错误信息的中间部分:

  File "/home/richard/Development/server/webservice.py", line 31, in POST
    print web.input()

事情在逐步好转,但我不太确定接下来该怎么做。这是我的客户端代码的问题,还是web.py的限制(也许它根本不支持多部分请求)?如果有任何提示或替代代码库的建议,我将非常感激。

编辑

上面的错误是因为数据没有自动进行base64编码。添加了

encoders.encode_base64(base)

之后,这个错误消失了,现在问题变得清晰了。HTTP请求在服务器上没有被正确解析,可能是因为邮件库把应该是HTTP头的信息放到了请求体里面:

<Storage {'Content-Type: multipart/mixed': u'', 
          ' boundary': u'"===============1342637378=="\n'
          'MIME-Version: 1.0\n\n--===============1342637378==\n'
          'Content-Type: application/octet-stream\n'
          'MIME-Version: 1.0\n' 
          'Content-Transfer-Encoding: base64\n'
          '\n0fINCs PBk1jAAAAAAAAA.... etc

所以这里面有些不对劲。

谢谢

理查德

3 个回答

-1

你的请求有很多问题。正如TokenMacGuy所说,multipart/mixed在HTTP中是没用的;应该使用multipart/form-data。除此之外,部分内容还需要有一个Content-disposition头。你可以在这个链接找到一个用Python实现的代码片段。

1

我使用了Will Holcomb开发的这个包,链接是http://pypi.python.org/pypi/MultipartPostHandler/0.1.0,它可以帮助你用urllib2发送多部分请求,可能对你有帮助。

1

经过一番探索,这个问题的答案变得清晰明了。简单来说,虽然在Mime编码的消息中,Content-Disposition是可选的,但是web.py要求每个mime部分都必须有这个字段,以便正确解析HTTP请求。

与其他评论不同的是,HTTP和电子邮件之间的区别并不重要,因为它们只是传输Mime消息的方式,没有其他意义。在内容交换的网络服务中,multipart/related(而不是multipart/form-data)消息是很常见的,这就是这里的使用场景。提供的代码片段是准确的,并且让我找到了一个稍微简洁的解决方案。

# open an HTTP connection
h1 = httplib.HTTPConnection('localhost:8080')

# create a mime multipart message of type multipart/related
msg = MIMEMultipart("related")

# create a mime-part containing a zip file, with a Content-Disposition header
# on the section
fp = open('file.zip', 'rb')
base = MIMEBase("application", "zip")
base['Content-Disposition'] = 'file; name="package"; filename="file.zip"'
base.set_payload(fp.read())
encoders.encode_base64(base)
msg.attach(base)

# Here's a rubbish bit: chomp through the header rows, until hitting a newline on
# its own, and read each string on the way as an HTTP header, and reading the rest
# of the message into a new variable
header_mode = True
headers = {}
body = []
for line in msg.as_string().splitlines(True):
    if line == "\n" and header_mode == True:
        header_mode = False
    if header_mode:
        (key, value) = line.split(":", 1)
        headers[key.strip()] = value.strip()
    else:
        body.append(line)
body = "".join(body)

# do the request, with the separated headers and body
h1.request("POST", "http://localhost:8080/server", body, headers)

web.py能够很好地处理这些内容,所以很明显,email.mime.multipart适合用来创建通过HTTP传输的Mime消息,唯一的例外是它的头部处理。

我还有一个总体的担忧,就是可扩展性。这个解决方案和这里提出的其他方案都不太好扩展,因为它们在将文件内容读入变量后再打包到mime消息中。一个更好的解决方案应该是在内容通过HTTP连接传输时,能够按需序列化。虽然我现在不急着解决这个问题,但如果我有时间会回来分享我的解决方案。

撰写回答