Python爬取图片,你也可以做到!


(注:本文章约11000字,约350行)

0.前言

本人是某大学大二计算机专业学生,寒假在家无聊学习编程,前几天突发奇想用Python爬取动漫图片。恰好找到了娟娟壁纸网,于是花费2-3天查找各种资料并进行实践,完成动漫图片的爬取。趁还没开学总结一下爬虫方法与大家交流分享,也欢迎大家来提问和纠正。
(娟娟壁纸网:http://www.jj20.com)
(我实际上使用的网站是:http://m.jj20.com,这是手机版的网站,但是这个网站与前者内容相同,结构相似,因此下面讲解以这个网站为准)

1.你需要的工具

  1. python 3.x(毕竟是python爬取图片)
  2. 相关模块(见下方,部分模块需要自行安装,这些模块在cmd中使用pip命令安装即可,并且网上也有很多教程,不作详细介绍)
  3. 充足的存储空间(我爬下来的图片一共7个多G)
    附python模块:(会在后文介绍这些模块的作用)
#可能需要安装
import requests					#获取网页代码时用
import html5lib					#配合bs4模块使用,是解析html代码的工具
from bs4 import BeautifulSoup 	#bs4模块,负责解析html代码,
#自带
import os
import time
import threading

2.你可能需要了解的知识

  1. Python基础语法运用(包括列表的处理,字符串的处理,文件读取写入,类与对象等)
  2. html5语法基本认知(至少你需要知道从哪找图片)
  3. 多线程方法(其实我也不能说是完全了解,毕竟我没深入研究过,但至少要有这个概念。用多线程是为了提高下载效率。)
    这些知识在网上都很好找到,这里不做过多介绍

3.爬虫流程介绍

爬虫说白了就是用代码从网上下载图片,因此我们需要知道图片的地址,之后确定下载的目录,最后进行下载。总结起来就是:

  1. 获取图片地址(你需要通过网页源代码,在python中解析)
  2. 确定下载的目录
  3. 进行下载
  4. 其他工作及反爬虫措施

其中最重要的,也是最容易翻车的就是第一步。因为有时图片地址并不容易得到(我之前还想从其他网站上爬取漫画到本地看,可惜无法从网页中解析目标图片地址,而目标图片地址又是无规律的,无法拼接,因此放弃)

以下介绍爬虫流程的思路

3.1 解析图片地址

3.1.1 怎么看图片的地址

首先,你要知道怎么图片地址在哪看。
登陆浏览器,按下F12,可以看到网站源代码(html5代码)
在这里插入图片描述
我们把光标放到代码上,可以看到这部分代码对应左边哪块内容。通常,我们需要打开div标签或其他标签进行进一步寻找,可打开的标签在左边有三角符号。当你不了解网站的具体结构时,你可以把每个标签都打看看看,通常图片地址一般都会在< img >标签下,也有可能在其他标签中,但有一个特征是会有".jpg"等图片格式名。
例如在这个网站上,我们找到了某一张图片的地址。在浏览器中打开这个地址,你会看到一张图片。
在这里插入图片描述

3.1.2 怎么找寻所有图片的地址

如果你只想下载一张图片,那么你已经找到其地址了,但是显然我们需要不止一张图片,因此工作还要继续。
我们点开某一栏查看,发现一个网页只显示一个大图,按照上述方法是可以找到这张图片地址,可我们需要的是这一栏的所有图片,这该怎么办?
在这里插入图片描述

如果你是浏览者,你肯定会找诸如“下一张”的按键。现在程序要看“下一张”,需要知道下一张图片所在的网页的地址。而“下一张”的按键实际上就包含一个其他网页的地址,一般位于html代码的< a >标签的"href"属性中。
在这里插入图片描述
当然如果想从连接中进入下一页,你还需要拼接一下才可以,因为这只是文件名并未完整的url。在前面加上”http://m.jj20.com/bz/ktmh/dmrw/“就可以,这并不难
其实你还可以通过网址名的规律推导出下一页的url,自己进行拼接,但你需要知道图片总数量,还是离不开html5代码。或者从选择栏中找寻url(其实可能反而这样更方便,因为可以一次找到所有的图片所在网页)。篇幅原因,这些方法不做过多介绍。

3.1.3小结和说明

总结起来就是:你需要找到所有图片的地址。对于这个网站而言,是在第一页中找寻图片地址和其他网页的地址(可以每次找下一张图片所在网页url直到最后一张,也可以一次获取全部),然后进入其他网页找图片。
当然,有时也可以通过拼接的方式进行。例如在爬取的过程中,我发现“查看原图”中的链接是一个更清晰的图片,但是在写代码的时候,却无法提取到这个链接(具体说来是某个span标签的内容没有被获取到,至今也没有搞明白是怎么回事),但我发现这个高清图片与不清楚图片的地址只差了一些地方,并且是有规律的,因此我用拼接url的方法,获取了高清图片。所以我们要善于运用多种方法,才能最快的解决问题

3.2 确定下载目录

这个其实很简单,指定一个文件夹就好。当然,在代码中时,为了保证正确性,最好还是判断一下文件夹是否存在,若不存在则创建
我爬虫的时候为了避免图片过于冗杂,把图片分别存放在35个文件夹中,因此需要判断文件夹是否存在和建立文件夹。(按右键手动建立35个文件夹,太累了吧)。

3.3 下载图片

现在我们已经找到目标图片的地址和下载目录了,我们需要进行下载。
我采取的方式是以二进制文件的方式写入。先获取网页图片的二进制编码,再写入到本地文件中。
还可以用urllib.request.urlretrieve进行下载,具体方法不做过多介绍,可以网上查询。

3.4 其他工作及反爬虫措施

  1. 由于图片很多,因此调用多线程的方法下载图片。当然为了提高效率,图片地址的获取也采用多线程。
  2. 即便如此,依然不可能一下就下载完图片,想看到下载的进度,需要一个进度条。
  3. 有可能图片并不是一下子都下完的,可能需要在完成爬虫之前关闭程序。这时候需要保存下载进度,下次下载时,不再重头开始下载。(实际上仍然会重新下载部分图片因为很可能有些图片没有下完)。
  4. 以非盈利目的爬取图片是合法的(盈利的不知道),但总会有非法的爬虫行为,因此很多网站都会有反爬虫措施。我所知道的反爬虫措施包括添加请求头,进行等待来避免网站认为频繁访问以及用try-expect机制规避某次访问失败导致的程序终止。
  5. 关于请求头的作用,我大致理解是让网站认为不是“程序”在访问而是“真人”在访问,因此不触发反爬虫机制(谁会拒绝浏览量呢)。由于在下载图片的过程中,我们是直接访问了图片的地址,而并没有从其他路径经过,网站可能会认为这是在爬虫,加入请求头可能会规避掉这一点。
    获取请求头的方法是:按F12,点击网络一栏,在左侧找一栏,看“标头”,滑轮滑到最下方就可以看到相关信息。在程序中,请求头是以字典的方式作为参数传入的
    在这里插入图片描述

4.代码展示及分析

上面我们介绍完了爬虫的流程,接下来是代码实现。

本程序使用的是requests模块实现网页的html源代码获取,得到的字符串,并用bs4模块进行解析(解析可以理解为从html源代码中提取相关标签即内容),使用的工具为html5lib。这三个模块可能需要安装。

为了尽量实现程序的高内聚低耦合,也是为了调试和修改方便,以下代码将不同功能写在了不同的函数或者是类当中,由main函数调用。

以下是程序的大体结构
在这里插入图片描述

4.0 全局变量声明

#全局变量,前四个用于多线程中
SubPageList=[]	#用于存放各个图集的首页的列表
PicList=[]		#图片列表,用于存放各个图片的信息
GetCount=0		#读取图片计数,用于进度条
DloadCount=0	#下载图片计数,用于进度条

header={
	'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36 Edg/88.0.705.74',
	'Referer':'http://m.jj20.com/'
}#请求头

4.1 CrawlerTool类——爬取工具

class CrawlerTool():
	def __init__(self,BaseUrl,IndexPage,LocalFolder,FolderNum):
		#传入参数
		self.BaseUrl=BaseUrl									#第一个导航页的url
		self.IndexPage=BaseUrl+"/"+IndexPage					#各导航页的路径,可合成各个导航页
		self.SubPath=os.path.join(LocalFolder,str(FolderNum))	#下载目录
		self.FolderNum=FolderNum								#文件夹目录,也负责构成各图片名
		
		#默认参数
		self.AnalysisTool='html5lib'	#解析工具
		self.GetThreadNum=5				#获取地址的线程数
		self.DloadThreadNum=10			#下载图片的线程数
	#该函数负责在每个导航页中找寻图集的首页
	def FindSubPage(self,bs):
		global SubPageList
		SubPageList=[]		#重置
		target=bs.find('div',class_="main")			#先找class名为'main'的div标签,因为只出现了一个所以用find
		target=target.find('ul',class_="ul_half")	#再找target下class名为'ul_half'的ul标签
		target=target.find_all('li')				#最后找target下所有的'li'标签
		for each in target:
			#对每个li标签,取其下面的a标签的属性,再取属性中的href的内容,便得到所求,然后扔进列表中
			SubPageList.append(each.a.attrs['href'])
	
	#该函数用多线程获取图片地址
	def CrawlPage(self):
		global GetCount
		global SubPageNum
		GetCount=0	#重置
		SubPageNum=len(SubPageList)#获取该导航页下图集数
		ThreadList=[]#存放线程对象的列表
		PrintBar(0,total=SubPageNum,scale=10)#初始化进度条
		#生成并启动线程
		for i in range(self.GetThreadNum):
			thread_=GetThread(self.SubPath,self.BaseUrl)
			ThreadList.append(thread_)
			thread_.start()
		#挂起每个线程,所有线程结束后再进行后续内容
		for eachthread in ThreadList:
			eachthread.join()
	
	#该函数负责生成存放图片地址信息的列表
	def InitPicList(self):
		global PicList
		global header
		PicList=[]#重置
		IndexReq=None
		#获取导航页,利用try-excpet机制防止访问失败而终止
		while(True):
			try:
				IndexReq=requests.get(url=self.IndexPage,headers=header)#申请访问
			except:
				print("\n访问失败,尝试重新访问\n")
				continue#访问不成功会一直尝试
			break
		#准备解析工具
		IndexReq.encoding="ansi"#防止中文乱码
		Indexhtml=IndexReq.text#获取源代码字符串
		IndexReq.close()#关闭访问
		bs=BeautifulSoup(Indexhtml,self.AnalysisTool)#获取bs对象用于解析
		#获取各个分页url的列表
		self.FindSubPage(bs)
		#合成文件夹
		if not os.path.exists(self.SubPath):
			os.mkdir(self.SubPath)
		#多线程调取各个分页的图片地址
		print("开始获取图片地址")
		self.CrawlPage()
		print("\n获取图片地址完毕")
	
	#该函数负责多线程下载图片,结构类似于多线程获取图片地址,不再介绍
	def DloadPic(self):
		global PicList
		global DLoadCount
		global PicNum
		PicNum=len(PicList)
		ThreadList=[]
		PrintBar(0,total=PicNum,scale=10)
		for i in range(self.DloadThreadNum):
			_thread=DloadThread()
			ThreadList.append(_thread)
			_thread.start()
		
		for _thread in ThreadList:
			_thread.join()

4.2读取线程类

#读取线程类,负责获得图片地址,在爬取类中会实例化之
class GetThread(threading.Thread):
	def __init__(self,Path,BaseUrl):
		threading.Thread.__init__(self)	#继承原有的threading.Thread类
		self.gLock=threading.Lock()		#定义锁,防止多个线程同时改变量导致错误
		self.Path=Path					#下载目录
		self.AnalysisTool='html5lib'	#解析工具
		self.BaseUrl=BaseUrl			#首个导航页Url,用于后续拼接url
	#本函数负责取某一图集下的所有图片,BaseName为图集代号,SubUrl为图集的url
	def GetSubList(self,SubUrl,BaseName):
		global header
		count=1	#从1开始
		SubList=[]#初始化,存放某个图集的图片列表
		while(True):
			req=None
			#获取访问
			while(True):
				try:
					req=requests.get(url=SubUrl,headers=header)
				except:
					print("\n访问失败,尝试重新访问\n")
					continue
				break
			#准备工具
			req.encoding="ansi"
			html=req.text
			req.close()
			bs=BeautifulSoup(html,'html5lib')
			tempList=[]
			#找原图
			target=bs.find('div',class_="main")
			target_1=target.find('div',class_="info_pic").find('img').attrs['src']
			target_1="http://pic"+target_1[10:-9]+'.jpg'#拼接法换高清图片,因高清图片网址未能成功解析
			name=BaseName+'-'+str(count)+'.jpg'#拼接本地目录下图片名
			tempList.append(target_1)
			tempList.append(os.path.join(self.Path,name))#拼接本地图片下载目录
			SubList.append(tempList)#图片url和本地名称扔进小列表,小列表放入大列表中
			#找下一页
			result="NULL"
			target_2=target.find('div',class_="page").find_all("div")
			for each_2 in target_2:
				#找标签内容为"下一张"的a,相当于找名称为"下一张"的按钮
				if each_2.a is not None:
					if each_2.a.string=="下一张":
						#拼接下一张的url,因为取出来的href内容只是html文件名
						result=self.BaseUrl+'/'+each_2.a.attrs['href']
						break
			if result=="NULL":
				break#相当于到了最后一页
			else:
				SubUrl=result#准备访问下一页
			count+=1#计数加1
		return SubList
	
	#执行函数
	def run(self):
		global GetCount
		global SubPageList
		global PicList
		global GetCount
		#以下循环必须保证图集地址列表不为空,否则会报错
		while(len(SubPageList)>0):
			self.gLock.acquire()#上锁防止冲突
			if(len(SubPageList)==0):#取完了,开锁退出
				self.gLock.release()
				continue
			else:
				SubUrl=SubPageList.pop()#从列表中取最后一个
				#取图集代号,从最后一个'/'取到倒数第6个
				TName=""
				i=-6
				while(SubUrl[i]!='/'):
					TName=SubUrl[i]+TName
					i-=1
				#取完了
				SubList=self.GetSubList(SubUrl,BaseName=TName)#取这个图集下的列表
				self.gLock.release()#放锁
				PicList=PicList+SubList#把个图集的信息加到总列表
				self.gLock.acquire()#上锁
				GetCount+=1#获取地址的计数加1
				PrintBar(GetCount,SubPageNum)#进度条更新
				self.gLock.release()#开锁

4.3 下载线程类

#下载线程,负责下载图片,在爬取类中会实例化之
class DloadThread(threading.Thread):
	def __init__(self):
		threading.Thread.__init__(self)	#继承
		self.gLock=threading.Lock()		#定义锁
	#该函数负责下载图片,与GetThread函数的run()函数结构类似
	def run(self):
		global PicList
		global DloadCount
		DloadCount=0
		while(len(PicList)>0):
			self.gLock.acquire()
			if len(PicList)==0:
				self.gLock.release()
				continue
			else:
				PicInfo=PicList.pop()
				self.gLock.release()
				#图片信息列表每个元素为长度为2小列表,第一个元素为图片的url,第二个为下载到本地的目录
				src=PicInfo[0]
				dst=PicInfo[1]
				DownLoadPic(src,dst)#下载图片
				time.sleep(1)#休息,尽量规避后续访问失败
				self.gLock.acquire()
				DloadCount+=1
				#更新进度条,scale为比例,因为图片通常有上百张,需要调整一下进度条输出频率
				PrintBar(finished=DloadCount,total=PicNum,scale=10)
				self.gLock.release()

4.4 辅助函数

#辅助函数,用于生成进度条
def PrintBar(finished,total=0,scale=1):
	StarNum=int(finished/scale)#用'*'表示进度,这里是进度数量
	process="{0}/{1}:{2}".format(finished,total,'*'*StarNum)#输出内容
	print("\r", end="")#回到该行开头,不换行
	print(process,end="",flush=True)#输出,不换行,flush=True保证能覆盖
	print("\r", end="")#回到改行开头,不换行

#辅助函数,负责单张图片的下载
def DownLoadPic(src,dst):
	global header
	#图片为二进制编码文件,因此参数为'wb'
	with open(dst,'wb') as img:
		while(True):
			try:
				req=requests.get(url=src,headers=header)#下载图片也要从url中获取图片编码,需要申请访问
				img.write(req.content)#写入图片编码
				req.close()#关闭访问
			#遭遇访问失败,用except处理
			except:
				print('\n遭遇访问拒绝,尝试重新下载\n')
				time.sleep(1)#休息
				continue
			break

4.5 main函数

def main():
	BaseUrl="http://m.jj20.com/bz/ktmh/dmrw"
	IndexPage="list_99_1.html"
	LocalFolder="D:\\ACGPicture\\MyPic"
	LogPath="D:\\ACGPicture\\log.txt"
	FolderNum=1#初始,文件夹名为1,也是导航页编号
	Total=34#一共34页
	while(True):
		#从日志文件中读取记录
		with open(LogPath,"r") as Log:
			message=Log.readline()
			if message=="":
				FolderNum=1#日志为空,从1开始
			else:
				FolderNum=int(message.split(' ')[1])+1#否则取之,取编号
		if FolderNum > Total:
			print("图片全部下载完毕!")
			break
		#合成各导航页的url
		IndexPage="list_99_{}.html".format(FolderNum)
		#实例化爬取类
		Crawler=CrawlerTool(BaseUrl=BaseUrl,IndexPage=IndexPage,LocalFolder=LocalFolder,FolderNum=FolderNum)
		print("准备获取第{}组图片地址".format(FolderNum))
		start_time=time.time()#取时间
		Crawler.InitPicList()#初始化图片信息列表
		print("开始下载第{}组图片".format(FolderNum))
		Crawler.DloadPic()#下载图片
		end_time=time.time()#再取时间,用于计算用时
		print("\n第{}组图片下载完毕,共用时{}秒".format(FolderNum,end_time-start_time))
		with open(LogPath,"w") as Log:#记录写入
			message="下载到第 {} 组\n".format(FolderNum)
			Log.write(message)
		print("保存记录完毕!\n")

4.6执行情况

如下图,可以看到进度条和相关信息
在这里插入图片描述
下载好的图片
在这里插入图片描述

5 总结及反思

  1. 总结:
    其实爬取图片用一句话总结就是获取图片的url并下载之。具体来说不仅要找一张图片的url,更要能找到其他图片所在网页,无论是从html代码中的< a >标签获取还是自己拼接。此外还要应对可能的反爬虫

  2. 反思:
    我写的程序虽然能爬取图片,但是还存在缺陷:

    1. 进度条的显示有时候会出现堆叠,虽然这不影响爬取的过程。
    2. 应对反爬虫的机制有待提升
    3. 代码水平有限,可读性一般,码风有待矫正

    以下是我遇到的问题及解决方法:

    1. 在程序使用时,曾遇到一个问题:下载进度条不动,正在下载的图片越来越大。结束程序后发现下载的图片有数百MB。开始是以为反爬虫机制,而钻研半天。后来检查代码发现有个continue语句少打个tab键,导致图片被反复写入。对于经常写c++而很少写py的我来说,这种错误应该多加注意。
    2. 我一度在读取标签的时候报错,反复检查解析语句仍然不能解决。后来输出网页html代码发现标签内的内容没有被读取到,而我找的就是那块内容,因此报错。后来用其他方法来代替读取这个标签。这告诉我要先自己从下载下来的html源码中找一下标签,再让程序找,因为程序会报错但不会反思。

以上是这篇文章的全部内容,希望对你有帮助,相信你也能正确爬取到想要的图片。当然也欢迎大家前来交流讨论或纠错。感谢阅读。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页