爬蟲案例:手把手教你爬取圖片
寫在最前面:程序很簡單,重要的是思路以及對錯(cuò)誤的分析
1.確認(rèn)目標(biāo)和大體思路:
今天我們需要爬取的網(wǎng)站是:http://www.bbsnet.com/doutu
下面是網(wǎng)站的首頁:

可以看見,這個(gè)網(wǎng)站的特點(diǎn)是,一個(gè)圖片是一系列圖片的集合,因此我們需要先獲取每一個(gè)圖片的地址,然后跳轉(zhuǎn)進(jìn)去才能獲取里面真正的圖片。
接下來我們首先獲取第一頁的圖片地址,一般來說,第一頁與其他頁的差別就在于url的一些小差別,因此,搞定第一頁等于搞定所有。
下面開始盤它。
補(bǔ)充:我們下面使用requets庫請求和xpath來解析(附贈(zèng)正則解析方法)
二、分析與編寫代碼:
1.第一步導(dǎo)入庫:
#導(dǎo)包
from?lxml?import?etree
import?requests
import?re
import?time
2.第二步寫出大體框架:
這里我們還是采用面向?qū)ο髞韺?,因?yàn)檫@樣后期如果要改為多線程還是挺容易的。
class?MySpider(object):
????def?__init__(self):
????????#顯然我們需要最基礎(chǔ)的headers信息
????????self.headers?=?{
????????????'User-Agent':'Mozilla/5.0?(Windows?NT?10.0;?Win64;?x64)?AppleWebKit/537.36?(KHTML,?like?Gecko)?Chrome/86.0.4240.75?Safari/537.36',
????????}
????????#其次我們需要url
????????self.url?=?'http://www.bbsnet.com/doutu'
????
????def?run(self):
????????#首先我們需要一個(gè)函數(shù)獲取每一個(gè)url
????????target_urls?=?self.get_target_urls()
????????#其次我們需要去獲取每一個(gè)url里面的圖片
????????self.get_target_images(target_urls)
????????
????def?get_target_urls(self):
????????'''這里我們使用一個(gè)東西,比如列表等來存儲(chǔ)url的結(jié)果并返回'''
????????pass
????def?get_target_images(self,target_urls):
????????'''這里我們需要接收之前獲取的url,所以需要一個(gè)參數(shù)'''
????????pass
if?__name__?==?'__main__':
????spider?=?MySpider()
????spider.run()
顯然上面的代碼跟我們的思路一模一樣,是不是so easy。
3.第三步完成第一個(gè)函數(shù) -- get_target_urls():
首先按照慣例,寫下以下代碼:
response?=?requests.get(self.url,headers=self.headers)
print(response.content.decode('utf-8'))
首先,分享下為什么有的時(shí)候我們對于文本要decode('utf-8'),而有時(shí)候?qū)τ谖谋拘枰猟ecode('gbk'),這是根據(jù)網(wǎng)頁源代碼的編碼形式來寫的,比如這個(gè)網(wǎng)站的源代碼如下:

但是,嘗試運(yùn)行了下,發(fā)現(xiàn)以下錯(cuò)誤:
UnicodeEncodeError:?'gbk'?codec?can't?encode?character?'\u2009'?in?position?424:?illegal?multibyte?sequence
這個(gè)意思是說,這個(gè)網(wǎng)頁存在一些字符是utf-8無法解碼的,這是很正常的,畢竟有些神奇的字符,是utf-8無法表示的。
但是,大家不必緊張,這并不影響我們寫代碼。于是我們就這樣吧,不解碼了,直接以二進(jìn)制的形式存儲(chǔ)它,因?yàn)樗⒉挥绊懳覀內(nèi)カ@取我們需要的鏈接。
于是寫下這樣代碼:
response?=?requests.get(self.url,headers=self.headers)
text?=?response.content
好的,下面我們開始解析網(wǎng)頁:

可以得到以下結(jié)果:

