本文将用面相对象的编程思维,利用多线程批量下载高清美女壁纸:效果如下:
网址可以查看我前天发的Blink。可以看到我们下载了近3600张图片,一共1.2G。用时350秒,平均一秒10张多点,速度约3.54M/S。
我们选了一张照片查看像素值如下图:
这个是高清无疑了,18002700,一般我手机拍摄的都是36002700,按理说大小应该刚好一半,但是不知道是不是压缩的问题,我手机拍的都是3m多,而这个300多K。
import os
import math
import requests
from time import time
from datetime import datetime
from bs4 import BeautifulSoup
from concurrent import futures
executor = futures.ThreadPoolExecutor(max_workers=5)
其中requestshe和BeautifulSoup需要在命令提示行通过pip/pip3 install requests的方式安装,或者利用自己的py编辑器的功能进行安装。其他的库为python自带的标准库,无需下载安装,直接导入即可
注意:这里的三个库安装起来应该很简单,如有疑问请参看这篇文章的第三章开始安装的相关内容。
1、os库提供了很丰富的处理文件和目录的方法,
本程序中主要是在工作空间创建一个文件用以存放下载的图片
2、time库提供各种了与时间相关的函数,本程序中主要是查看程序消耗时间以便更好的优化你的程序,如若不需要优化的话,其实可以不用。
3、datetime库提供各种了与日期相关的函数,本程序中主要是利用他获得当前系统时间,然后结合os在工作空间创建一个以当前时间日期命名的文件夹,这样做的好处想必有些人已经知道了,不明白没关系,后面会详细介绍。
4、math库这个就不用过多介绍了吧,本程序主要是利用他的一个向上取整的函数,具体的后面涉及到它的时候再讲。
5、requests和BeautifulSoup这个也不必多说,这个是爬虫入门的两个库,也是静态网页爬取所必学的。
6、futures这个是多线程的一个库,利用好了爬去效率能成倍提升。
其中最后一行就是利用这个库初始化一个线程池,最大线程为5。
class Image_Download():
def __init__(self,start_url,page_u_want=2):
#所有图集列表页的链接
self.url_list=[(start_url+'index_%s.html'%str(tmp) if tmp else start_url )for tmp in range(26)]
headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'}
self.sesn = requests.Session()
self.sesn.headers.update(headers)
self.page_u_want=page_u_want
self.setup_time = datetime.now().strftime('%Y年%m月%d日%H时%M分%S秒')
self.path1 = './%s' % (self.setup_time)
os.makedirs(self.path1)
其中第二行类的初始化中接收将来类的实例化后传递进来的两个属性 start_url和page_u_want 。分别是初始链接(其实也就是第一列表页的链接)和你想要下载多少页的图片。
self.url_list那一行是运用的列表推导式不过其中的表达式那块用了个三元表达式判断是不是第一页。有的第一页的url其实只用把参数设置为1就行了,但是本网站不行,设置为1会出错,和其他页格式不一样。
这一块有点长 可以拆分下,当然实际你不可能这样拆分,只是为了便于理解
[ x for tmp in range(26)]
x =(start_url+‘index_%s.html’%str(tmp) if tmp else start_url )
这样就好理解了,x是一个三元表达式 当tmp=0时,tmp相当于False,直接返回start_url,否则就是返回start_url+index_5.html这样的链接。
这里列表页第一页网址最后是 /s/18/
第五页网址就换为了/s/18/index_5.html
headers以及后面两行不用多说了,就是为了保存请求状态,让后续的所有请求不用加headers
此处应该就这一个功能,但session的功能还远不止此,比如可以保存登陆状态,不用频繁的进行登陆,然后请求数据,以后如果我们需要登陆后才能爬取的网站就可以利用此功能来实现
最后三行作用依次是
获取系统当前时间
利用当前日期时间模拟一个路径并将之赋值给对象的属性path1
在模拟的路径上创造一个文件夹
这样 每次登陆都会重新创建一个以当前时间日期的为名的文件夹用以存放下载下来的数据,这样一来你测试程序的时候就不用频繁的改文件夹名了。
再讲类的方法1,2之前,我们先分析一下网站结构。
如下图:
列表页的第一页中有40个图集。我们看到,每一个图集都对应于一个li标签,每个图集的详情页都在li标签紧跟着下面的a标签中。这样我们获取就方便多了。代码请看下小节。
#获取某一列表页所有图集连接,过程函数等待多线程调用
def Tmp_get_list_url(self,tmp_url):
res_text=self.sesn.get(tmp_url).text
tmp_list=BeautifulSoup(res_text,'html.parser').select('li>a')
list=[tmp['href'] for tmp in tmp_list][8:]
return list
#调用多线程获取所有图集连接
def get_list_url(self):
fs=[]
print('正在调用多线程获取所有的图集连接,请稍后。。。。。。')
for tmp in self.url_list[:self.page_u_want]:
f=executor.submit(self.Tmp_get_list_url,tmp)
fs.append(f)
futures.wait(fs)
res=[]
for f in fs:res+=f.result()
return res
我们会接触到四个这种结构的代码,共计8个方法。其中四个是过程函数我会用Tmp开头。如下图
我们首先定义一个函数,获取某一页的相关信息,参数为某一页的地址。然后在调用多线程利用循环将所有页面参数传入。
好了啰嗦了这么多。开始代码的讲解吧。
方法1
#获取某一列表页所有图集连接,过程函数等待多线程调用
def Tmp_get_list_url(self,tmp_url):
res_text=self.sesn.get(tmp_url).text
tmp_list=BeautifulSoup(res_text,'html.parser').select('li>a')
list=[tmp['href'] for tmp in tmp_list][8:]
return list
这个应该很简单吧,利用session中的get方法发起请求,并转化为文本传入BeautifulSoup,接着利用select选择所有li标签下的a标签,然后利用列表推导式生成该页面的所有图集链接。
唯一需要注意的是li标签下的a标签在本网站中前面有8个不是我们目标图集的,需要去除,直接利用切片 [8:] 去除即可
方法2
#调用多线程获取所有图集连接
def get_list_url(self):
fs=[]
print('正在调用多线程获取所有的图集连接,请稍后。。。。。。')
for tmp in self.url_list[:self.page_u_want]:
# 提交任务到线程池
f=executor.submit(self.Tmp_get_list_url,tmp)
fs.append(f)
# 等待这些任务全部完成
futures.wait(fs)
res=[]
for f in fs:res+=f.result()
return res
首先我们定义一个空列表,存放每一个任务的执行状态。
然后利用循环把上面类的初始化时候得到的所有图集列表页链接传入。 然后提交任务到线程池,并将这个任务的状态添加到列表。
等待这个任务全部完成之后,我们将得到一个包含所有结果的列表,当然这个结果不能直接用,还需要用result进行提取。
这里需要注意每一个任务得到的result都是一个数组,如果这最后三行直接用列表推导式的话讲得到一个嵌套的列表,还需要进行处理,到时候可能就又要多几行代码,这里就直接利用循环相加的直接将每一个列表的元素添加到定义的空列表中
还有一点需要注意的是本程序得到就是从第一页开始到你想要的页面的所有页。如果你想得到一个区间的话可以把for tmp in self.url_list[:self.page_u_want]:中最后的切片改为[self.start_page:self.end_page]并在对象的初始化时候把这两个属性加上即可。
这个方法结束后,我们将得到一个包含所有图集链接的列表。
2.3、方法3和4获取所有图集分页链接 2.3.1、 对应网站结构网址还是 以 a/32757/2.html这种结构结尾。一个url后面跟一个2.html表示页数。那么问题来了,有的图集有99张,有的图集却只有70来张,我们怎么知道他有多少页呢。
进一步分析发现每个图集的分页有5张图片(最后一页有的不足5张),而左上角有这个图集所含图片的数量,那么就好办了,直接数量除以5,然后向上取整即可,别问我为什么向上取整,自己想嘿嘿。
方法3
#获取某一图集的所有分页链接,过程函数等待线程调用
def Tmp_get_page_url(self,url):
tt=self.sesn.get(url).text
soup=BeautifulSoup(tt,'html.parser')
#计算图集中图片数量
count=soup.select('div.tuji p')[1].text[-4:-1].strip()
#获取图集分页链接
page_list=[(url+'%s.html'%str(tmp+1) if tmp else url ) for tmp in range(math.ceil(int(count)/5))]
return page_list
这里没有什么好讲的,上面讲网站结构都说过。
计算图片数量时候怕图片数量可能是三位数 就直接取了文本的倒数2,3.4个字符组成的字符,两位数的话,他前面就有一个空格,直接调用strip保持默认参数去除空格即可。
获取图集分页链接时候,和上面方法1一样 通过列表推导式中嵌套三元表达式。有两个不同的地方就是:
1、方法1是index_2.html 而这个直接就是2.html.
2、循环次数不固定,我们调用了math.ceil将图片数量除以5进行了向上取整。
方法4
#调用多线程获取所有分页链接
def get_page_url(self):
urls_list=self.get_list_url()
print('正在调用多线程获取所有图集的所有分页链接,请稍后。。。。。。')
fs=[]
for url in urls_list:
f=executor.submit(self.Tmp_get_page_url,url)
fs.append(f)
futures.wait(fs)
res = []
for f2 in fs: res += f2.result()
return res
这个绝大部分和前面方法2一样,不过,在第二行我们调用了方法2 获取到了所有图集的链接,然后再通过循环传入方法3。
这样一来如果你想调用方法4 可以直接对象实例化之后调用方法4即可,如果没有这一行的话,你还要先调用方法2得到一个列表,在传入方法4
最终,我们得到了所有图集的所有分页链接
2.4、方法5和6获取所有图片下载链接 2.4.1、 对应网站结构分析网站结构,我们可以得出每个图片的下载链接储存在图集的分页链接的div.content标签下的img标签中的src了,那么他的获取应该就很简单了。
2.4.2、相应代码讲解方法5
#获取某一页的所有图片下载链接,过程函数等待线程调用
def Tmp_get_download_url(self,url):
soup=BeautifulSoup(self.sesn.get(url).text,'html.parser').select('div.content img')
picture_list=[tmp1['src'] for tmp1 in soup]
return picture_list
结合前面网站结构那块,这个代码应该很好理解。这里不再多说。主要就是为了减少代码行数,嵌套的函数略多,可以稍微拆分一下。另外大家可以试试,是一行长代码效率高,还是拆分成几行利用几个过度变量效率高。
方法6
#调用多线程获取所有图片下载链接
def get_download_url(self):
urls_list1 = self.get_page_url()
print('正在调用多线程获取所有图片下载链接,请稍后。。。。。。')
fs = []
for url in urls_list1:
f = executor.submit(self.Tmp_get_download_url, url)
fs.append(f)
futures.wait(fs)
res = []
for f in fs: res += f.result()
return res
同样这个方法6和方法4如出一辙,只不过这里的第二行同样调用了方法4得到了所有图集的所有分页链接。
最终,我们得到了所有图片的下载链接
方法7
#根据图片,保存某一图片。过程函数等待线程调用
def Tmp_download_image(self,url):
res = self.sesn.get(url)
with open(self.path1 +'/'+('_'.join(url.split('/')[-2:])), 'wb') as file:
# 将数据的二进制形式写入文件中
file.write(res.content)
到了这里就不需要解析网址了,直接读取图片的下载链接并转化为二进制形式写入即可。
当然,写入文件,我们要传入文件名,为了加以区分,我们直接从下载链接中截取。比如某个链接是这样的src=“https://***********/a/1/32757/6.jpg”,我们可以直接截取32757/6.jpg .但是要注意 / 不能用做文件名,并且有的不止6张图片,有的也许也有三位数甚至更多。所以我们直接以 / 来分割字符串,然后取后面两部分用下划线连接即可。
方法8
def download_image(self):
urls_list = self.get_download_url()
print('正在调用多线程,下载全部图片')
fs = []
for url in urls_list:
f = executor.submit(self.Tmp_download_image, url)
fs.append(f)
futures.wait(fs)
这个也基本和方法4 6 相同,不过这里因为方法7没有返回值,直接存储的图片,所以我们不用取得结果。当然这里也可以返回fs的数量代表已下载图片的数量。
2.6、对象的实例化及其他t0=time()
start_url='我前天发的Blink中有'
page_u_want=1 # 注意这里的页数不要弄得太大,我下载一页就40个图集3500多个图片 1.2个G的内存
image=Image_Download(start_url,page_u_want)
res=image.download_image()
t1=time()
count=len(os.listdir(image.path1))
print('共下载了%s张图片,耗时%0.2f秒,平均每张耗时%0.2f秒'%(count,t1-t0,(t1-t0)/count))
前面t0到t1就是对象的实例化,其中start_url可以换成其他的,不过,如果换成其他的,当然要注意对象初始化第一行的这个代码中的26要换成相应的总页数。
self.url_list=[(start_url+'index_%s.html'%str(tmp) if tmp else start_url )for tmp in range(26)]
count那个是统计你下载了这么久下载了多少东西用的,我也是直接抄了,就不发表意见了。
至此你的代码就全部完成了。让我们运行起来。
import os
import math
import requests
from time import time
from datetime import datetime
from bs4 import BeautifulSoup
from concurrent import futures
#初始化一个线程池,最大线程为5
executor = futures.ThreadPoolExecutor(max_workers=2)
class Image_Download():
def __init__(self,start_url,page_u_want=1):
#所有图集列表页的链接
self.url_list=[(start_url+'index_%s.html'%str(tmp) if tmp else start_url )for tmp in range(26)]
headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'}
self.sesn = requests.Session()
self.sesn.headers.update(headers)
self.page_u_want=page_u_want
self.setup_time = datetime.now().strftime('%Y年%m月%d日%H时%M分%S秒')
self.path1 = './%s' % (self.setup_time)
os.makedirs(self.path1)
#获取某一列表页所有图集连接,过程函数等待多线程调用
def Tmp_get_list_url(self,tmp_url):
res_text=self.sesn.get(tmp_url).text
tmp_list=BeautifulSoup(res_text,'html.parser').select('li>a')
list=[tmp['href'] for tmp in tmp_list][8:]
return list
#调用多线程获取所有图集连接
def get_list_url(self):
fs=[]
print('正在调用多线程获取所有的图集连接,请稍后。。。。。。')
for tmp in self.url_list[:self.page_u_want]:
f=executor.submit(self.Tmp_get_list_url,tmp)
fs.append(f)
futures.wait(fs)
res=[]
for f in fs:res+=f.result()
return res
#获取某一图集的所有分页链接,过程函数等待线程调用
def Tmp_get_page_url(self,url):
tt=self.sesn.get(url).text
soup=BeautifulSoup(tt,'html.parser')
#计算图集中图片数量
count=soup.select('div.tuji p')[1].text[-4:-1].strip()
#获取图集分页链接
page_list=[(url+'%s.html'%str(tmp+1) if tmp else url ) for tmp in range(math.ceil(int(count)/5))]
return page_list
#调用多线程获取所有分页链接
def get_page_url(self):
urls_list=self.get_list_url()
print('正在调用多线程获取所有图集的所有分页链接,请稍后。。。。。。')
fs=[]
for url in urls_list:
f=executor.submit(self.Tmp_get_page_url,url)
fs.append(f)
futures.wait(fs)
res = []
for f2 in fs: res += f2.result()
return res
#获取某一页的所有图片下载链接,过程函数等待线程调用
def Tmp_get_download_url(self,url):
soup=BeautifulSoup(self.sesn.get(url).text,'html.parser').select('div.content img')
picture_list=[tmp1['src'] for tmp1 in soup]
return picture_list
#调用多线程获取所有图片下载链接
def get_download_url(self):
urls_list1 = self.get_page_url()
print('正在调用多线程获取所有图片下载链接,请稍后。。。。。。')
fs = []
for url in urls_list1:
f = executor.submit(self.Tmp_get_download_url, url)
fs.append(f)
futures.wait(fs)
res = []
for f in fs: res += f.result()
return res
#根据图片,保存某一图片。过程函数等待线程调用
def Tmp_download_image(self,url):
res = self.sesn.get(url)
with open(self.path1 +'/'+('_'.join(url.split('/')[-2:])), 'wb') as file:
# 将数据的二进制形式写入文件中
file.write(res.content)
#调用多线程,保存全部图片
def download_image(self):
urls_list = self.get_download_url()
print('正在调用多线程,下载全部图片')
fs = []
for url in urls_list:
f = executor.submit(self.Tmp_download_image, url)
fs.append(f)
futures.wait(fs)
t0=time()
start_url='我前天发的Blink中有'
page_u_want=1 # 注意这里的页数不要弄得太大,我下载一页就40个图集3500多个图片 1.2个G的内存
image=Image_Download(start_url,page_u_want)
res=image.download_image()
t1=time()
count=len(os.listdir(image.path1))
print('共下载了%s张图片,耗时%0.2f秒,平均每张耗时%0.2f秒'%(count,t1-t0,(t1-t0)/count))
第四章、结语
这里需要注意几点;
1、start_url在我前天发的Blink中有,知道这一个链接了那其他的你也就知道了。
1、正如最后我备注的一样,下载一页就需要350秒,3600张,1.2个G的储存空间,你要想下载多页,那么你还是先计算好再说。比如,你要下载全部的26页估计要两个半小时甚至更多。那你的先准备好30G左右的空间、一部电影、一张床、一包洽洽香瓜子。。。。
2、你可以稍作改动,对象实例化时候可以传入初始页数,最终页数,这样你就可以下载某1页或者某几页的内容了。当然,与此同时方法2中的那个for询循环后面的切片操作就要同时变喽。
3、方法5和方法7可以合并,这样方法6和方法8同样只要一个就行了,你可以尝试下合并后他的效率是变高了还是变低了,我没有试过,还不清楚。
4、关于此文章有任何问题欢迎私信评论,谢谢。
Title:the 6th blog
By:P&p
Time:2020-03-19 09:54(学python的第48天)