根据http://www.oschina.net/code/snippet_70229_2407修改而来的增强版。貌似原版源自Axel这个多线程下载工具。

'''
Created on 2014-10-24 @author: Maple
''' import sys
import os
import time
import getopt
import urllib.request
import urllib.parse
from threading import Thread #===============================================================================
# def download(url, output=os.getcwd(), blocks=6, proxies=local_proxies)
# output:输出文件路径,默认为当前路径
# blocks:线程数
# proxies:代理地址
#=============================================================================== local_proxies = {}#代理地址 class Maple(Thread):
version = "Mozilla/5.0" def __init__(self, threadname, url, filename, ranges=0, proxies={}):
Thread.__init__(self, name=threadname)
self.name = threadname
self.url = url
self.proxies = proxies
self.filename = filename
self.ranges = ranges
self.downloaded = 0 def run(self): try:
self.downloaded = os.path.getsize( self.filename ) #获取已下载的文件字节块块,支持断点续传
except OSError:
#print 'never downloaded'
self.downloaded = 0
opener=GetUrlOpener(self.proxies) #根据代理参数生成相应的url opener
if self.ranges: #ranges为线程需要下载的文件块的字节范围
# rebuild start poind
self.startpoint = self.ranges[0] + self.downloaded #从已下载字节块后的位置开始下载 # This part is completed
if self.startpoint >= self.ranges[1]:
self.downloaded = self.ranges[1] - self.ranges[0]
print ('Part %s has been downloaded over.' % self.filename)
return
opener.addheaders=[('Range','bytes={}-{}'.format(self.startpoint, self.ranges[1])),('User-agent','Mozilla/5.0')] #添加请求头部内容,仅下载指定范围的字节,伪装成浏览器请求
print ('task %s will download from %d to %d' % (self.name, self.startpoint+1, self.ranges[1]+1))
else: #ranges未指定(文件大小未知,无法切割),从已下载字节块后的位置开始下载剩余全部字节
self.startpoint = self.downloaded
opener.addheaders=[('Range','bytes={}-'.format(self.startpoint)),('User-agent','Mozilla/5.0')]
self.fetchsize = 16384 #每次读取的字节数
self.urlhandle = opener.open(self.url) #打开文件地址
data = self.urlhandle.read( self.fetchsize )
while data: #循环读取数据写入临时文件,并更新已下载字节数
filehandle = open( self.filename, 'ab+' )
filehandle.write( data )
filehandle.close()
self.downloaded += len( data )
data = self.urlhandle.read( self.fetchsize ) def Sec2Time(second): #将秒数转换为标准时间格式。以为有现成的函数,结果愣是没找到
day=second//(3600*24)
second-=day*3600*24
hour=second//3600
second -=hour*3600
minute=second//60
second-=minute*60
if day == 0:
if hour == 0:
if minute == 0:
return '{:0.2f}S.'.format(second)
else:
return '{:02}M:{:0.2f}S'.format(minute,second)
else:
return '{:02}H:{:02}M:{:0.2f}S'.format(hour,minute,second)
else:
return '{:03}D:{:02}H:{:02}M:{:0.2f}S'.format(day,hour,minute,second) def GetUrlOpener(proxies={}): #分析代理参数,返回url opener。完整代理格式:user/passwd@http://127.0.0.1:8087。如格式不同,需要修改此分析函数
if proxies:
try:
ap=proxies.split('@')
if len(ap) > 1:
auth=ap[0]
addr=ap[1]
else:
addr=ap[0]
auth=''
if '://' in addr:
ptype=addr[:addr.find('://')]
phost=addr[addr.find('://')+3:]
else:
ptype='http'
phost=addr
proxy={ptype:ptype+'://'+phost}
proxy_handler = urllib.request.ProxyHandler(proxy)
except Exception as ex:
print(ex)
return urllib.request.build_opener()
try:
authlist=auth.split('/')
if len(authlist) > 1:
user=authlist[0]
passwd=authlist[1]
proxy_auth_handler = urllib.request.ProxyBasicAuthHandler()
proxy_auth_handler.add_password('realm',phost,user,passwd)
opener = urllib.request.build_opener(proxy_handler,proxy_auth_handler)
else:
opener = urllib.request.build_opener(proxy_handler)
return opener
except Exception as ex:
print(ex)
return urllib.request.build_opener(proxy_handler)
else:
# urlHandler=urllib.request.urlopen(url)
return urllib.request.build_opener() def GetUrlFileInfo(url,proxies={}): #获取要下载的文件的信息,包括文件名,文件类型和文件大小
scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url) #分析url
filename=urllib.parse.unquote(path) #如果url中的文件名部分存在中文,将其正确解码出来
filename=filename.split('/')[-1]
opener=GetUrlOpener(proxies) #通过网络请求读取响应头部,根据头部获取文件信息。文件名以服务器返回的文件名信息为准
urlHandler=opener.open(url)
headers=urlHandler.info()
if 'Content-Disposition' in headers: #Content-Disposition字段有可能获取到文件名,不过可能是乱码,没找到解决办法</span>
disposition=headers.get('Content-Disposition')
if 'filename=' in disposition:
filename = disposition.split('filename=')[1]
if filename[0] == '"' or filename[0] == "'":
filename = filename[1:-1]
filename=urllib.parse.unquote(filename)
if filename:
(name,ext)=os.path.splitext(filename)
else:
(name,ext)=('Unknown','')
if 'Content-Length' in headers: #获取文件长度,如果获取失败,则只能使用单线程下载
length=int(headers.get('Content-Length'))
else:
length=-1
(type, kind)=headers.get('Content-Type').split('/') #获取文件类型,备用
infos=[(name,ext),(type,kind),length]
return infos def SpliteBlocks(totalsize, blocknumber): #根据指定的线程数参数和获取到的文件长度划分各线程的下载范围
blocksize = totalsize//blocknumber
ranges = []
for i in range(0, blocknumber-1):
ranges.append((i*blocksize, i*blocksize +blocksize - 1))
ranges.append(( blocksize*(blocknumber-1), totalsize -1 )) return ranges
def islive(tasks): #检查各线程是否全部下载完成
for task in tasks:
if task.isAlive():
return True
return False def download(url, target=os.getcwd(), blocks=6, proxies=local_proxies): flag=True
print('Retrieving resource information...') url=urllib.parse.quote(url,safe='/%&@=+?$;,:') #将提供的url编码,非英文字符将被编码为标准格式
try:
infos=GetUrlFileInfo(url,proxies) #获取文件信息
except Exception as ex:
print(ex)
flag=False
if flag:
if not os.path.exists(target):
os.makedirs(target)
size=infos[2] #获取到的文件大小
output=os.path.join(target,''.join(infos[0])) #根据获取到的文件名和指定的保存目录生成完整路径
type=infos[1][0] starttime=time.time() #开始计时
print('Infomation:')
print('FileName:{0} FileType:{1} FileLength:{2}'.format(''.join(infos[0]),'/'.join(infos[1]),infos[2] if int(infos[2]) > 0 else 'Unknown')) #打印获取到的文件信息
if size > 0: #size大于0表示成功获取文件长度,可以进行多线程下载
print('Starting multithread download...')
ranges = SpliteBlocks( size, blocks )
else: #只能单线程下载,线程数置1,ranges置空,
print('Starting single thread download...')
ranges=()
blocks=1
threadname = [ infos[0][0]+"_thread_%d" % i for i in range(0, blocks) ] #生成线程名
filename = [ infos[0][0]+ "_tmpfile_%d" % i for i in range(0, blocks) ] #生成各线程的临时文件名
tasks = []
for i in range(0,blocks): #生成下载线程,设置为后台线程后启动,将线程加入到线程列表中
task = Maple( threadname[i], url, filename[i], ranges[i] if ranges else ranges,proxies)
task.setDaemon( True )
task.start()
tasks.append( task ) time.sleep( 1 )
downloaded = 0
while islive(tasks): #统计线程列表中各线程的状态,输出下载进度
downloaded = sum( [task.downloaded for task in tasks] )
if size > 0:
process = downloaded/float(size)*100
show = '\rFilesize:%d Downloaded:%d Completed:%.2f%%' % (size, downloaded, process)
else:
show = '\rDownloaded:%d ' % downloaded
sys.stdout.write(show)
sys.stdout.flush()
time.sleep( 0.2 )
endtime=time.time() #下载完成后停止计时
consuming=Sec2Time(endtime-starttime)
if size > 0: #多线程下载的后续处理
downloadsize = 0
for i in filename:
downloadsize += os.path.getsize(i)
if downloadsize == size:
show = '\rFilesize:%d Downloaded:%d Completed:%.2f%%\n' % (size, downloadsize,100)
else:
show = '\nSize is not mathed!\n'
flag=False
else: #单线程下载的后续处理
show = '\nTotal Size: %d\n'% downloaded
sys.stdout.write(show)
sys.stdout.flush()
if flag: #确认下载的临时文件没问题后将各文件整合为最终的目标文件
print('Integrating files...')
num=1
while os.path.exists(output): #防止与本地已存在文件重名
fname,fext=os.path.splitext(output)
if '('+str(num-1)+')'+fext in output:
output = output.replace('('+str(num-1)+')'+fext,'('+str(num)+')'+fext)
else:
fname += '('+str(num)+')'
output = fname+fext
num +=1
if len(filename) ==1 : #单线程下载的话,直接将下载的文件重命名为目标文件即可
os.rename(filename[0], output)
else: #多线程临时文件整合
filehandle = open( output, 'wb+' )
for i in filename:
try:
f = open( i, 'rb' )
filehandle.write( f.read() )
f.close()
os.remove(i)
except Exception as ex:
print(ex)
filehandle.close()
if os.path.exists(output):
print('Download Complete!')
else:
print('Failed to generate target file!')
try:
#os.remove(output)
pass
except:
pass
else:
for i in filename:
try:
os.remove(i)
pass
except:
pass
print('Download Failed!')
pass
print('Consuming: {}\n'.format(consuming)) #输出耗时
else:
print('Failed to retrieve resource information!')
sys.exit()
def main(argv): #处理传入参数,使用了getopt模块,另外有一个更强大的处理传入参数的模块optparse
try:
options,args=getopt.getopt(argv,'hu:f:n:p:',['help','url=','target=','num=','proxy='])
except Exception as ex:
print(ex)
sys.exit()
num = 2
url,target,proxies= '','','' url = 'http://www.pygtk.org/dist/pygtk2-tut.pdf'
target = '/home/maple/Desktop'
#proxies = 'http://127.0.0.1:8087'
#proxies={}
for name, value in options:
if name in ('-h','--help'):
print('No Help ^^')
sys.exit()
if name in ('-u','--url'):
url = value
if name in ('-t','--target'):
target = value
if name in ('-n','--num'):
num = int(value)
if name in ('-p','--proxy'):
proxies=value
#check args
download(url,target,num,proxies) if __name__ == '__main__':
main(sys.argv[1:])

