标签归档:post

使用python向服务器POST大文件

使用python向服务器POST大文件

python 对http操作有几个库 urllib 、 urllib2 还有httplib

httplib比较偏底层 一般情况下使用urllib和urllib2就行了

NOTICE

在python3中urllib与urllib2被分割合并为了 urllib.request, urllib.parse, and urllib.error

httplib重命名为 http.client

分析http协议

python的这几个库中并没有提供直接上传文件的接口 我们先看下普通浏览器是怎么上传文件的 这里我在本地创建简单php程序 若有提交文件,则打印出文件相关的信息。否则显示一个上传表单表单

<?php
header('Content-Type: text/html; charset=utf-8');
if(!empty($_FILES)){
    var_dump($_FILES);
    exit;
}

?>

<!DOCTYPE HTML>
<html lang="en-US">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body style="text-align:center;">
    <form action="" method="POST" enctype="multipart/form-data" >
        <input type="text" name="username" value="ksc"/> <br/>
        <input type="file" name="file" />
        <input type="submit" value="submit"/>
    </form>
</body>
</html>

这里推荐一个抓包工具fildder 可以很方便的抓取htttp数据,并且直观的显示

下面就是抓取到的内容

POST http://localhost/test/postfile.php HTTP/1.1
Host: localhost
Connection: keep-alive
Content-Length: 295
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: http://localhost
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynHbCm1TeYE1AHFXv
DNT: 1
Referer: http://localhost/test/postfile.php
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6

------WebKitFormBoundarynHbCm1TeYE1AHFXv
Content-Disposition: form-data; name="username"

ksc
------WebKitFormBoundarynHbCm1TeYE1AHFXv
Content-Disposition: form-data; name="file"; filename="b.txt"
Content-Type: text/plain

我是文件内容
------WebKitFormBoundarynHbCm1TeYE1AHFXv--

从第1-13行就是请求消息http头部的内容,然后下面有个空行 比较重要的几行是

POST http://localhost/test/postfile.php HTTP/1.1
Host: localhost
Content-Length: 295
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynHbCm1TeYE1AHFXv

内容数据

请求消息(Request message)有以下几部分组成

  • 请求行(request line), 例如 GET /images/logo.png HTTP/1.1
  • 请求头(Request Headers) 比如上面的 Host: localhost、 Content-Length: 295
  • 空行
  • 消息主体 message body

NOTICE

请求行与请求头必须以结尾,空行只能有不能有空格什么的 在HTTP/1.1协议中,除了Host 所有的请求头都是可选的(当然若上传文件的话,就必须设置了Content-Length和Content-Type了, 不然服务器收不到数据的,虽然也能成功响应)

这里message body的类型是multipart/form-data;

boundary 是随机的内容每次请求都不一样, Content-Type为 multipart/form-data; 可同时传输多项数据,而这些数据就是通过 boundary分割开来的

每一项的数据都是’–‘+boundary+换行开始 ,然后是Content-Disposition: form-data;name=”表单项名”

若是文件的话 还有个filename 以及Content-Type,接下来一个空行

最后’–‘+boundary+’–‘+换行结束

到这里整个http请求就结束了

模拟提交数据

其实http协议就是字符串按照约定规则拼接到一起 然后服务器再来解析得到数据 所以我们自己直接使用socket也能发起了一个http请求

但是有了urllib2我们可以省很多事 只需“拼接”内容部分就行了

urllib2.urlopen(url[, data][, timeout])

url 可以是一个链接或者Request对象 data 就是我们要“拼接的内容”

# coding=utf-8

import urllib2
import mimetypes
import os
import uuid

mimetypes.init()

url='http://localhost/test/postfile.php'
fileFieldName='myfile'
file_path='/a.txt'

