python多线程下单个文件源码

网上看了很多, 真能用的不多, 主要是不处理解决网络异常, 导致网络特好的情况下才能正常工作. 更多的不识别非断点续传网站, 导致这些网站成功下载但是文件不对. 这里我改了一个能用的,  速度还不错, 加入了进度条,看上去很直观, 测试外网的下载地址最好用上代理,  否则速度慢. 有些网站不允许用request访问, 如果不能正常下载先确定是不是对方设置了. 有些下载链接是要跳转的, 本代码没处理.  有的链接用basename取到的文件名不是合法的windows文件名,在硬盘上保存会发生异常, 如果你使用本程序异常且确定不属于以上情况, 可以留言附上你的下载链接,我会抽时间检测.  验证数据错的技巧是下载ZIP,RAR,gz之类的压缩文件, 能正常解压说明下载无误. (重要提示:本程序里测试的URL下载如果是EXE文件请不要执行,不保证无毒)

实测: https://dl.google.com/dl/android/studio/ide-zips/3.5.2.0/android-studio-ide-191.5977832-windows.zip

移动100M光纤, 10个线程, 不开代理用时:108-130s左右, 开代理也差不多, 速度接近专业工具,  主要归功于对方网站好,基本不发生重试.

可改进的地方是:

1. 线程异常后不要完整重新下载整个数据块, 而应当哪里异常哪里重新开始,这样下载大文件可以大大提高速度.

2. 线程异常超过重试次数后应当返回一个错误码, 否则下一步还正常合并文件,导致数据错.得到每个线程的返回值有点麻烦,暂时不修改了.

原始代码来自网上, 原作者是谁忘记了. 在此谢过!

from time import sleep, time
from threading import Thread
from multiprocessing import Process
from os import listdir, remove
from os.path import basename
from tqdm import tqdm
from urllib.request import Request,urlopen
from urllib import error
import os

#缓冲区大小
BUFFER_SIZE = 64*1024
#存放所有进程/线程
threads =[]
def download_thread(url,start,end,thread_num):
    total_down_size = end - start
    req = Request(url, headers={'Range': f'bytes={start}-{end}'})
    current_fn = f't_{thread_num}_{basename(url)}'
    #每个进程/线程下载目标文的一部分
    retry_weberr_flag = True        # 当发生网络错误时重试标志
    retry_weberr_times = 0          # 网络错误时重试次数
    retry_weberr_max_times = 3

    while retry_weberr_flag:
        with open(current_fn,'wb') as fp_local:
            TotalSize = 0
            try:
                with urlopen(req,timeout=20) as fp_web:
                    with tqdm(total=total_down_size , desc=f'线程{thread_num}下载中:' ) as bar:        # 进度条
                        while True:
                            # 分块下载,读不到数据表示结束
                            data = fp_web.read(BUFFER_SIZE)
                            data_len = len(data)
                            TotalSize = TotalSize + data_len
                            if not data:
                                bar.close()
                                break
                            fp_local.write(data)
                            fp_local.flush()
                            sleep(0.1)
                            bar.update(data_len)
                retry_weberr_flag = False
            except Exception as e:
                fp_local.close()                # 异常时必须关闭本地文件重写, 否则可能写入错误数据
                if  (retry_weberr_times < retry_weberr_max_times) :
                    retry_weberr_times = retry_weberr_times +1
                    print ( f"线程{thread_num} 发生异常, 第{retry_weberr_times}次重试" )                    
                    sleep(2)
                    continue
                else:
                    retry_weberr_flag =False  

def download(url,count):
    '''url:耍下载的文件地址;count:线程数量'''
    req = Request(url, headers={'Range': 'bytes=0-20'})
    try:
        with urlopen(req) as fp:
            # 用 'content-Range' 判读断点续传
            rangtmp = fp.getheader('content-Range')
            if rangtmp:
                print("可以断点续传")
                length = int(fp.getheader('content-Range').split('/')[1])
            else:
                print("可能无法续传, 线程数自动设为1")
                length = fp.length
                count = 1
    except error.URLError as e:
        print ("发生异常 URLError:",e.reason )
        return False
    except error.HTTPError as e:
        print ("发生异常 HTTPError, 错误代码:", e.code,'错误原因:\n',e.reason,'返回Http头:\n',e.headers)
        if e.code==403:
            print("对方网站禁止访问,程序返回")
            return False

    print(f'文件总大小:{length}字节')
    #每个线程负责下载的字节数量
    #length-1是为了保证最后一个线程有活干
    each = (length-1) // count
    #创建线程,开始下载
    #防止下面的range对象为空时,最后一个进程/线程因为i没定义而创建失败
    i = -1
    for i in range(count-1):
        start, end = i*each, (i+1)*each
        t = Thread(target=download_thread,
                           args=(url,start,end-1,i))
        t.start()
        threads.append(t)
        #每隔2秒启动1个线程
        sleep(2)
    #最后一个线程
    t = Thread(target=download_thread,
                       args=(url,each*(i+1),length,i+1))
    t.start()
    threads.append(t)
    return True

def downloadOneFile(url, threadCount =5 , newLocalName=''):
    count = threadCount     # 默认线程数量
    #下载文件
    start_time = time()
    retry_flags=True
    retry_times =0 
    retry_max_times = 3
    retry_wait_len = 2  
    while retry_flags:
        if ( not download(url,count) ):   # 下载出错
            retry_times = retry_times +1
            if (retry_times > retry_max_times):
                retry_flags = False
            else:
                sleep(retry_wait_len)
                print(f" 下载出错, 第{retry_times}次重试")
                continue
        else:
            break       # 下载正常

    if (not retry_flags ) :
        # 如果没有下载正常,函数返回 false
        return False

    for t in threads:
        t.join()
    #获取所有临时文件,按编号顺序拼接
    temp_files = [fn for fn in listdir() if fn.startswith('t_')]
    temp_files.sort(key=lambda fn: int(fn.split('_')[1]))

    # basename(url)
    if newLocalName=='':
        localSaveName = basename(url)
    else:
        localSaveName = newLocalName
    with open( localSaveName , 'wb') as fp_final:
        for fn in temp_files:
            with open(fn, 'rb') as fp_temp:
                fp_final.write(fp_temp.read())
    #删除临吋文件
    for fn in temp_files:
        remove(fn)
    os.system("cls")
    print(f' 文件下载完成。用时:{time()-start_time}秒, 保存为: {basename(localSaveName)}')
    return True


if __name__ == "__main__":
    url ="http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz"
    # 不能续传 
    # url ="https://iiif.nlm.nih.gov/nlm:nlmuid-101455976-img/full/full/0/default.jpg"
    # url ="http://n.sinaimg.cn/photo/transform/700/w1000h500/20200907/eac7-iytwsca6488747.jpg"
    # url ="https://img.supmil.net/data/attachment/forum/201911/26/215029zyzr7gyqmydj9y25.png"
    # url ="https://www.python.org/ftp/python/2.7.18/python-2.7.18.amd64.msi"
    # url ="https://www.python.org/ftp/python/3.8.5/python-3.8.5-embed-amd64.zip"
    # url ="https://raw.githubusercontent.com/zalandoresearch/fashion-mnist/master/data/fashion/train-images-idx3-ubyte.gz"
    # url = "https://az764295.vo.msecnd.net/stable/e790b931385d72cf5669fcefc51cdf65990efa5d/VSCode-win32-x64-1.49.0.zip"

    downloadOneFile(url,10)

    

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值