利用requests与Threading编写python多线程HTTP下载器

本文介绍了一个自制的高效下载器,采用多线程技术并利用Python的requests模块进行数据请求,通过分段下载和断点续传功能提高了下载速度和稳定性。文章详细讲解了如何利用open函数的seek()方法进行精确的数据写入,以及如何通过全局变量和JSON文件存储下载进度,实现了断点续传。同时,文章还分享了如何实时显示下载进度和网速,以及在多线程环境下如何统计下载信息。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

准备

最近神经网络研究遇到了一点瓶颈,于是想着琢磨点其他的东西缓冲一下,正好那天遇到了一些关于下载的问题,我就想着这些网络上的下载器,不是垃圾就是广告太多,还慢,既然这样,那就自己写一个呗!
基本思路就是利用requests这个模块向路由请求数据,然后open打开文件把数据写进去。考虑到多线程,那得利用open函数里的seek()去指定每个线程负责写入那一块数据,然后大家一起工作最后就拼凑成一个完整的文件。
但是,既然是写一个下载器,那么,文件中断下载了,那也不能重新下啊。于是我就增加了一个data文件,用来记录本次下载的数据指针,这样就算中途断开也能从这个data文件里读取到上一次下载了那些数据,然后继续从上次断开的地方继续下载!

分段下载的requests请求

多线程下载的关键就是如何从服务器分段请求数据,这个我也是从别人哪里看来的:

headers = {'Range': 'bytes=%d-%d'%(startSeek,endSeek)}
re = requests.get(self.url, headers=headers, stream=True)

像这样请求,服务器就会返回你 ‘bytes’ 标段的数据段。

分段存储open()

有了分开下载以后,自然是得分开存储:

with open(filePath, 'r+b') as file:
    file.seek(startSeek)
    file.write(data)

这样打开文件,利用**seek()**函数,将指针指向你所需要的位置,就会从你指定的位置开始存储数据。

下载情况的实时显示

首先既然要实时显示,要么就像pip下载东西一样,一个动态的进度条,要么就动态的跳数字,这里我想简单一点,打算在屏幕上动态跳几个数字来展示下载到什么地步了就行。
在python里,想要动态显示屏幕信息,我只知道两种方法(直接用python的动画模块除外,不过效果可能也不错的哦!)

1.就是利用大家常见的print!

print("\r 第 %d 次迭代中,第 %d 个样本的 %d 次循环迭代!"%(goNumber,i+1,cishu),end="",flush=True)

一个就是像这样利用 “\r” 以及 end="", flush=True 来告诉print函数你需要清屏。这样就可以动态显示你需要的画面了。

2.利用sys模块的write函数来实现,说实话,一开始看到这个函数的时候我以为我穿越到了java。

sys.stdout.write("\r文件下载进度:%.2f%%(%.2f M/%.2f M) - %.2f KB/s" % (getSize*100/fileSize, self.changeFormat(getSize), self.changeFormat(fileSize), self.changeFormat(speed)*1024))
sys.stdout.flush()

关于这个函数我就不多做解释了,效果与print函数是一样的。但是这个表达方式,乍一看,跟java还真的很像呢!

现在,我们需要显示出来的动态数据,包括下载数据大小,下载进度,以及实时网速。
下载数据大小,我们利用累加写入文件的data的长度来计算。

 for data in re.iter_content():
     file.write(data)
     getSize = getSize + len(data)

利用一个for循环,将数据从response里取出来,然后累加计算每个写入文件的数据段长度,这样就有了已经下载的数据大小。

计算进度则很简单了,先根据请求头,获取文件总大小,然后除一下就有了。

re = requests.head(url)
fileSize = int(re.headers['content-length'])

利用fileSize去除我们的getSize再取百分数,就有了我们的下载进度百分比。

现在计算网速。

        startTime = time.time()
        headers = {'Range': 'bytes=%d-%d'%(self.startSeek,self.endSeek)}
        re = requests.get(self.url, headers=headers, stream=True)
        getSize = self.startSeek
        timeGetSize = 0
        with open(self.filePath, 'r+b') as file:
            file.seek(getSize)
            for data in re.iter_content():
                file.write(data)
                getSize = getSize + len(data)
                timeGetSize = timeGetSize + len(data)
                endTime = time.time()
                if (endTime-startTime)>=1:
                    startTime = endTime
                    speed = timeGetSize
                    timeGetSize = 0

这是完整代码里的一整块。其中利用time模块来计算经过的时间,然后新建一个变量timeGetSize来累加计算在一秒的时间里,我们接收到多少数据。待到endTime-startTime等于一秒后,输出speed

好了,现在基本的问题都解决了,还剩一个,如何统计多线程的网速和下载信息?
没法,我只能想到利用全局变量的办法统计。

global getSizeAll
global speedAll

在程序的一开始,新建两个全局变量,然后在每一个线程里引用他,并且每个线程对应自己的索引:

getSizeAll[self.threadID] = getSize
speedAll[self.threadID] = speed

然后在外面的循环里统计这个全局变量,这样就有了多线程的网速和下载进度了。

断点下载问题

既然是一个完整的下载器,自然得有断点恢复下载的能力,在这里我的做法实时保存现在的下载进度,当现在的下载中断了以后,从保存的数据里恢复下载进度,继续下载。
因此,只要保存这个getSizeAll就行了,即每个线程自己下载到哪儿了,下次恢复的时候只要继续请求从这里开始的数据就行。
存储上,因为要存一个list,要么转二进制存文件,然后二进制解码,要么用json编码成字符,然后再json解码,这里我就用我熟悉的json编码了:

savaMsg = json.dumps(getSizeAll,ensure_ascii=False)
            with open(msgFilePath,'w') as f:
                f.write(savaMsg)