boundary='--------ksc'+uuid.uuid4().hex;
print('boundary:'+boundary)
req = urllib2.Request(url)
req.add_header("User-Agent", 'ksc')
req.add_header("Content-Type", "multipart/form-data, boundary="+boundary)
def getdata(boundary,file_path):
    global fileFieldName
    file_name=os.path.basename(file_path)
    file_name=file_name.encode('utf-8')
    file_type= mimetypes.guess_type(file_name)[0] #根据文件名匹配文件的mime类型若没有匹配到会返回None
    if file_type==None:
        file_type = "text/plain; charset=utf-8"

    print file_type
    print file_name
    fileData=open(file_path,'rb').read()
    CRLF='\r\n'
    body = '';     
    body += '--' + boundary + CRLF;
    body += 'Content-Disposition: form-data; name="'+fileFieldName+'"; filename="' + file_name + '"'+CRLF;     
    body += "Content-Type: "+file_type+CRLF
    body += CRLF;     
    body += fileData + CRLF;     
    body += "--" + boundary + "--"+CRLF;  
    print 'body size:{0}'.format(len(body))
    return body

#req.add_header('Content-Length',len(body) ) #urllib2会自动添加
res = urllib2.urlopen(req,getdata(boundary,file_path))
print res.read().decode('utf-8');

下面是运行后的输出结果

boundary:--------ksc114eaa38291f43fdaca15ab3b4265a82
text/plain
a.txt
body size:388
array(1) {
  ["myfile"]=>
  array(5) {
    ["name"]=>
    string(5) "a.txt"
    ["type"]=>
    string(10) "text/plain"
    ["tmp_name"]=>
    string(24) "D:\xampp\tmp\php151B.tmp"
    ["error"]=>
    int(0)
    ["size"]=>
    int(197)
  }
}

php能正常接收传输的文件了 但是这里有个问题 ,我们拼接数据的时候使用的是

fileData=open(file_path,'rb').read()

直接把文件内容读到内存中去了,小文件还好,要是几百M上G的文件还用这种方法就不行了,这样会把你的内存吃干。

所以我们就需要弄个变通的方法,让它一块一块的发送数据,读一点发送一点。 这几个库并没有提供类似的方法实现这一个功能,所以就需要自己动手了。

首先介绍一个python的关键字 yield 关于介绍python yield的文章可以查看Python yield 使用浅析 可以通过它来实现一个生成器(generator) 来不断的读取文件数据,而且是只在我们需要的时候读取

def read_file(fpath): 
    BLOCK_SIZE = 1024 
    with open(fpath, 'rb') as f: 
        while True: 
            block = f.read(BLOCK_SIZE) 
            if block: 
                yield block 
            else: 
                return

然后我重写了 httplib 中 HTTPConnection.send(data)使它可以接收一个generator,从里面取数据然后发送 下面是原版HTTPConnection.send的源码

def send(self, data):
    """Send data to the server."""
    if self.sock is None:
        if self.auto_open:
            self.connect()
        else:
            raise NotConnected()

    if self.debuglevel > 0:
        print "send:", repr(data)
    blocksize = 8192
    if hasattr(data,'read') and not isinstance(data, array):
        if self.debuglevel > 0: print "sendIng a read()able"
        datablock = data.read(blocksize)
        while datablock:
            self.sock.sendall(datablock)
            datablock = data.read(blocksize)
    else:
        self.sock.sendall(data)

它支持文件类型File Object 或者是一个字符串

下面是个重新版本

def send(self, data):
    """Send data to the server."""
    if self.sock is None:
        if self.auto_open:
            self.connect()
        else:
            raise NotConnected()

    if self.debuglevel > 0:
        print "send:", repr(data)
    blocksize = 8192
    if hasattr(data,'next'):#支持迭代
        for datablock in data:
            self.sock.sendall(datablock)
    elif hasattr(data,'read') and not isinstance(data, array):
        if self.debuglevel > 0: print "sendIng a read()able"
        datablock = data.read(blocksize)
        while datablock:
            self.sock.sendall(datablock)
            datablock = data.read(blocksize)
    else:
        self.sock.sendall(data)

当然若想实现完美的文件提交,还需要做个封装,下面是一个简单的实现,还有写不完善的地方,post数据只能传文件,打算做个python lib封装一下

