准备
最近神经网络研究遇到了一点瓶颈,于是想着琢磨点其他的东西缓冲一下,正好那天遇到了一些关于下载的问题,我就想着这些网络上的下载器,不是垃圾就是广告太多,还慢,既然这样,那就自己写一个呗!
基本思路就是利用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全局锁的原因,还在研究中。
另外,既然是下载器,怎么也得能使用迅雷下载地址啊。研究中。