这是本人的第二篇文章 在第一篇文章的基础上进行了内容的优化
由于发现大量视频和图片下载难免遇见下载速度慢和时间久不能一直开着程序下载的问题
于是乎增加两个功能让我们更加无忧无语的爬取想要的资源
虽然urllib.request的urlretrieve方法可以下载但是我们需要的就是将他重写加快下载速度
urllib.request.urlretrieve(donwload_url, filename="ted.mp4", reporthook=Schedule)
多线程下载的思路
通过requests模块的head(url)方法可以获得url路径的响应头而且不会对其进行下载
返回的响应头中间含有具体的数据大小 这样我们就可以对这个数据进行分割 然后分配线程进行下载
断点重下的思路
思路1.在下载的时候我们可以通过在对每次下载完之后将url存入数据库 这样就可以在第二次重新下载的时候遍历数据库的url 下载还未下载完毕的url
思路2.python有个模块叫做pickle 作用是将当前的对象持久化dump写入文件中 当我们第二次启动爬虫的时候只要先去寻找这个文件然后load这个文件将这个对象提取出来就可以继续我们的工作了
思路3.在每次爬到一定程度的时候给写出一份日志记录我们的爬取情况 同理只需要在爬取之前查看日志便可以知道从那个地方开始爬取
另外如果是用scrapy框架的话他是有自带的持久化的命令可以通过命令持久化一个爬虫本文只是通过urllib的方式在阐述 让大家对底层的东西更加了解
多线程写入遇见的坑
由于多个线程在不加锁的状态下对一个文件进行写入就会导致一个现象就是第二个线程的指针会乱跑 并不会让你想象的那样安静的做个劳模 导致如果按照一般的写入那就不能很如意的完成我们的要求
并且在创建文件时要先创建一个空的文件然后在对它读取 这样在我们进行写入的时候用seek指针就不会报指针为空异常
多线程写入的解决方法
以下的解决方法乃个人见解,如有错误请望指出
python提供的os模块即对系统的操作模块可以让我们对写入的文件进行镜像复制 然后对各个镜像进行写入 然后系统会自动将写完的内容拼接成完整的内容 这样也可以避免我们出现指针乱跑的问题
并没有深入去研究os模块~~
那么开始愉快的撸代码吧
1.获取内容的大小
以下我以ted的某个视频作演示
ted="https://pc.tedcdn.com/talk/podcast/2017/None/TimKruger_2017-480p.mp4"
我们先对这个url获取响应头
import requests
response = requests.head(ted)
print(response.headers)
打印结果我们发现他的长度在'Content-Length'中
所以通过提取这个值就是我们所要看视频的长度了
filesize = int(requests.head(ted).headers['Content-Length'])
2.设定线程数将内容分隔
假设我们定义了3个线程然后定义一个数组让每一截放入数组中
mun = 3
threading.BoundedSemaphore(mun)#设定的线程的数量最多为3个
lengs = []
for _ in range(0, mun):
lengs.append(_ * ((filesize) // mun))#//整除返回整数 如果是/返回的是小数
lengs.append(leng-1) #由于最后的长度是不一样的所以另外加入 由于是0开始的所以长度要减1
print(lengs)
通过打印我们得到数组
[0, 21521363, 43042726, 64564090]
这个就是每一段的开始和结束的位置啦
3.启动python的多线程模式
由于python的多线程和java一样需要继承然后重写run方法从start中启动那么我们就在run方法中写入我们需要的爬虫和下载内容
class mutiThread(threading.Thread):
def __init__(self,url,f,startpos,end):
super(mutiThread,self).__init__()
self.url=url
self.f=f
self.startpos=startpos
self.end=end
初始化线程由于线程有自带的start属性 这里如果我传入start会报错 所以我将名字改为startpos,f就是我们想要的写入的file文件了
重写run方法
开始传入位置时需要给start的位置+1防止和end位置重复下载
数组为[0, 21521363, 43042726, 64564090]
即下载的节点为(0, 21521363)( 21521364,43042726)(43042727, 64564090)
def run(self):
print("start thread:%s "% (self.getName()))
if self.startpos==1: #因为开始的时候start为1 所以要将他变为0
self.startpos=0
print(str(self.startpos) + " xxxxx " + str(self.end))
header={"Range":"bytes=%s-%s"%(self.startpos,self.end)}
response = requests.get(self.url,headers=header)
self.f.seek(self.startpos)#这个是指针 但记住不是file的 是os.fdopen的
self.f.write(response.content)
这样就写完了
4.创建写入文件和文件镜像
tempf = open("ted.mp4", 'w')#创建临时文件 如果存在w模式会重新覆盖清零 这样在seek的时候不会报空指针
tempf.close()
with open("ted.mp4",'rb+') as f: #通过二进制读取的方式打开文件
for _ in range(0,mun):
fd=f.fileno() #获得文件描述fd
dup_fd=os.dup(fd) #dup是复制的意思 获得描述文件的复制的对象
fd_=os.fdopen(dup_fd,"rb+",-1)#通过复制的对象进行读写 这里-1表示的是默认使用系统缓冲
threads=mutiThread(url,fd_,lengs[_]+1,lengs[_+1]) #将数据传入线程中进行读写 这里开始的位置要+1
threads.start()
好了 这样我们就完成了多线程对视频的下载 经过我的测试 速度比原来的快了大概2-3倍
多线的问题搞定了开始我们的断点续传问题了
这里我使用的士最简单的pickle模块,其他小伙伴可以试下别的方法或者进行多方法备份防止数据丢失
启动爬虫前:
我们可以在爬虫开始的时候先去路径下面去寻找持久化的对象,如果为空则就重新开始.否则就是将已经序列化的对象反序列化 由于持久化是讲对象转化成二进制写入文件所以我们就要执行以下代码 假设我们的序列化文件叫enduration.pkl
import pickle
f=open("enduration.pkl","rb+")
crawl=pickle.load(f)
print(crawl)#通过打印我们可以看见序列化的内容
我们需要持久化的属性是什么呢
我们的爬虫是根据我们的提供的url进行工作的 所以在我们第一次进行进行选择url地址之后
只需要在停下爬虫的时候将剩下未爬取的url序列化即可
我这里序列化了剩下的装有页码的url的和对应视频个数的数组即可
将数据交给爬虫
crawl是一个数组对象所以只要依次遍历这个对象即可 将"_"赋值给page即可
cont=""
for _ in crawl:
if _=1:
cont=_ #crawl第一个装的是url地址 后面就是要下载的page页面了
page=_
再将数据传入下载爬虫即可
ted.download(conts,page)
这个方法是我之前写的下载 只需要传入page和cont就可以继续工作了
如何持久化
我们可以在爬虫爬取的时候进行异常捕捉
try:
do something
excpetion:
pick=[]
pick.append(cont)
pick.append(pages)
f=open("enduration.pkl","wr+")
pikle.dump(pick,f,Ture)
f.close()
这样我们就可以在手动停掉程序的时候自动将序列化对象写入文件中了
其实我们可以将其看成一个同日志文件一样 在硬盘上存下需要的变量即可 其实也可以在启动时用os模块遍历文件下的视频,如果不同没用重名就可以开始下载了
同样的我将我的新爬虫git到了我的github了 需要的小伙伴欢迎下载