这段代码在异常处理方面写得有些乱,没怎么关心异常处理,需要时再改吧

另外多线程下载时,如果使用了代理,会导致下载到的文件与服务器提供的文件大小不符。从而下载失败。我使用的是GoAgent,代理服务器会自动对目标文件进行多线程下载,无视程序指定的下载字节范围。第一个线程就会下载到完整的文件,其他线程会下载冗余内容。没有找到规范的处理办法。变通的处理办法有2种:

1、将使用了代理的多线程下载强制指定为单线程下载

2、不进行文件大小的校验,将多线程下载的0号临时文件重命名为目标文件,其他临时文件删除。

两种方法实现都很简单,但是破坏代码的整体逻辑。没有加入代码中。运行截图:

Python3 多线程下载代码的更多相关文章

  1. Python之FTP多线程下载文件之多线程分块下载文件

    Python之FTP多线程下载文件之多线程分块下载文件 Python中的ftplib模块用于对FTP的相关操作,常见的如下载,上传等.使用python从FTP下载较大的文件时,往往比较耗时,如何提高从 ...

  2. Android版多线程下载器核心代码分享

    首先给大家分享多线程下载核心类: package com.example.urltest; import java.io.IOException; import java.io.InputStream ...

  3. 【Python数据分析】Python3多线程并发网络爬虫-以豆瓣图书Top250为例

    基于上两篇文章的工作 [Python数据分析]Python3操作Excel-以豆瓣图书Top250为例 [Python数据分析]Python3操作Excel(二) 一些问题的解决与优化 已经正确地实现 ...

  4. 【Java EE 学习 22 下】【单线程下载】【单线程断点下载】【多线程下载】

    一.文件下载简述 1.使用浏览器从网页上下载文件,Servlet需要增加一些响应头信息 (1)response.setContentType("application/force-downl ...

  5. Java--使用多线程下载,断点续传技术原理(RandomAccessFile)

    一.基础知识 1.什么是线程?什么是进程?它们之间的关系? 可以参考之前的一篇文章:java核心知识点学习----并发和并行的区别,进程和线程的区别,如何创建线程和线程的四种状态,什么是线程计时器 简 ...

  6. android 多线程下载 断点续传

    来源:网易云课堂Android极客班第八次作业练习 练习内容: 多线程 asyncTask handler 多线程下载的原理 首先获取到目标文件的大小,然后在磁盘上申请一块空间用于保存目标文件,接着把 ...

  7. 无废话Android之smartimageview使用、android多线程下载、显式意图激活另外一个activity,检查网络是否可用定位到网络的位置、隐式意图激活另外一个activity、隐式意图的配置,自定义隐式意图、在不同activity之间数据传递(5)

    1.smartimageview使用 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android&q ...

  8. android程序---->android多线程下载(一)

    多线程下载是加快下载速度的一种方式,通过开启多个线程去执行一个任务,可以使任务的执行速度变快.多线程的任务下载时常都会使用得到断点续传下载,就是我们在一次下载未结束时退出下载,第二次下载时会接着第一次 ...

  9. [C#]多线程下载

    发现电脑里以前编写的下载程序... 做个记录,那时做的挺匆忙的,没用委托,通过公开出窗体来修改下载进度,做的比较乱... ==!! 程序具体功能(流程): 1.检测系统托盘图标内的进程名是否符合要求 ...

随机推荐

  1. [转]Vs解决方案的目录结构设置和管理

    原文地址:[转]Vs解决方案的目录结构设置和管理 作者:大明   以下内容为“原创”+“转载” 首先,解决方案和项目文件夹包含关系(c++项目): VS解决方案和各个项目文件夹以及解决方案和各个项目对 ...

  2. ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室 实战系列(不断更新中)

    项目简介 利用ASP.NET SignalR技术与Layim前端im框架实现的一个简单的web聊天室,包括单聊,群聊,加好友,加群,好友搜索,管理,群组管理,好友权限设置等功能.涉及技术: Elast ...

  3. English sentence

    For a better environment, we should teach our children to put litter/garbage/trash into dustbin/dust ...

  4. mysql 安装日志

    善于使用 mysqld.exe --console 来得到提示

  5. textview 显示html方法解析

    现在网络的繁盛时代,光文字是不能满足人们的胃口的,图片,flash,音频,视频就成为浏览网页的主流显示,在手机上也一样.在手机上显示从网络端获取的数据显示,大家很自然的想起两种方式,一种就是webvi ...

  6. 一张图教你搞定Mac App Store 应用安装包存储路径

    还在为找不到App Store 更新应用的安装文件发愁吗?是否有过多个人同时需要更新Xcode,都自己下载一次的痛苦经历? 大家都知道通过苹果服务器下载东西,确实难耐!AppStore 甚至都经常提示 ...

  7. Django model 中meta options

    之前学了abstract,这是后续的一些options app_label: app_label的作用是:如果一个model定义不在INSTALLED_APPS中,那么此时就需要声明,这个model的 ...

  8. JAVA设计模式之桥梁模式

    在阎宏博士的<JAVA与模式>一书中开头是这样描述桥梁(Bridge)模式的: 桥梁模式是对象的结构模式.又称为柄体(Handle and Body)模式或接口(Interface)模式. ...

  9. SQL SERVER安装序列号

    MICROSOFT SQL SERVER 2012 DEVELOPER 版 序列号:YQWTX-G8T4R-QW4XX-BVH62-GP68Y MICROSOFT SQL SERVER 2012 EN ...

  10. 纯CSS实现圆形进度条

    CSS的优点在于的可以随意组合HTML元素来实现许多中效果,这儿我将使用CSS来实现一个运行进度条,效果如下: 思路是用两个div来作为进度条外观,表示进度的div和外面div宽度和高度保持一致,并是 ...