# coding=utf-8

import httplib
import mimetypes
import os
import uuid
import socket

mimetypes.init()

url='http://localhost/test/postfile.php'
fileFieldName='myfile'
file_path='/temp/a.txt'

content_length=0   
class builder:

    def __init__(self):

        self.boundary='--------ksc'+uuid.uuid4().hex;
        self.boundary='--------kscKKJFkfo93jmjfd0ismf'
        self.ItemHeaders=[];
        self.CRLF='\n'

    def getBoundary(self):
        return self.boundary

    def getHeaders(self):    
        '''return Request Headers'''
        headers={"Content-Type":"multipart/form-data, boundary="+self.boundary
                ,"Content-Length":self.getContentLength()
                }
        return headers        
    def putItemHeader(self,fieldName,file_path):

        file_name=os.path.basename(file_path)
        print file_name
        file_name=file_name.encode('utf-8')
        file_type= mimetypes.guess_type(file_name)[0] #guess file's mimetype
        if file_type==None:
            file_type = "text/plain; charset=utf-8"

        print file_type

        CRLF=self.CRLF
        head = '';     
        head += '--' + self.boundary + CRLF;
        head += 'Content-Disposition: form-data; name="'+fieldName+'"; filename="' + file_name + '"'+CRLF;     
        head += "Content-Type: "+file_type+CRLF
        head += CRLF;
        self.ItemHeaders.append({'head':head,'file_path':file_path})

    def getContentLength(self):    
        length=0
        for item in self.ItemHeaders:
            length+=len(item['head'])+os.path.getsize(item['file_path'])+len(self.CRLF)

        return length+len("--" + self.boundary + "--"+self.CRLF)

    def getdata(self):

        blocksize=4096
        for item in self.ItemHeaders:
            yield item['head']

            fileobj=open(item['file_path'],'rb')
            while True:
                block = fileobj.read(blocksize)
                if block:
                    yield block
                else:
                    yield self.CRLF
                    break

        body = "--" +self.boundary + "--"+self.CRLF;  

        yield body

class MyHTTPConnection(httplib.HTTPConnection):
    def send(self, value):
        if self.sock is None:
            if self.auto_open:
                self.connect()
                print '-----------------ksc: reconnect'
            else:
                raise NotConnected()

        try:
            if hasattr(value,'next'):
                print "--sendIng an iterable"
                for data2 in value:
                    self.sock.sendall(data2)    
            else:
                print '\n--send normal str'
                self.sock.sendall(value) 
        except socket.error, v: 
            if v[0] == 32:      # Broken pipe
                print(v)
                self.close()
            raise
        print '--send end' 

bl=builder()    

bl.putItemHeader(fileFieldName, file_path) 
bl.putItemHeader('bb', u'/temp/贪吃蛇.zip') 
#bl.putItemHeader('zipfile', u'/temp/a.zip') 

#注意web端post数据大小限制  
#这里 php.ini post_max_size = 30M

content_length=bl.getContentLength()
print "content_length:"+str(content_length)

url='/test/postfile.php'
#这里使用fiddler捕捉数据包,设置了fiddler为代理
fiddler_monitor=True
fiddler_monitor=False

if fiddler_monitor:
    httpconnect=MyHTTPConnection('127.0.0.1',8888)
    url='http://localhost'+url
else:
    httpconnect=MyHTTPConnection('localhost')

httpconnect.set_debuglevel(1)

headers = bl.getHeaders()
data    = bl.getdata()

httpconnect.request('POST', url, data ,headers)

httpres=httpconnect.getresponse()
print httpres.read().decode('utf-8')

现在有很多现成的第三方库比如 poster Requests 可以实现这些功能了,但是由于是第三方库需要用户手动安装,在一个我需要上传大文件时有个回调功能,这样就能显示进度了。 等运行稳定了会添加到kuaipan cli 里面,就可以摆脱poster依赖

参考资料

HTTP