当前位置: 移动技术网 > IT编程>脚本编程>Python > 荐 [Python]_[初级]_[多线程下载单个文件]

荐 [Python]_[初级]_[多线程下载单个文件]

2020年07月20日  | 移动技术网IT编程  | 我要评论

场景

  1. 使用Python做自动化测试时,有时候需要从网络下载软件安装包并安装。但是使用urllib库时,默认都是单线程下载文件,如果文件比较小还好说,如果文件有20M时,普通的网速就要等待很长的时间。有没有模块类似下载工具那样能多线程下载同一个文件?

  2. 如果没有多线程下载单个文件的模块,那我们应该如何编码实现功能?

说明

  1. Python作为日常的部署语言,编写自动化脚本目前看来还是比较方便的,因为它的库很多,动态语言特性灵活,自动内存管理,编码到执行都无需编译等待等。

  2. 说到这个多线程下载单个文件,在Python的使用手册里,真没发现有相关的模块做这个功能。搜索了下也没简单能用的模块。

  3. 实现多线程下载同一个静态文件(注意是静态文件,而流式文件是获取不到大小的),原理就是每个线程下载文件的不同部分(一个文件可以看成不同大小的块组成),这样每个线程执行完之后,文件就全部下载完了。多线程下载速度也不一定是快的,要看下载网站的带宽出口,如果它的带宽出口比较小,那么多线程都会比单线程快。如果像腾讯那种大厂,它的网站带宽很大,还有DDOS检测防护,比你自己的网络带宽还大,所以基本上只用一个线程就很快了。

  4. 多线程下载文件的不同部分,首先需要发送HEAD请求获取http头里的Content-Length的文件大小,之后才能根据文件大小和线程个数分成多个块,每个块有起始位置和结束位置,而每个线程只下载自己的文件块就行了。

  5. 这里说明下,访问https和支持TLS协议,需要安装额外的模块,请查看关于如何使用urllib3库和访问https的问题,总的说需要先通过pip安装pyOpenSSL, cryptography, idna, certifi模块。

pip install pyOpenSSL
pip install cryptography
pip install idna
pip install certifi
  1. 之后如果想支持命令行参数管理,可以安装click模块。
pip install click
  1. 最后就是需要设定一个下载缓存大小,我这里设置为100k。可以根据自己的网速设定,太大的话,http请求可能就会超过远程网站的返回大小导致速度很慢。之后还需要通过发送GET请求,附带请求头内容属性Range来获取文件指定范围的数据。当然如果某个线程负责下载的文件块过大,我们还需要分割为100k的子块,循环请求多次直到完成下载负责的文件块。
headers = {"Range":"bytes=%d-%d"%(start,end)}
res = self.http.request('GET',self.url,headers=headers,preload_content=False)
  1. 我这里的测试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独立文件

  1. 我使用pyinstaller打包了为单个独立的EXE文件, 如果没有Python环境的,可以从
    https://gitee.com/tobey-robot/AutomaticPython/releases下载MultiThreadDownloadFile.zip

参考

关于如何使用urllib3库和访问https的问题

urllib3库的官方说明

Python开发环境配置

Simple Multithreaded Download Manager in Python

python多线程下载文件

本文地址:https://blog.csdn.net/infoworld/article/details/107430867

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网