然后解码:

        with open(msgFilePath, 'r') as f:
            getSizeAll = json.loads(f.read())

遇到的坑。。。

第一个就是open函数的读写,因为涉及多线程,所以要分块读写数据,但是我一开始用的 ‘wb’ 模式来读写,最后在下载文件以后频频出现文件损坏无法打开的问题,而且下载的文件很容易莫名其妙少了一块数据,导致损坏,后来折腾了半天才发现这个问题。

r或rt 默认模式,文本模式读
rb 二进制文件

w或wt 文本模式写,打开前文件存储被清空
wb 二进制写,文件存储同样被清空

a 追加模式,只能写在文件末尾
a+ 可读写模式,写只能写在文件末尾

w+ 可读写,与a+的区别是要清空文件内容
r+ 可读写,与a+的区别是可以写到文件任何位置

完整代码

#-------多线程可断点下载器----------------
class HttpDownloadThreading(threading.Thread):
    def __init__(self, url, filePath, startSeek, endSeek, threadID):
        threading.Thread.__init__(self)
        self.url = url
        self.filePath = filePath
        self.startSeek = startSeek
        self.endSeek = endSeek
        self.threadID = threadID
    def run(self):
        global getSizeAll
        global speedAll
        startTime = time.time()
        headers = {'Range': 'bytes=%d-%d'%(self.startSeek,self.endSeek)}
        re = requests.get(self.url, headers=headers, stream=True)
        getSize = self.startSeek
        timeGetSize = 0
        with open(self.filePath, 'r+b') as file:
            file.seek(getSize)
            for data in re.iter_content():
                file.write(data)
                getSize = getSize + len(data)
                timeGetSize = timeGetSize + len(data)
                endTime = time.time()
                if (endTime-startTime)>=1:
                    startTime = endTime
                    speed = timeGetSize
                    timeGetSize = 0
                    getSizeAll[self.threadID] = getSize
                    speedAll[self.threadID] = speed
                    #sys.stdout.write("\r文件下载进度:%.2f%%(%.2f M/%.2f M) - %.2f KB/s" % (getSize*100/fileSize, self.changeFormat(getSize), self.changeFormat(fileSize), self.changeFormat(speed)*1024))
                    #sys.stdout.flush()
        getSizeAll[self.threadID] = self.endSeek
        speedAll[self.threadID] = 0
def changeFormat(x):
        return x/(1024*1024)
def HttpDownloadThreadingFunction(url, filePath, threadNum):
    msgFilePath = filePath + '.data'
    global getSizeAll
    global speedAll
    speedAll = []
    for i in range(0,threadNum):
        speedAll.append(0)
    re = requests.head(url)
    fileSize = int(re.headers['content-length'])
    if os.path.isfile(filePath):
        with open(msgFilePath, 'r') as f:
            getSizeAll = json.loads(f.read())
        sizeForThread = []
        for i in range(0, threadNum):
            sizeForThread.append(i * int(fileSize / threadNum))
        sizeForThread.append(fileSize + 1)
        for i in range(0,threadNum):
            h = HttpDownloadThreading(url,filePath,getSizeAll[i],sizeForThread[i+1], i)
            h.start()
        while 1:
            time.sleep(1)
            getSum = 0
            speedSum = 0
            for i in range(0,threadNum):
                getSum = getSum + getSizeAll[i] - sizeForThread[i]
                speedSum = speedSum + speedAll[i]
            sys.stdout.write("\r文件下载进度:%.2f%%(%.2f M/%.2f M) - %.2f KB/s" % (getSum*100/fileSize, changeFormat(getSum), changeFormat(fileSize), changeFormat(speedSum)*1024))
            sys.stdout.flush()
            if int((getSum+threadNum)/fileSize) >= 1:
                break
            savaMsg = json.dumps(getSizeAll,ensure_ascii=False)
            with open(msgFilePath,'w') as f:
                f.write(savaMsg)
    else:
        f =  open(filePath,'wb')
        f.truncate(fileSize)
        f.close()
        sizeForThread = []
        getSizeAll = []
        for i in range(0,threadNum):
            sizeForThread.append(i*int(fileSize/threadNum))
            getSizeAll.append(i*int(fileSize/threadNum))
        sizeForThread.append(fileSize+1)
        for i in range(0,threadNum):
            h = HttpDownloadThreading(url,filePath,sizeForThread[i],sizeForThread[i+1], i)
            h.start()
        while 1:
            time.sleep(1)
            getSum = 0
            speedSum = 0
            for i in range(0,threadNum):
                getSum = getSum + getSizeAll[i] - sizeForThread[i]
                speedSum = speedSum + speedAll[i]
            sys.stdout.write("\r文件下载进度:%.2f%%(%.2f M/%.2f M) - %.2f KB/s" % (getSum*100/fileSize, changeFormat(getSum), changeFormat(fileSize), changeFormat(speedSum)*1024))
            sys.stdout.flush()
            if int(getSum/fileSize) >= 1:
                break
            savaMsg = json.dumps(getSizeAll,ensure_ascii=False)
            with open(msgFilePath,'w') as f:
                f.write(savaMsg)
    try:
        os.remove(msgFilePath)
    except:
        pass
    print('下载完成')

开始下载试试:

url = 'https://d2.xia12345.com/down/109/2019/04/2HgugAHm.mp4'
HttpDownloadThreadingFunction(url, 'move.mp4', 20)

在这里插入图片描述
效果还是不错的!另外,这个网址是个彩蛋。想看的可以下载看看哦!

结尾

最后,经过我的测试,发现这个下载器性能也一般,特别在下载大文件的时候,经常直接卡主,然后线程好像直接就没了,不知道是不是python全局锁的原因,还在研究中。
另外,既然是下载器,怎么也得能使用迅雷下载地址啊。研究中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值