下面我們使用xpath來解析這個(gè)html代碼,我們首先如上圖定位a標(biāo)簽,顯然這個(gè)a標(biāo)簽在h2標(biāo)簽里面,且這個(gè)h2標(biāo)簽里面只有這么一個(gè)a標(biāo)簽,所以十分容易獲取,代碼如下:
html?=?etree.HTML(text)
target_urls?=?html.xpath('//h2/a/@href')
print(target_urls)
下面是嘗試打印的結(jié)果(部分):
['http://www.bbsnet.com/aoteman.html',?'http://www.bbsnet.com/jieqian.html',?'http://www.bbsnet.com/youxitu.html',?'http://www.bbsnet.com/liaotian-3.html',......]
好的,至此,這個(gè)函數(shù)就完成了,是不是十分簡單。該函數(shù)代碼如下:
def?get_target_urls(self):
?response?=?requests.get(self.url,headers=self.headers)
????text?=?response.content
?html?=?etree.HTML(text)
????target_urls?=?html.xpath('//h2/a/@href')
?return?target_urls
4.第四步完成第二個(gè)函數(shù) --get_target_images():
老套路,寫下下面的代碼:
for?url?in?target_urls:
????response?=?requests.get(url,headers=self.headers)
????text?=?response.content.decode('utf-8')
????print(text)
????#這里強(qiáng)烈建議加上break測試
????break
于是,得到下面的結(jié)果:
<html><head><script?type="text/javascript">function?f(){window.location.href="http://www.bbsnet.com/aoteman.html";}script>head><body?onload="f()"><img?style="display:none"?src="http://tieba.baidu.com/_PXCK_7132125338940_2652120900.gif"?/>body>html>
好吧,顯然這并不是我們需要的結(jié)果,那這是什么呢?這段代碼(html代碼)會(huì)自動(dòng)把我們重定向到首頁,這樣我們顯然獲取不成功,于是,我們怎么辦呢?在訪問一次就可以了,我們僅需要在加上一次請求即可獲取所需的html代碼。如下:
for?url?in?target_urls:
????response?=?requests.get(url,headers=self.headers)
????response?=?requests.get(url,headers=self.headers)
????text?=?response.content
????#這里同上面,不解碼,直接解析,原因同上
????print(text)
????break
結(jié)果如下(部分結(jié)果):
b'html?PUBLIC?"-//W3C//DTD?XHTML?1.0?Transitional//EN"?"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\r\n<html?xmlns="http://www.w3.org/1999/xhtml">\r\n<head?profile="http://gmpg.org/xfn/11">\r\n<meta?charset="UTF-8">\r\n<meta?http-equiv="Content-Type"?content="text/html"?/>\r\n<meta?name="referrer"?content="no-referrer"?/>.......
好的,解決了這個(gè)大麻煩,我們下面的工作就很簡單了。

可以得到以下內(nèi)容:

我們需要獲取img標(biāo)簽里面的src屬性,僅需獲取div標(biāo)簽下的p標(biāo)簽下的img標(biāo)簽即可。下面是代碼:
for?url?in?target_urls:
????response?=?requests.get(url,headers=self.headers)
????time.sleep(2)?#一定要加上,不然會(huì)訪問太快會(huì)被禁止的
????response?=?requests.get(url,headers=self.headers)
????text?=?response.content
????#開始解析
????html?=?etree.HTML(text)
????target_images?=?html.xpath('//div[@id="post_content"]//p//img/@src')
????print(target_images)
????break
部分結(jié)果如下:
['http://wx1.sinaimg.cn/mw690/6a04b428ly1g19akwia0rg209b09qwgf.gif',?'http://wx1.sinaimg.cn/mw690/6a04b428ly1g19al1td90g209q08sq4i.gif',....]
好的,我們僅僅剩最后一步,下載圖片,這里我給出兩者方式:
方式一:使用urllib庫里的urlretrieve()函數(shù),代碼如下:
#該段代碼添加至獲得target_images后面
#大家使用這個(gè)方法的話,一定要記得導(dǎo)入庫:from urllib import request
for?image_url?in?target_images:
????request.urlretrieve(image_url,'./info/test.gif')
????break
方式二:獲取鏈接的源代碼后保存至本地,代碼如下:
#該段代碼添加至獲得target_images后面
for?image_url?in?target_images:
????response?=?requests.get(image_url,headers=self.headers)
????f?=?open('./info/test.gif','wb')
????f.write(response.content)
????f.close()
????break
兩者的結(jié)果都如下:

