欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-05-04 19:17:55
场景使用Python做自动化测试时,有时候需要从网络下载软件安装文件并安装。但是使用urllib库时,默认都是单线程下载文件,如果文件比较小还好说,如果文件有20M时,普通的网速就要等待很长的时间。有没有模块类似下载工具那样能多线程下载同一个文件?如果没有多线程下载单个文件的模块,那我们应该如何编码实现功能?说明Python作为日常的部署语言,编写自动化脚本目前看来还是比较方便的,因为它的库很多,动态语言特性灵活,自动内存管理,编码到执行都无需编译等待等。说到这个多线程下载单个...

场景

  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