0. 写在前面
-
代码和所需要库,以存放github,需要的自提。传送门:https://github.com/Vrolist/ScriptPython-ImageSpider
-
视频教程【免费】:https://study.163.com/course/courseMain.htm?courseId=1006148015
1. 准备工作
-
安装Python3
-
安装requests库,命令行 pip3 install requests
-
安装lxml【需要解析xpath】,命令行 pip3 install lxml
-
一点xpath基础,参考教程:《xpath教程》: 伊始
测试下python3以及库是否可用:
test_enviroment.PNG2. 分析目标网站
目标网站:斗图啦,网址:www.doutula.com
本次文档主要是对斗图啦网站进行抓取,下载图片。
首先来分析下网站首页
web.PNG page.PNG首页这里有不同的套图,底部有个翻页。点击不同的翻页,看下效果...
2.PNG 6.PNG这里贴出了第二页和第六页的截图,url的规律是http://www.doutula.com/article/list/?page=页码
所以本次的目的,就是抓取该类url下的全部图片并保存本地
3. 获取单页的全部图片
这里以第二页的网页为例:
2-s.PNG图片的右侧已经打开了调试工具,且当前是Elements栏,看到class="col-sm-9"
和class="col-sm-3"
,这个页面是基于Bootstrap搭建的,肯定跑不了。
然后需要下载的图片,全部在col-sm-9
里面,所以定位所需的图片,就简单了,然后就有了下面这张图:
PS:在Elements栏打开xpaht检索框,按 ctrl+f 就会弹出框,支持字符串、selector、xpath
三种检索方式。
这里的检索结果,只有一个,就是我们要的那个,太好了,接下来就开始获取图片的img标签,然后提取它的属性src即可。于是....
xpath-img.PNG
结果显示有50个图片,但是这个结果绝对是错的,因为我数了,没这么多。然后我就自席间擦,发现gif标志就是一个img的标签图,所以,这里的xpath需要做判断,只要大图,不要gif图。
于是,又有了下面这个图:
xpath-img2.PNGxpath规则,由.//div[@class="col-sm-9"]//img/@src
变成了.//div[@class="col-sm-9"]//img/@data-original
,为啥呀?原因有下:
-
gif 的 img 标签,没有 data-original 属性
-
正常图片的img和data-original属性,它们的值是完全一样的
另外,特别重要的一点。有经验的应该知道,data-original这个属性的出现,肯定是该网页使用了jQuery图片延迟加载插件jQuery.lazyload
,该插件的使用,就是依赖于data-original属性,并且该img标签,类名肯定有个值,叫lazy
到这,就正确的提取到了我们需要的40个图片【我数了,4*10,正确无误】,而且取出来的url,是包含了域名的,不需要做额外操作。
接下来就是写代码了,贴个截图,看下代码和效果:
import requests
from lxml import etree
url = 'http://www.doutula.com/article/list/?page=2'
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36',
}
resp = requests.get(url,headers=headers)
html = etree.HTML(resp.text)
imgs = html.xpath('.//div[@class="col-sm-9"]//img/@data-original')
print(imgs,len(imgs))
py-1.PNG
运行结果一切正常,这里对代码做个简短的说明:
-
导入部分
-
headers,这个是最好加的,请求一个网站,请求头里面的User-Agent是浏览器信息,有这个就是模拟浏览器发出信息了
-
html = etree.HTML(resp.text)
解析纯html字符串,解析之后就可以使用xpath了 -
imgs = html.xpath('.//div[@class="col-sm-9"]//img/@data-original')
使用规则提取指定的数据,结果是列表格式;如果没有数据就是空列表; -
最后将数据输出,40个,正常。
单页的图片地址提取,到这就完成了。后面,我们来做图片下载
4. 图片下载
前面我们拿到了图片的地址,有图片地址就可以直接请求图片并保存本地了,很简单。步骤如下:
-
首先,通过图片地址,发起请求,拿到响应,响应的数据就是图片了
-
然后,新建一个本地文件,将响应数据写到文件上
逻辑是非常简单的,下载图片也是一个从url到本地文件的过程,那就来封装函数吧
先上代码:
def download_img(src):
filename = src.split('/')[-1] # 步骤1
img = requests.get(src, headers=headers) # 步骤2
# img是图片响应,不能字符串解析;
# img.content是图片的字节内容
with open('imgs/' + filename, 'wb') as file: # 步骤3
file.write(img.content) # 步骤4
print(src, filename) # 步骤5
代码比较简洁,定义一个 download_img 函数,接收一个参数src,然后完成下载操作,这里也来介绍下:
-
步骤一,从传入的参数src里面,提取图片的名字。通常url经过 / 分割,最后一个字符串就是图片名。
-
对src发起请求,记住要带上请求头,就可以拿到响应,存入img。此时的img是http响应,响应数据是一张图片,响应的头部还有些数据。
-
步骤三是使用open函数,以二进制写的方式打开一个文件,然后写入img.content。为什么用二进制?因为img.content是字节。
-
另外,步骤三里面,有个imgs/,需要在当前py文件所在的目录中,创建一个imgs的文件夹,程序运行前创建好。
-
输出,其余的没啥
看下运行结果和图片文件截图:
40.PNG 40-2.PNG重要说明:虽然这里没有碰到坑,但是图片请求和下载,一定会遇到一个Referer的坑。这个问题的来源,是云托管服务中,在存储图片时,不希望别人网站拿去盗用,所以就设置一个“防跨域请求”的限制。
在请求图片时,查看下请求头的Referer字段【浏览器请求时,会把域名放上去】。如果是来自未设置网站的来源,则不返回图片。所以请求图片,推荐在请求头中,加上Referer字段,值就是域名
当前的完整代码部分:
import requests
from lxml import etree
url = 'http://www.doutula.com/article/list/?page=2'
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36',
'Referer':'http://www.doutula.com/',
}
def download_img(src):
filename = src.split('/')[-1]
img = requests.get(src, headers=headers)
# img是图片响应,不能字符串解析;
# img.content是图片的字节内容
with open('imgs/' + filename, 'wb') as file:
file.write(img.content)
print(src, filename)
resp = requests.get(url,headers=headers)
html = etree.HTML(resp.text)
imgs = html.xpath('.//div[@class="col-sm-9"]//img/@data-original')
for img in imgs:
download_img(img)
5. 翻页处理
翻页这里,也不难,但是涉及些知识点,所以这里好好讲讲,多个方案对比看看。
第一种:函数递归
分析图片的url以及翻页,封装一个函数,在函数里面判断下一页,有则调用自身。代码会很简单,且思路清晰。
第二种:循环拼接URL
这种方式,适合有规则的url,找到规律,循环操作,并且可以预判最后一个url,方便停止。循环就是翻页的过程,简单。
第三种:函数返回URL
基于 【1 + 3】 思路,首先一个死循环,循环内调用函数,处理第一个url。函数在处理了图片url和翻页的时候,这时返回下一页的url。循环收到了函数的返回值,判断有没有下一页?有,继续调用函数;没有,停止循环。这样就不构成递归,而是简单的循环。
第四种:生成器
基于思路【3】,函数返回,return即可;如果你将return改成yield,就成了生成器,用法基本和思路【3】一致。
不考虑实际情况,再多思路都是扯淡,所以这里还是先上截图,看下斗图啦网站的翻页是怎么样的。
page-1.PNG page-2.PNG page-8.PNG page-587.PNG上面贴了三张图,分别是 1 - 2 - 8 - 587 三页。
从图中可以看出,第二页没有 9 和 10 页,第八页有 9 和 10 、 11页。也就是说到了某一页,对应的前后三页的数据都是展示【除了没有的】。
然后看到第一页和最后一页,第一页中往前翻的箭头是无法点击的;最后一页中往后翻的箭头是无法点击的。
所以,有以下几个方案可以做:
-
获取翻页的最大数值,走思路【2】的方案,循环拼接URL
-
获取翻页的全部URL,逐个请求并分析下次所得的URL,做个筛查,请求过的URL不在请求。思路【4】
-
针对不是最后一页就有下一页翻页的思想,做函数的递归调用。思路【1】,总共587
-
针对不是最后一页就有下一页翻页的思想,做函数返回URL的方案。思路【3】
-
判断是否有图片,有图片则表示可能有下一页,继续请求,用图片来判断是否坐下一页的请求,思路【2】
-
等等,方法挺多的....
方法这么多,选一个简单且可行的,第5个方法。
为什么选第5个?因为:数字递增且URL容易拼接,每页都需要对图片进行解析,操作方便。循环+函数返回的操作,是Python的基本操作,要求低。
有思路有方法,那撸起袖子开始干了....
首先是封装函数函数,在解析图片的基础之上,判断图片的数据:如果有返回True;没有返回False;很简单,上函数代码:
def parse_page(url):
resp = requests.get(url,headers=headers)
html = etree.HTML(resp.text)
imgs = html.xpath('.//div[@class="col-sm-9"]//img/@data-original')
if imgs:
return True
else:
return False
有这个函数,那下面就是写个循环逻辑,对该函数进行调用并一直判断函数返回值,再递增数字,拼接URL,在调用函数了,整体代码如下:
import requests
from lxml import etree
from time import sleep
url = 'http://www.doutula.com/article/list/?page=2'
headers = {
'Referer':'http://www.doutula.com/',
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36',
}
def parse_page(url):
resp = requests.get(url,headers=headers)
html = etree.HTML(resp.text)
imgs = html.xpath('.//div[@class="col-sm-9"]//img/@data-original')
if imgs:
return True
else:
return False
base_url = 'http://www.doutula.com/article/list/?page={}'
i = 1
next_link = True
while next_link:
next_link = parse_page(base_url.format(i))
if next_link :
i += 1
else:
break
print(i)
print('~OVER~')
代码中,首先定义几个值,用于拼接的base_url,循环用的i,以及下一页判断参数next_link。
在函数返回值为True是,i加1,同时while循环成立,继续调用函数;否则break,跳出循环。
测试结果:
145.PNG怎么只有145页?因为145页是个错误页面,跳过就好,来看下145页的界面:
145page.PNG所以代码稍微带动下,在i等于145 时再做个增加,跳过它。
不过,很快,我意识到了这是一个错误的思路。既然145页会错,后面还有400多页,肯定还有错误的页面,于是我就测试一下,错误页面有这些:
145,246, 250, 344, 470, 471, 563, 565, 589, 590, 591, 592
所以,在不知道哪些是错误页面的前提是,不能随便给数字做加法,然后就有了另一个思路:
-
错误的数字,不会连着出现,最多出现一次
-
末尾的几页,不会报错,直接出现无效,也就是末尾会出现连续的无图片
所以,逻辑上就可以做连错处理。如果连续出现三次无页面,跳出循环,如果仅仅是一次、两次,跳过,继续往下爬。
上逻辑部分代码:
base_url = 'http://www.doutula.com/article/list/?page={}'
i = 1
error_time = 0
next_link = True
while next_link:
next_link = parse_page(base_url.format(i))
if next_link :
i += 1
error_time = 0
else:
if error_time>=3:
print(error_time,'break')
break
i+=1
error_time+=1
next_link = True
print(i,error_time)
print('~OVER~')
上运行结果截图【测试多次,结果会有差异】:
589.PNG6. 总结
到这,翻页就完成了。加上前面的图片URL解析、图片下载、翻页处理,一个简单的图片爬虫就完成了。
不过呢,要加异常处理,否则一个错误终止整个程序,后续都没得玩了
贴下完成代码,以及运行结果图:
import requests
from lxml import etree
from time import sleep
url = 'http://www.doutula.com/article/list/?page=2'
headers = {
'Referer':'http://www.doutula.com/',
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36',
}
def parse_page(url):
resp = requests.get(url,headers=headers)
html = etree.HTML(resp.text)
imgs = html.xpath('.//div[@class="col-sm-9"]//img/@data-original')
for img in imgs:
try:
download_img(img)
except:
pass
if imgs:
return True
else:
return False
def download_img(src):
filename = src.split('/')[-1]
img = requests.get(src, headers=headers)
# img是图片响应,不能字符串解析;
# img.content是图片的字节内容
with open('imgs/' + filename, 'wb') as file:
file.write(img.content)
print(src, filename)
base_url = 'http://www.doutula.com/article/list/?page={}'
i = 1
error_time = 0
next_link = True
while next_link:
sleep(0.5)
try:
next_link = parse_page(base_url.format(i))
except:
next_link = True
if next_link :
i += 1
error_time = 0
else:
if error_time>=3:
print(error_time,'break')
break
i+=1
error_time+=1
next_link = True
print(i,error_time)
print('~OVER~')
截图是,下载15926张图片,但是程序依旧在运行,也就是说....图片还更多。
15926.PNG
你们拿到爬虫代码后,可以自己去运行,记得创建一个imgs文件夹,慢慢下载哟
网友评论