ok,到此處,恭喜大家,已經(jīng)完成了99%的任務(wù)了。下面就是完善和補(bǔ)充一些代碼了。
此函數(shù)完整代碼如下:
def?get_target_images(self,target_urls):
????for?url?in?target_urls:
????response?=?requests.get(url,headers=self.headers)
????time.sleep(2)
????response?=?requests.get(url,headers=self.headers)
????text?=?response.content
????html?=?etree.HTML(text)
????target_images?=html.xpath('//div[@id="post_content"]//p//img/@src')
????for?image_url?in?target_images:
????????request.urlretrieve(image_url,'./info/test.gif')
????????break
????break
????#建議大家測試的時(shí)候,加上break,如果有基礎(chǔ)的可以考慮代理或者多線程
5.第五步優(yōu)化與完善:
一個(gè)優(yōu)秀的人,總是想寫出點(diǎn)優(yōu)秀的代碼,所以優(yōu)化必不可少
1.優(yōu)化角度一:文件存儲(chǔ)的優(yōu)化:
首先,上面我在這個(gè)py文件下面創(chuàng)建了一個(gè)info文件夾,所以才能寫"./info/..."這樣的代碼,但是我相信大家的目錄下肯定沒有。所以必須優(yōu)化。
其次,我們可以這樣考慮:使用os模塊自動(dòng)創(chuàng)建一個(gè)images的文件夾,其次可以在獲取圖片集合的url時(shí)順便獲取每個(gè)圖片集合的標(biāo)題,所以開始改寫代碼:
修改完成后如下(只需修改第二個(gè)函數(shù)):
def?get_target_images(self,target_urls):
????os.mkdir('./images')
????for?url?in?target_urls:
????response?=?requests.get(url,headers=self.headers)
????time.sleep(2)
????response?=?requests.get(url,headers=self.headers)
????text?=?response.content
????html?=?etree.HTML(text)
????target_images?=?html.xpath('//div[@id="post_content"]//p//img/@src')
????title?=?html.xpath('//div[@id="post_content"]//p//img/@title')[0]
????os.mkdir('./images/'+title)
????for?index,image_url?in?enumerate(target_images):
????????response?=?requests.get(image_url,?headers=self.headers)
????????#這里我是用文件方式下載,因?yàn)椴恢罏槭裁次疫\(yùn)行urlretrieve下載時(shí)下太久了
????????f?=?open('./images/%s/%s.gif'%(title,index),?'wb')
????????f.write(response.content)
????????f.close()
????break
示例結(jié)果;

