利用requests与Threading编写python多线程HTTP下载器
发布日期:2021-06-28 22:04:52 浏览次数:2 分类:技术文章

本文共 7777 字,大约阅读时间需要 25 分钟。

准备

最近神经网络研究遇到了一点瓶颈,于是想着琢磨点其他的东西缓冲一下,正好那天遇到了一些关于下载的问题,我就想着这些网络上的下载器,不是垃圾就是广告太多,还慢,既然这样,那就自己写一个呗!

基本思路就是利用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 getSizeAllglobal speedAll

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

getSizeAll[self.threadID] = getSizespeedAll[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] = 0def 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全局锁的原因,还在研究中。

另外,既然是下载器,怎么也得能使用迅雷下载地址啊。研究中。

转载地址:https://blog.csdn.net/Yezeqi0328/article/details/89710215 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:HC_SR04超声波模块的应用
下一篇:关于bp神经网络的原理与自建研究以及计算结果。

发表评论

最新留言

留言是一种美德,欢迎回访!
[***.207.175.100]2024年04月22日 12时23分11秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章