[Python]_[初级]_[多线程下载单个文件]
场景
-
使用
Python
做自动化测试时,有时候需要从网络下载软件安装包并安装。但是使用urllib
库时,默认都是单线程下载文件,如果文件比较小还好说,如果文件有20M
时,普通的网速就要等待很长的时间。有没有模块类似下载工具那样能多线程下载同一个文件? -
如果没有多线程下载单个文件的模块,那我们应该如何编码实现功能?
说明
-
Python
作为日常的部署语言,编写自动化脚本目前看来还是比较方便的,因为它的库很多,动态语言特性灵活,自动内存管理,编码到执行都无需编译等待等。 -
说到这个多线程下载单个文件,在
Python
的使用手册里,真没发现有相关的模块做这个功能。搜索了下也没简单能用的模块。 -
实现多线程下载同一个静态文件(注意是静态文件,而流式文件是获取不到大小的),原理就是每个线程下载文件的不同部分(一个文件可以看成不同大小的块组成),这样每个线程执行完之后,文件就全部下载完了。多线程下载速度也不一定是快的,要看下载网站的带宽出口,如果它的带宽出口比较小,那么多线程都会比单线程快。如果像腾讯那种大厂,它的网站带宽很大,还有
DDOS
检测防护,比你自己的网络带宽还大,所以基本上只用一个线程就很快了。 -
多线程下载文件的不同部分,首先需要发送
HEAD
请求获取http
头里的Content-Length
的文件大小,之后才能根据文件大小和线程个数分成多个块,每个块有起始位置和结束位置,而每个线程只下载自己的文件块就行了。 -
这里说明下,访问
https
和支持TLS
协议,需要安装额外的模块,请查看关于如何使用urllib3库和访问https的问题,总的说需要先通过pip
安装pyOpenSSL, cryptography, idna, certifi
模块。
pip install pyOpenSSL
pip install cryptography
pip install idna
pip install certifi
- 之后如果想支持命令行参数管理,可以安装
click
模块。
pip install click
- 最后就是需要设定一个下载缓存大小,我这里设置为
100k
。可以根据自己的网速设定,太大的话,http
请求可能就会超过远程网站的返回大小导致速度很慢。之后还需要通过发送GET
请求,附带请求头内容属性Range
来获取文件指定范围的数据。当然如果某个线程负责下载的文件块过大,我们还需要分割为100k
的子块,循环请求多次直到完成下载负责的文件块。
headers = {"Range":"bytes=%d-%d"%(start,end)}
res = self.http.request('GET',self.url,headers=headers,preload_content=False)
- 我这里的测试
Python
版本是3.7
.
代码
MultiThreadDownloadFile.py
import threading
import time
import urllib3
import urllib3.contrib.pyopenssl
import certifi
import click
import random
lock = threading.Lock()
count = 0
def requestFileSize(http,url):
r = http.request('HEAD',url)
return r.headers["Content-Length"]
def test_urllib3(http,url):
header = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36"
}
response = http.request('GET', url, None, header)
data = response.data.decode('utf-8') # 注意, 返回的是字节数据.需要转码.
print (data) # 打印网页的内容
class MulThreadDownload(threading.Thread):
def __init__(self,http,url,startpos,endpos,fo):
super(MulThreadDownload,self).__init__()
self.url = url
self.startpos = startpos
self.endpos = endpos
self.fo = fo
self.http = http
def downloadBlock(self,start,end):
headers = {"Range":"bytes=%d-%d"%(start,end)}
res = self.http.request('GET',self.url,headers=headers,preload_content=False)
lock.acquire()
self.fo.seek(start)
global count
count = (count+ len(res.data))
print("download total %d" % count)
self.fo.write(res.data)
self.fo.flush()
lock.release()
def download(self):
print("start thread:%s at %s" % (self.getName(), time.process_time()))
bufSize = 102400
pos = self.startpos+bufSize
while pos < self.endpos:
time.sleep(random.random()) # 延迟 0-1s,避免被服务器识别恶意访问
self.downloadBlock(self.startpos,pos)
self.startpos = pos+1
pos = self.startpos + bufSize
self.downloadBlock(self.startpos,self.endpos)
print("stop thread:%s at %s" % (self.getName(), time.process_time()))
def run(self):
self.download()
def createFile(filename,size):
with open(filename,'wb') as f:
f.seek(size-1)
f.write(b'\x00')
@click.command(help="""多线程下载单个静态文件,注意,目前不支持数据流文件.如果下载不了,请减少线程个数. \n
MultiThreadDownloadFile.py pathUrl pathOutput""")
@click.option('--threads_num',default=2, help="线程个数")
@click.option('--url_proxy',default="", help="HTTP代理")
@click.argument('path_url',type=click.Path())
@click.argument('path_output',type=click.Path())
@click.pass_context
def runDownload(ctx,threads_num,url_proxy,path_url,path_output):
print(" threadNum: %d\n urlProxy: %s\n pathUrl: %s\n PathOutput %s\n"
% (threads_num,url_proxy,path_url,path_output))
http = None
if len(url_proxy) == 0:
http = urllib3.PoolManager(cert_reqs='CERT_REQUIRED', ca_certs=certifi.where())
else:
http = urllib3.ProxyManager(url_proxy,cert_reqs='CERT_REQUIRED', ca_certs=certifi.where())
print(path_url)
print(http)
fileSize = int(requestFileSize(http,path_url))
print(fileSize)
step = fileSize // threads_num
mtd_list = []
createFile(path_output,fileSize)
startTime = time.time()
# rb+ ,二进制打开,可任意位置读写
with open(path_output,'rb+') as f:
loopCount = 1
start = 0
while loopCount < threads_num:
end = loopCount*step -1
t = MulThreadDownload(http,path_url,start,end,f)
t.start()
mtd_list.append(t)
start = end+1
loopCount = loopCount+1
t = MulThreadDownload(http,path_url,start,fileSize-1,f)
t.start()
mtd_list.append(t)
for i in mtd_list:
i.join()
endTime = time.time()
print("Download Time: %fs" % (endTime - startTime))
if __name__ == "__main__":
urllib3.contrib.pyopenssl.inject_into_urllib3()
random.seed()
runDownload(obj = {})
下载
如何执行
python MultiThreadDownloadFile.py --help
python MultiThreadDownloadFile.py http://dldir1.qq.com/invc/tt/QTB/Wechat_QQBrowser_Setup.exe setup.exe
MultiThreadDownloadFile.exe --help
MultiThreadDownloadFile.exe http://dldir1.qq.com/invc/tt/QTB/Wechat_QQBrowser_Setup.exe setup.exe
下载EXE独立文件
- 我使用
pyinstaller
打包了为单个独立的EXE
文件, 如果没有Python
环境的,可以从
https://gitee.com/tobey-robot/AutomaticPython/releases下载MultiThreadDownloadFile.zip
参考
Simple Multithreaded Download Manager in Python
本文地址:https://blog.csdn.net/infoworld/article/details/107430867