2.優(yōu)化角度二:速度優(yōu)化:
我相信如果直接去掉break去爬取所有的肯定會(huì)報(bào)錯(cuò),因?yàn)樵L問太快了,所以可以采取一定的優(yōu)化手段去優(yōu)化它,這里我推薦兩種方式,第一種是多線程,第二種是代理。這里我就不給出如何實(shí)現(xiàn)這種優(yōu)化了,因?yàn)檫@篇文章的目的不在于此。
3.優(yōu)化角度三:代碼本身優(yōu)化:
這里第二個(gè)函數(shù)的代碼其實(shí)有點(diǎn)長了,因?yàn)槔锩嬗袃蓚€(gè)循環(huán)。所以可以考慮將其中的每一個(gè)循環(huán)封裝為一個(gè)新的函數(shù)。這個(gè)結(jié)果我將給在最后的全部代碼里面。
4.優(yōu)化角度四:異常捕獲:
想要捕獲異常,有簡單的方式,也有復(fù)雜的方式。下面只給出簡單方式的代碼:
這里我們只需修改主函數(shù):
def?run(self):
????try:
????????#首先我們需要一個(gè)函數(shù)獲取每一個(gè)url
????????target_urls?=?self.get_target_urls()
????????#其次我們需要去獲取每一個(gè)url里面的圖片
????????self.get_target_images(target_urls)
????except?Exception?as?e:
????????print('錯(cuò)誤原因:',e)
復(fù)雜的方式就是分別去獲取一些常見的異常,如404或者其他異常。
5.完善代碼:
下面給出如何爬取多頁的思路:
首先我們需要提取出第一、二、三頁的url,如下:
第一頁:http://www.bbsnet.com/doutu
第二頁:http://www.bbsnet.com/doutu/page/2
第三頁:http://www.bbsnet.com/doutu/page/3
好的,下面提出共同的部分:http://www.bbsnet.com/doutu/page/ + 頁數(shù)
這里為了驗(yàn)證上面對于第一頁這個(gè)特殊頁是否適用,訪問http://www.bbsnet.com/doutu/page/1,結(jié)果可以成功訪問,因此下面我們只需去一個(gè)個(gè)迭代即可獲取一頁頁的圖片。
代碼我將給在整體代碼中。
三、全部代碼:
#?-*-?coding:utf-8?-*-
#?Author:?自學(xué)小白菜
'''
既然選擇了前進(jìn),那每一步都要認(rèn)真的去做
'''
#導(dǎo)包
from?lxml?import?etree
import?requests
from?urllib?import?request
import?time
import?os
class?MySpider(object):
????def?__init__(self):
????????#顯然我們需要最基礎(chǔ)的headers信息
????????self.headers?=?{
????????????'User-Agent':'Mozilla/5.0?(Windows?NT?10.0;?Win64;?x64)?AppleWebKit/537.36?(KHTML,?like?Gecko)?Chrome/86.0.4240.75?Safari/537.36',
????????}
????????#其次我們需要url
????????self.url?=?'http://www.bbsnet.com/doutu/page/{page}'
????def?run(self):
????????try:
????????????pages?=?int(input('請輸入需要獲取多少頁:'))
????????????for?page?in?range(1,pages+1):
????????????????url?=?self.url.format(page?=?page)
????????????????#首先我們需要一個(gè)函數(shù)獲取每一個(gè)url
????????????????target_urls?=?self.get_target_urls(url)
????????????????#其次我們需要去獲取每一個(gè)url里面的圖片
????????????????self.get_target_images(target_urls)
????????except?Exception?as?e:
????????????print('錯(cuò)誤原因:',e)
????def?get_target_urls(self,url):
????????'''這里我們使用一個(gè)東西,比如列表等來存儲(chǔ)url的結(jié)果并返回'''
????????response?=?requests.get(url,headers=self.headers)
????????text?=?response.content
????????#下面開始解析
????????html?=?etree.HTML(text)
????????target_urls?=?html.xpath('//h2/a/@href')
????????return?target_urls
????def?get_target_images(self,target_urls):
????????'''這里我們需要接收之前獲取的url,所以需要一個(gè)參數(shù)'''
????????os.mkdir('./images')
????????for?url?in?target_urls:
????????????target_images,title?=?self.get_info(url)
????????????for?index,image_url?in?enumerate(target_images):
????????????????self.download(image_url,index,title)
????????????????break
????????????break
????def?get_info(self,url):
????????response?=?requests.get(url,?headers=self.headers)
????????time.sleep(2)
????????response?=?requests.get(url,?headers=self.headers)
????????text?=?response.content
????????#?開始解析
????????html?=?etree.HTML(text)
????????target_images?=?html.xpath('//div[@id="post_content"]//p//img/@src')
????????title?=?html.xpath('//div[@id="post_content"]//p//img/@title')[0]
????????os.mkdir('./images/'?+?title)
????????return?target_images,title
????def?download(self,image_url,index,title):
????????response?=?requests.get(image_url,?headers=self.headers)
????????f?=?open('./images/%s/%s.gif'?%?(title,?index),?'wb')
????????f.write(response.content)
????????f.close()
if?__name__?==?'__main__':
????spider?=?MySpider()
????spider.run()
