Python发送二进制数据的POST请求

74

我正在编写一些与Redmine接口的代码,需要在过程中上传一些文件,但是我不确定如何在Python中创建一个包含二进制文件的POST请求。

我试图模仿这里的命令:http://www.redmine.org/projects/redmine/wiki/Rest_api_with_curl

curl --data-binary "@image.png" -H "Content-Type: application/octet-stream" -X POST -u login:password http://redmine/uploads.xml

在Python中(如下所示),但似乎不起作用。我不确定问题是否与文件编码相关,或者头文件有问题。
import urllib2, os

FilePath = "C:\somefolder\somefile.7z"
FileData = open(FilePath, "rb")
length = os.path.getsize(FilePath)

password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(None, 'http://redmine/', 'admin', 'admin')
auth_handler = urllib2.HTTPBasicAuthHandler(password_manager)
opener = urllib2.build_opener(auth_handler)
urllib2.install_opener(opener)
request = urllib2.Request( r'http://redmine/uploads.xml', FileData)
request.add_header('Content-Length', '%d' % length)
request.add_header('Content-Type', 'application/octet-stream')
try:
    response = urllib2.urlopen( request)
    print response.read()
except urllib2.HTTPError as e:
    error_message = e.read()
    print error_message

我可以访问服务器,看起来是一个编码错误:

...
invalid byte sequence in UTF-8
Line: 1
Position: 624
Last 80 unconsumed characters:
7z¼¯'ÅÐз2^Ôøë4g¸R<süðí6kĤª¶!»=}jcdjSPúá-º#»ÄAtD»H7Ê!æ½]j):

(further down)

Started POST "/uploads.xml" for 192.168.0.117 at 2013-01-16 09:57:49 -0800
Processing by AttachmentsController#upload as XML
WARNING: Can't verify CSRF token authenticity
  Current user: anonymous
Filter chain halted as :authorize_global rendered or redirected
Completed 401 Unauthorized in 13ms (ActiveRecord: 3.1ms)
4个回答

101

你的做法基本上是正确的。查看你链接的redmine文档,似乎url中点号后缀表示发布数据类型(.json代表JSON,.xml代表XML),这与你得到的响应相符,即“Processing by AttachmentsController#upload as XML”。我猜也许文档存在错误,如果要发布二进制数据,你应该尝试使用http://redmine/uploads url而不是http://redmine/uploads.xml

顺便说一下,我强烈推荐使用非常好用且非常流行的Requests库来处理Python中的http请求。它比标准库(urllib2)更好用,也支持身份验证,但出于简洁起见,我在这里跳过了它。

import requests
with open('./x.png', 'rb') as f:
    data = f.read()
res = requests.post(url='http://httpbin.org/post',
                    data=data,
                    headers={'Content-Type': 'application/octet-stream'})

# let's check if what we sent is what we intended to send...
import json
import base64

assert base64.b64decode(res.json()['data'][len('data:application/octet-stream;base64,'):]) == data

更新

为了弄清楚这个请求使用Requests能成功,但用urllib2却不行的原因,我们需要检查发送的内容有何不同。我将发送流量到运行在8888端口上的http代理(Fiddler)以查看这一点:

使用Requests

import requests

data = 'test data'
res = requests.post(url='http://localhost:8888',
                    data=data,
                    headers={'Content-Type': 'application/octet-stream'})

我们看到

POST http://localhost:8888/ HTTP/1.1
Host: localhost:8888
Content-Length: 9
Content-Type: application/octet-stream
Accept-Encoding: gzip, deflate, compress
Accept: */*
User-Agent: python-requests/1.0.4 CPython/2.7.3 Windows/Vista

test data

并使用urllib2

import urllib2

data = 'test data'    
req = urllib2.Request('http://localhost:8888', data)
req.add_header('Content-Length', '%d' % len(data))
req.add_header('Content-Type', 'application/octet-stream')
res = urllib2.urlopen(req)

我们得到

POST http://localhost:8888/ HTTP/1.1
Accept-Encoding: identity
Content-Length: 9
Host: localhost:8888
Content-Type: application/octet-stream
Connection: close
User-Agent: Python-urllib/2.7

test data

我没有看到任何差异,可以引起您观察到的不同行为。话虽如此,HTTP服务器检查User-Agent标头并根据其值改变行为并不罕见。尝试逐个更改Requests发送的标头,使其与urllib2发送的标头相同,并查看何时停止工作。


不知道为什么,但是使用requests模块相同的代码可以正常工作...非常感谢。虽然现在我非常好奇为什么urllib不起作用... - Mac
使用requests请看这里:https://dev59.com/BWct5IYBdhLWcg3wD5WU - lorenzo

3

2

您需要添加Content-Disposition头,类似于这样(尽管我在这里使用了mod-python,但原理应该是相同的):

request.headers_out['Content-Disposition'] = 'attachment; filename=%s' % myfname

我使用了类似于 curl --data-binary "@users.csv" -b cookie.txt -X POST http://myhost/site.py 的命令,Wireshark 显示它是 HTTP/POST 请求,因此我认为 curl 确实使用了 POST 方法,但它使用的是像我在上面评论中链接的第一个 pastie 中一样的 URL 编码文件来传输数据包内容。 - mrkafk
重新阅读短语“我正在尝试模仿这里的命令:”,并查看curl --data-binary "@image.png"的作用。 - Piotr Dobrogost
@PiotrDobrogost:第一句话是:“我正在编写一些代码来与Redmine进行接口交互,并且我需要在此过程中上传一些文件。” Curl的功能与否并不相关。 - mrkafk
@PiotrDobrogost:上传文件是OP的目标,而不是任何“捷径”,这是您不理解问题并错误地关注curl在此上下文中的奇特行为的借口。如果是curl/urlencoded问题,甚至会将其命名为“模拟curl”或类似的东西,而不是“Python POST二进制数据”。我认为对于OP或任何合理的人来说,无论他们以Redmine curl模仿方式还是其他方式上传文件,只要达到目标即可。 OP的目标没有任何“误导”的意思。 - mrkafk
我会尝试最后一次解释。在HTTP的上下文中,“上传文件”的术语在大多数情况下意味着做与浏览器在您有上传文件选项时所做的相同的事情。在这种情况下,浏览器使用“multipart/form-data”编码发送文件,并通常也发送文件名 - 请参见使用Python脚本从POST发送文件。从问题中给出的curl示例可以明确看出没有使用“multipart/form-data”编码,因此我们不讨论最常见的含义中的“文件上传” - Piotr Dobrogost
显示剩余8条评论

-2
你可以使用unirest,它提供了一种简单的方法来发送POST请求。
import unirest
 
def callback(response):
 print "code:"+ str(response.code)
 print "******************"
 print "headers:"+ str(response.headers)
 print "******************"
 print "body:"+ str(response.body)
 print "******************"
 print "raw_body:"+ str(response.raw_body)
 
# consume async post request
def consumePOSTRequestASync():
 params = {'test1':'param1','test2':'param2'}
 
 # we need to pass a dummy variable which is open method
 # actually unirest does not provide variable to shift between
 # application-x-www-form-urlencoded and
 # multipart/form-data
  
 params['dummy'] = open('dummy.txt', 'r')
 url = 'http://httpbin.org/post'
 headers = {"Accept": "application/json"}
 # call get service with headers and params
 unirest.post(url, headers = headers,params = params, callback = callback)
 
 
# post async request multipart/form-data
consumePOSTRequestASync()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接