聲明:本文講解的實(shí)戰(zhàn)內(nèi)容,均僅用于學(xué)習(xí)交流,請(qǐng)勿用于任何商業(yè)用途!
一、前言
強(qiáng)烈建議:請(qǐng)?jiān)陔娔X的陪同下,閱讀本文。本文以實(shí)戰(zhàn)為主,閱讀過(guò)程如稍有不適,還望多加練習(xí)。
本文的實(shí)戰(zhàn)內(nèi)容有:
網(wǎng)絡(luò)小說(shuō)下載(靜態(tài)網(wǎng)站)
優(yōu)美壁紙下載(動(dòng)態(tài)網(wǎng)站)
愛(ài)奇藝VIP視頻下載
二、網(wǎng)絡(luò)爬蟲(chóng)簡(jiǎn)介
網(wǎng)絡(luò)爬蟲(chóng),也叫網(wǎng)絡(luò)蜘蛛(Web Spider)。它根據(jù)網(wǎng)頁(yè)地址(URL)爬取網(wǎng)頁(yè)內(nèi)容,而網(wǎng)頁(yè)地址(URL)就是我們?cè)跒g覽器中輸入的網(wǎng)站鏈接。比如:https://www.baidu.com/,它就是一個(gè)URL。
在講解爬蟲(chóng)內(nèi)容之前,我們需要先學(xué)習(xí)一項(xiàng)寫爬蟲(chóng)的必備技能:審查元素(如果已掌握,可跳過(guò)此部分內(nèi)容)。
1. 審查元素
在瀏覽器的地址欄輸入U(xiǎn)RL地址,在網(wǎng)頁(yè)處右鍵單擊,找到檢查,如下圖所示:(不同瀏覽器的叫法不同,Chrome瀏覽器叫做檢查,F(xiàn)irefox瀏覽器叫做查看元素,但是功能都是相同的)
我們可以看到,右側(cè)出現(xiàn)了一大推代碼,這些代碼就叫做HTML。什么是HTML?舉個(gè)容易理解的例子:我們的基因決定了我們的原始容貌,服務(wù)器返回的HTML決定了網(wǎng)站的原始容貌。
為啥說(shuō)是原始容貌呢?因?yàn)槿丝梢哉莅。≡牧耍心居校磕蔷W(wǎng)站也可以"整容"嗎?可以!請(qǐng)看下圖:
我能有這么多錢嗎?顯然不可能。我是怎么給網(wǎng)站"整容"的呢?就是通過(guò)修改服務(wù)器返回的HTML信息。我們每個(gè)人都是"整容大師",可以修改頁(yè)面信息。我們?cè)陧?yè)面的哪個(gè)位置點(diǎn)擊審查元素,瀏覽器就會(huì)為我們定位到相應(yīng)的HTML位置,進(jìn)而就可以在本地更改HTML信息。
再舉個(gè)小例子:我們都知道,使用瀏覽器"記住密碼"的功能,密碼會(huì)變成一堆小黑點(diǎn),是不可見(jiàn)的。可以讓密碼顯示出來(lái)嗎?可以,只需給頁(yè)面"動(dòng)個(gè)小手術(shù)"!以淘寶為例,在輸入密碼框處右鍵,點(diǎn)擊檢查。
可以看到,瀏覽器為我們自動(dòng)定位到了相應(yīng)的HTML位置。將下圖中的password屬性值改為text屬性值(直接在右側(cè)代碼處修改):
就這樣,瀏覽器"記住的密碼"顯現(xiàn)出來(lái)了:
說(shuō)這么多,什么意思呢?瀏覽器就是作為客戶端從服務(wù)器端獲取信息,然后將信息解析,并展示給我們的。我們可以在本地修改HTML信息,為網(wǎng)頁(yè)"整容",但是我們修改的信息不會(huì)回傳到服務(wù)器,服務(wù)器存儲(chǔ)的HTML信息不會(huì)改變。刷新一下界面,頁(yè)面還會(huì)回到原本的樣子。這就跟人整容一樣,我們能改變一些表面的東西,但是不能改變我們的基因。
2. 簡(jiǎn)單實(shí)例
網(wǎng)絡(luò)爬蟲(chóng)的第一步就是根據(jù)URL,獲取網(wǎng)頁(yè)的HTML信息。在Python3中,可以使用urllib.request和requests進(jìn)行網(wǎng)頁(yè)爬取。
urllib庫(kù)是python內(nèi)置的,無(wú)需我們額外安裝,只要安裝了Python就可以使用這個(gè)庫(kù)。
requests庫(kù)是第三方庫(kù),需要我們自己安裝。
requests庫(kù)強(qiáng)大好用,所以本文使用requests庫(kù)獲取網(wǎng)頁(yè)的HTML信息。requests庫(kù)的github地址:https://github.com/requests/requests
(1)requests安裝
在學(xué)習(xí)使用requests庫(kù)之前,我們需要在電腦中安裝好requests庫(kù)。在cmd中,使用如下指令安裝requests庫(kù):
pip install requests
easy_install requests
使用pip和easy_install都可以安裝,二選一即可。
(2)簡(jiǎn)單實(shí)例
安裝好requests庫(kù)之后,我們先來(lái)大體瀏覽一下requests庫(kù)的基礎(chǔ)方法:
官方中文教程地址:http://docs.python-requests.org/zh_CN/latest/user/quickstart.html
requests庫(kù)的開(kāi)發(fā)者為我們提供了詳細(xì)的中文教程,查詢起來(lái)很方便。本文不會(huì)對(duì)其所有內(nèi)容進(jìn)行講解,摘取其部分使用到的內(nèi)容,進(jìn)行實(shí)戰(zhàn)說(shuō)明。
首先,讓我們看下requests.get()方法,它用于向服務(wù)器發(fā)起GET請(qǐng)求,不了解GET請(qǐng)求沒(méi)有關(guān)系。我們可以這樣理解:get的中文意思是得到、抓住,那這個(gè)requests.get()方法就是從服務(wù)器得到、抓住數(shù)據(jù),也就是獲取數(shù)據(jù)。讓我們看一個(gè)例子(以 www.gitbook.cn? 為例)來(lái)加深理解:
# -*- coding:UTF-8 -*-
import requests
if __name__ == '__main__':
target = 'http://gitbook.cn/'
req = requests.get(url=target)
print(req.text)
requests.get()方法必須設(shè)置的一個(gè)參數(shù)就是url,因?yàn)槲覀兊酶嬖VGET請(qǐng)求,我們的目標(biāo)是誰(shuí),我們要獲取誰(shuí)的信息。我們將GET請(qǐng)求獲得的響應(yīng)內(nèi)容存放到req變量中,然后使用req.text就可以獲得HTML信息了。運(yùn)行結(jié)果如下:
左側(cè)是我們程序獲得的結(jié)果,右側(cè)是我們?cè)趙ww.gitbook.cn? 網(wǎng)站審查元素獲得的信息。我們可以看到,我們已經(jīng)順利獲得了該網(wǎng)頁(yè)的HTML信息。這就是一個(gè)最簡(jiǎn)單的爬蟲(chóng)實(shí)例,可能你會(huì)問(wèn),我只是爬取了這個(gè)網(wǎng)頁(yè)的HTML信息,有什么用呢?客官稍安勿躁,接下來(lái)進(jìn)入我們的實(shí)戰(zhàn)正文。
三、爬蟲(chóng)實(shí)戰(zhàn)
實(shí)戰(zhàn)內(nèi)容由簡(jiǎn)單到復(fù)雜,難度逐漸增加,但均屬于入門級(jí)難度。下面開(kāi)始我們的第一個(gè)實(shí)戰(zhàn)內(nèi)容:網(wǎng)絡(luò)小說(shuō)下載。
1. 小說(shuō)下載
(1)實(shí)戰(zhàn)背景
小說(shuō)網(wǎng)站《筆趣看》URL:http://www.biqukan.com/
《筆趣看》是一個(gè)盜版小說(shuō)網(wǎng)站,這里有很多起點(diǎn)中文網(wǎng)的小說(shuō),該網(wǎng)站小說(shuō)的更新速度稍滯后于起點(diǎn)中文網(wǎng)正版小說(shuō)的更新速度。并且該網(wǎng)站只支持在線瀏覽,不支持小說(shuō)打包下載。因此,本次實(shí)戰(zhàn)就是從該網(wǎng)站爬取并保存一本名為《一念永恒》的小說(shuō),該小說(shuō)是耳根正在連載中的一部玄幻小說(shuō)。PS:本實(shí)例僅為交流學(xué)習(xí),支持耳根大大,請(qǐng)上起點(diǎn)中文網(wǎng)訂閱。
(2)小試牛刀
我們先看下《一念永恒》小說(shuō)的第一章內(nèi)容,URL:http://www.biqukan.com/1_1094/5403177.html
用已經(jīng)學(xué)到的知識(shí)獲取HTML信息試一試,編寫代碼如下:
# -*- coding:UTF-8 -*-
import requests
if __name__ == '__main__':
target = 'http://www.biqukan.com/1_1094/5403177.html'
req = requests.get(url=target)
print(req.text)
運(yùn)行代碼,可以看到如下結(jié)果:
可以看到,我們很輕松地獲取了HTML信息。但是,很顯然,很多信息是我們不想看到的,我們只想獲得如右側(cè)所示的正文內(nèi)容,我們不關(guān)心那些看著眼暈的英文字母。如何把正文內(nèi)容從這些眾多的HTML信息中提取出來(lái)呢?這就是本小節(jié)實(shí)戰(zhàn)的主要內(nèi)容。
(3)Beautiful Soup
爬蟲(chóng)的第一步,獲取整個(gè)網(wǎng)頁(yè)的HTML信息,我們已經(jīng)完成。接下來(lái)就是爬蟲(chóng)的第二步,解析HTML信息,提取我們感興趣的內(nèi)容。對(duì)于本小節(jié)的實(shí)戰(zhàn),我們感興趣的內(nèi)容就是文章的正文。提取的方法有很多,例如使用正則表達(dá)式、Xpath、Beautiful Soup等。對(duì)于初學(xué)者而言,最容易理解,并且使用簡(jiǎn)單的方法就是使用Beautiful Soup提取感興趣內(nèi)容。
Beautiful Soup的安裝方法和requests一樣,使用如下指令安裝(也是二選一):
pip install beautifulsoup4
easy_install beautifulsoup4
一個(gè)強(qiáng)大的第三方庫(kù),都會(huì)有一個(gè)詳細(xì)的官方文檔。我們很幸運(yùn),Beautiful Soup也是有中文的官方文檔。URL:http://beautifulsoup.readthedocs.io/zh_CN/latest/
同理,我會(huì)根據(jù)實(shí)戰(zhàn)需求,講解Beautiful Soup庫(kù)的部分使用方法,更詳細(xì)的內(nèi)容,請(qǐng)查看官方文檔。
現(xiàn)在,我們使用已經(jīng)掌握的審查元素方法,查看一下我們的目標(biāo)頁(yè)面,你會(huì)看到如下內(nèi)容:
不難發(fā)現(xiàn),文章的所有內(nèi)容都放在了一個(gè)名為div的“東西下面”,這個(gè)"東西"就是html標(biāo)簽。HTML標(biāo)簽是HTML語(yǔ)言中最基本的單位,HTML標(biāo)簽是HTML最重要的組成部分。不理解,沒(méi)關(guān)系,我們?cè)倥e個(gè)簡(jiǎn)單的例子:一個(gè)女人的包包里,會(huì)有很多東西,她們會(huì)根據(jù)自己的習(xí)慣將自己的東西進(jìn)行分類。鏡子和口紅這些會(huì)經(jīng)常用到的東西,回歸放到容易拿到的外側(cè)口袋里。那些不經(jīng)常用到,需要注意安全存放的證件會(huì)被放到不容易拿到的里側(cè)口袋里。
html標(biāo)簽就像一個(gè)個(gè)“口袋”,每個(gè)“口袋”都有自己的特定功能,負(fù)責(zé)存放不同的內(nèi)容。顯然,上述例子中的div標(biāo)簽下存放了我們關(guān)心的正文內(nèi)容。這個(gè)div標(biāo)簽是這樣的:
細(xì)心的朋友可能已經(jīng)發(fā)現(xiàn),除了div字樣外,還有id和class。id和class就是div標(biāo)簽的屬性,content和showtxt是屬性值,一個(gè)屬性對(duì)應(yīng)一個(gè)屬性值。這東西有什么用?它是用來(lái)區(qū)分不同的div標(biāo)簽的,因?yàn)閐iv標(biāo)簽可以有很多,我們?cè)趺醇右詤^(qū)分不同的div標(biāo)簽?zāi)兀烤褪峭ㄟ^(guò)不同的屬性值。
仔細(xì)觀察目標(biāo)網(wǎng)站一番,我們會(huì)發(fā)現(xiàn)這樣一個(gè)事實(shí):class屬性為showtxt的div標(biāo)簽,獨(dú)一份!這個(gè)標(biāo)簽里面存放的內(nèi)容,是我們關(guān)心的正文部分。
知道這個(gè)信息,我們就可以使用Beautiful Soup提取我們想要的內(nèi)容了,編寫代碼如下:
# -*- coding:UTF-8 -*-
from bs4 import BeautifulSoup
import requests
if __name__ == "__main__":
target = 'http://www.biqukan.com/1_1094/5403177.html'
req = requests.get(url = target)
html = req.text
bf = BeautifulSoup(html)
texts = bf.find_all('div', class_ = 'showtxt')
print(texts)
在解析html之前,我們需要?jiǎng)?chuàng)建一個(gè)Beautiful Soup對(duì)象。BeautifulSoup函數(shù)里的參數(shù)就是我們已經(jīng)獲得的html信息。然后我們使用find_all方法,獲得html信息中所有class屬性為showtxt的div標(biāo)簽。find_all方法的第一個(gè)參數(shù)是獲取的標(biāo)簽名,第二個(gè)參數(shù)class_是標(biāo)簽的屬性,為什么不是class,而帶了一個(gè)下劃線呢?因?yàn)閜ython中class是關(guān)鍵字,為了防止沖突,這里使用class_表示標(biāo)簽的class屬性,class_后面跟著的showtxt就是屬性值了。看下我們要匹配的標(biāo)簽格式:
這樣對(duì)應(yīng)的看一下,是不是就懂了?可能有人會(huì)問(wèn)了,為什么不是find_all('div', id = 'content', class_ = 'showtxt')?這樣其實(shí)也是可以的,屬性是作為查詢時(shí)候的約束條件,添加一個(gè)class_='showtxt'條件,我們就已經(jīng)能夠準(zhǔn)確匹配到我們想要的標(biāo)簽了,所以我們就不必再添加id這個(gè)屬性了。運(yùn)行代碼查看我們匹配的結(jié)果:
我們可以看到,我們已經(jīng)順利匹配到我們關(guān)心的正文內(nèi)容,但是還有一些我們不想要的東西。比如div標(biāo)簽名,br標(biāo)簽,以及各種空格。怎么去除這些東西呢?我們繼續(xù)編寫代碼:
# -*- coding:UTF-8 -*-
from bs4 import BeautifulSoup
import requests
if __name__ == "__main__":
target = 'http://www.biqukan.com/1_1094/5403177.html'
req = requests.get(url = target)
html = req.text
bf = BeautifulSoup(html)
texts = bf.find_all('div', class_ = 'showtxt')
print(texts[0].text.replace('\xa0'*8,'\n\n'))
find_all匹配的返回的結(jié)果是一個(gè)列表。提取匹配結(jié)果后,使用text屬性,提取文本內(nèi)容,濾除br標(biāo)簽。隨后使用replace方法,剔除空格,替換為回車進(jìn)行分段。 在html中是用來(lái)表示空格的。replace('\xa0'*8,'\n\n')就是去掉下圖的八個(gè)空格符號(hào),并用回車代替:
程序運(yùn)行結(jié)果如下:
可以看到,我們很自然的匹配到了所有正文內(nèi)容,并進(jìn)行了分段。我們已經(jīng)順利獲得了一個(gè)章節(jié)的內(nèi)容,要想下載正本小說(shuō),我們就要獲取每個(gè)章節(jié)的鏈接。我們先分析下小說(shuō)目錄,URL:http://www.biqukan.com/1_1094/
通過(guò)審查元素,我們發(fā)現(xiàn)可以發(fā)現(xiàn),這些章節(jié)都存放在了class屬性為listmain的div標(biāo)簽下,選取部分html代碼如下:
《一念永恒》最新章節(jié)列表
《一念永恒》正文卷
在分析之前,讓我們先介紹一個(gè)概念:父節(jié)點(diǎn)、子節(jié)點(diǎn)、孫節(jié)點(diǎn)。
和
限定了
標(biāo)簽的開(kāi)始和結(jié)束的位置,他們是成對(duì)出現(xiàn)的,有開(kāi)始位置,就有結(jié)束位置。我們可以看到,在
標(biāo)簽包含
標(biāo)簽,那這個(gè)
標(biāo)簽就是
標(biāo)簽的子節(jié)點(diǎn),
標(biāo)簽又包含標(biāo)簽和
標(biāo)簽,那么
標(biāo)簽和
標(biāo)簽就是
標(biāo)簽的孫節(jié)點(diǎn)。有點(diǎn)繞?那你記住這句話:誰(shuí)包含誰(shuí),誰(shuí)就是誰(shuí)兒子!
他們之間的關(guān)系都是相對(duì)的。比如對(duì)于
標(biāo)簽,它的子節(jié)點(diǎn)是標(biāo)簽,它的父節(jié)點(diǎn)是
標(biāo)簽。這跟我們?nèi)耸且粯拥模嫌欣舷掠行 ?/p>
看到這里可能有人會(huì)問(wèn),這有好多
標(biāo)簽和標(biāo)簽啊!不同的
標(biāo)簽,它們是什么關(guān)系啊?顯然,兄弟姐妹嘍!我們稱它們?yōu)樾值芙Y(jié)點(diǎn)。
好了,概念明確清楚,接下來(lái),讓我們分析一下問(wèn)題。我們看到每個(gè)章節(jié)的名字存放在了標(biāo)簽里面。標(biāo)簽還有一個(gè)href屬性。這里就不得不提一下標(biāo)簽的定義了,標(biāo)簽定義了一個(gè)超鏈接,用于從一張頁(yè)面鏈接到另一張頁(yè)面。標(biāo)簽最重要的屬性是 href 屬性,它指示鏈接的目標(biāo)。
我們將之前獲得的第一章節(jié)的URL和標(biāo)簽對(duì)比看一下:
http://www.biqukan.com/1_1094/5403177.html
不難發(fā)現(xiàn),標(biāo)簽中href屬性存放的屬性值/1_1094/5403177.html是章節(jié)URLhttp://www.biqukan.com/1_1094/5403177.html的后半部分。其他章節(jié)也是如此!那這樣,我們就可以根據(jù)標(biāo)簽的href屬性值獲得每個(gè)章節(jié)的鏈接和名稱了。
總結(jié)一下:小說(shuō)每章的鏈接放在了class屬性為listmain的
標(biāo)簽下的標(biāo)簽中。鏈接具體位置放在html->body->div->dl->dd->a的href屬性中。先匹配class屬性為listmain的
# -*- coding:UTF-8 -*-
from bs4 import BeautifulSoup
import requests
if __name__ == "__main__":
target = 'http://www.biqukan.com/1_1094/'
req = requests.get(url = target)
html = req.text
div_bf = BeautifulSoup(html)
div = div_bf.find_all('div', class_ = 'listmain')
print(div[0])
還是使用find_all方法,運(yùn)行結(jié)果如下:
很順利,接下來(lái)再匹配每一個(gè)標(biāo)簽,并提取章節(jié)名和章節(jié)文章。如果我們使用Beautiful Soup匹配到了下面這個(gè)標(biāo)簽,如何提取它的href屬性和標(biāo)簽里存放的章節(jié)名呢?
方法很簡(jiǎn)單,對(duì)Beautiful Soup返回的匹配結(jié)果a,使用a.get('href')方法就能獲取href的屬性值,使用a.string就能獲取章節(jié)名,編寫代碼如下:
# -*- coding:UTF-8 -*-
from bs4 import BeautifulSoup
import requests
if __name__ == "__main__":
server = 'http://www.biqukan.com/'
target = 'http://www.biqukan.com/1_1094/'
req = requests.get(url = target)
html = req.text
div_bf = BeautifulSoup(html)
div = div_bf.find_all('div', class_ = 'listmain')
a_bf = BeautifulSoup(str(div[0]))
a = a_bf.find_all('a')
for each in a:
print(each.string, server + each.get('href'))
因?yàn)閒ind_all返回的是一個(gè)列表,里邊存放了很多的標(biāo)簽,所以使用for循環(huán)遍歷每個(gè)標(biāo)簽并打印出來(lái),運(yùn)行結(jié)果如下。
最上面匹配的一千多章的內(nèi)容是最新更新的12章節(jié)的鏈接。這12章內(nèi)容會(huì)和下面的重復(fù),所以我們要濾除,除此之外,還有那3個(gè)外傳,我們也不想要。這些都簡(jiǎn)單地剔除就好。
(3)整合代碼
每個(gè)章節(jié)的鏈接、章節(jié)名、章節(jié)內(nèi)容都有了。接下來(lái)就是整合代碼,將獲得內(nèi)容寫入文本文件存儲(chǔ)就好了。編寫代碼如下:
# -*- coding:UTF-8 -*-
from bs4 import BeautifulSoup
import requests, sys
"""
類說(shuō)明:下載《筆趣看》網(wǎng)小說(shuō)《一念永恒》
Parameters:
無(wú)
Returns:
無(wú)
Modify:
2017-09-13
"""
class downloader(object):
def __init__(self):
self.server = 'http://www.biqukan.com/'
self.target = 'http://www.biqukan.com/1_1094/'
self.names = [] ? ? ? ? #存放章節(jié)名
self.urls = [] ? ? ? ? ?#存放章節(jié)鏈接
self.nums = 0 ? ? ? ? ? #章節(jié)數(shù)
"""
函數(shù)說(shuō)明:獲取下載鏈接
Parameters:
無(wú)
Returns:
無(wú)
Modify:
2017-09-13
"""
def get_download_url(self):
req = requests.get(url = self.target)
html = req.text
div_bf = BeautifulSoup(html)
div = div_bf.find_all('div', class_ = 'listmain')
a_bf = BeautifulSoup(str(div[0]))
a = a_bf.find_all('a')
self.nums = len(a[15:]) #剔除不必要的章節(jié),并統(tǒng)計(jì)章節(jié)數(shù)
for each in a[15:]:
self.names.append(each.string)
self.urls.append(self.server + each.get('href'))
"""
函數(shù)說(shuō)明:獲取章節(jié)內(nèi)容
Parameters:
target - 下載連接(string)
Returns:
texts - 章節(jié)內(nèi)容(string)
Modify:
2017-09-13
"""
def get_contents(self, target):
req = requests.get(url = target)
html = req.text
bf = BeautifulSoup(html)
texts = bf.find_all('div', class_ = 'showtxt')
texts = texts[0].text.replace('\xa0'*8,'\n\n')
return texts
"""
函數(shù)說(shuō)明:將爬取的文章內(nèi)容寫入文件
Parameters:
name - 章節(jié)名稱(string)
path - 當(dāng)前路徑下,小說(shuō)保存名稱(string)
text - 章節(jié)內(nèi)容(string)
Returns:
無(wú)
Modify:
2017-09-13
"""
def writer(self, name, path, text):
write_flag = True
with open(path, 'a', encoding='utf-8') as f:
f.write(name + '\n')
f.writelines(text)
f.write('\n\n')
if __name__ == "__main__":
dl = downloader()
dl.get_download_url()
print('《一年永恒》開(kāi)始下載:')
for i in range(dl.nums):
dl.writer(dl.names[i], '一念永恒.txt', dl.get_contents(dl.urls[i]))
sys.stdout.write(" ?已下載:%.3f%%" % ?float(i/dl.nums) + '\r')
sys.stdout.flush()
print('《一年永恒》下載完成')
很簡(jiǎn)單的程序,單進(jìn)程跑,沒(méi)有開(kāi)進(jìn)程池。下載速度略慢,喝杯茶休息休息吧。代碼運(yùn)行效果如下圖所示:
2. 優(yōu)美壁紙下載
(1)實(shí)戰(zhàn)背景
已經(jīng)會(huì)爬取文字了,是不是感覺(jué)爬蟲(chóng)還是蠻好玩的呢?接下來(lái),讓我們進(jìn)行一個(gè)進(jìn)階實(shí)戰(zhàn),了解一下反爬蟲(chóng)。URL:https://unsplash.com/
看一看這些優(yōu)美的壁紙,這個(gè)網(wǎng)站的名字叫做Unsplash,免費(fèi)高清壁紙分享網(wǎng)是一個(gè)堅(jiān)持每天分享高清的攝影圖片的站點(diǎn),每天更新一張高質(zhì)量的圖片素材,全是生活中的景象作品,清新的生活氣息圖片可以作為桌面壁紙也可以應(yīng)用于各種需要的環(huán)境。
看到這么優(yōu)美的圖片,我的第一反應(yīng)就是想收藏一些,作為知乎文章的題圖再好不過(guò)了。每張圖片我都很喜歡,批量下載吧,不多爬,就下載50張好了。
(2)實(shí)戰(zhàn)進(jìn)階
我們已經(jīng)知道了每個(gè)html標(biāo)簽都有各自的功能。標(biāo)簽存放一下超鏈接,圖片存放在哪個(gè)標(biāo)簽里呢?html規(guī)定,圖片統(tǒng)統(tǒng)給我放到
標(biāo)簽中!既然這樣,我們截取就Unsplash網(wǎng)站中的一個(gè)
need-to-insert-img
標(biāo)簽,分析一下:
need-to-insert-img
可以看到,
標(biāo)簽有很多屬性,有alt、src、class、style屬性,其中src屬性存放的就是我們需要的圖片保存地址,我們根據(jù)這個(gè)地址就可以進(jìn)行圖片的下載。
need-to-insert-img
那么,讓我們先捋一捋這個(gè)過(guò)程:
使用requeusts獲取整個(gè)網(wǎng)頁(yè)的HTML信息;
使用Beautiful Soup解析HTML信息,找到所有
標(biāo)簽,提取src屬性,獲取圖片存放地址;
need-to-insert-img
根據(jù)圖片存放地址,下載圖片。
我們信心滿滿地按照這個(gè)思路爬取Unsplash試一試,編寫代碼如下:
# -*- coding:UTF-8 -*-
import requests
if __name__ == '__main__':
target = 'https://unsplash.com/'
req = requests.get(url=target)
print(req.text)
按照我們的設(shè)想,我們應(yīng)該能找到很多
標(biāo)簽。但是我們發(fā)現(xiàn),除了一些
need-to-insert-img
答案就是,這個(gè)網(wǎng)站的所有圖片都是動(dòng)態(tài)加載的!網(wǎng)站有靜態(tài)網(wǎng)站和動(dòng)態(tài)網(wǎng)站之分,上一個(gè)實(shí)戰(zhàn)爬取的網(wǎng)站是靜態(tài)網(wǎng)站,而這個(gè)網(wǎng)站是動(dòng)態(tài)網(wǎng)站,動(dòng)態(tài)加載有一部分的目的就是為了反爬蟲(chóng)。
對(duì)于什么是動(dòng)態(tài)加載,你可以這樣理解:
我們知道化妝術(shù)學(xué)的好,賊厲害,可以改變一個(gè)人的容貌。相應(yīng)的,動(dòng)態(tài)加載用的好,也賊厲害,可以改變一個(gè)網(wǎng)站的容貌。
動(dòng)態(tài)網(wǎng)站使用動(dòng)態(tài)加載常用的手段就是通過(guò)調(diào)用JavaScript來(lái)實(shí)現(xiàn)的。怎么實(shí)現(xiàn)JavaScript動(dòng)態(tài)加載,我們不必深究,我們只要知道,動(dòng)態(tài)加載的JavaScript腳本,就像化妝術(shù)需要用的化妝品,五花八門。有粉底、口紅、睫毛膏等等,它們都有各自的用途。動(dòng)態(tài)加載的JavaScript腳本也一樣,一個(gè)動(dòng)態(tài)加載的網(wǎng)站可能使用很多JavaScript腳本,我們只要找到負(fù)責(zé)動(dòng)態(tài)加載圖片的JavaScript腳本,不就找到我們需要的鏈接了嗎?
對(duì)于初學(xué)者,我們不必看懂JavaScript執(zhí)行的內(nèi)容是什么,做了哪些事情,因?yàn)槲覀冇袕?qiáng)大的抓包工具,它自然會(huì)幫我們分析。這個(gè)強(qiáng)大的抓包工具就是Fiddler。URL:http://www.telerik.com/fiddler
PS:也可以使用瀏覽器自帶的Networks,但是我更推薦這個(gè)軟件,因?yàn)樗僮髌饋?lái)更高效。
安裝方法很簡(jiǎn)單,傻瓜式安裝,一直下一步即可,對(duì)于經(jīng)常使用電腦的人來(lái)說(shuō),應(yīng)該沒(méi)有任何難度。
這個(gè)軟件的使用方法也很簡(jiǎn)單,打開(kāi)軟件,然后用瀏覽器打開(kāi)我們的目標(biāo)網(wǎng)站,以Unsplash為例,抓包結(jié)果如下:
我們可以看到,上圖左側(cè)紅框處是我們的GET請(qǐng)求的地址,就是網(wǎng)站的URL,右下角是服務(wù)器返回的信息,我們可以看到,這些信息也是我們上一個(gè)程序獲得的信息。這個(gè)不是我們需要的鏈接,我們繼續(xù)往下看。
我們發(fā)現(xiàn)上圖所示的就是一個(gè)JavaScript請(qǐng)求,看右下側(cè)服務(wù)器返回的信息是一個(gè)json格式的數(shù)據(jù)。這里面,就有我們需要的內(nèi)容。我們局部放大看一下:
這是Fiddler右側(cè)的信息,上面是請(qǐng)求的Headers信息,包括這個(gè)Javascript的請(qǐng)求地 址:http://unsplash.com/napi/feeds/home,其他信息我們先不管,我們看看下面的內(nèi)容。里面有很多圖片的信息,包括圖片的id,圖片的大小,圖片的鏈接,還有下一頁(yè)的地址。這個(gè)腳本以json格式存儲(chǔ)傳輸?shù)臄?shù)據(jù),json格式是一種輕量級(jí)的數(shù)據(jù)交換格式,起到封裝數(shù)據(jù)的作用,易于人閱讀和編寫,同時(shí)也易于機(jī)器解析和生成。這么多鏈接,可以看到圖片的鏈接有很多,根據(jù)哪個(gè)鏈接下載圖片呢?先別急,讓我們繼續(xù)分析:
在這個(gè)網(wǎng)站,我們可以按這個(gè)按鈕進(jìn)行圖片下載。我們抓包分下下這個(gè)動(dòng)作,看看發(fā)送了哪些請(qǐng)求。
https://unsplash.com/photos/1PrQ2mHW-Fo/download?force=true
https://unsplash.com/photos/JX7nDtafBcU/download?force=true
https://unsplash.com/photos/HCVbP3zqX4k/download?force=true
通過(guò)Fiddler抓包,我們發(fā)現(xiàn),點(diǎn)擊不同圖片的下載按鈕,GET請(qǐng)求的地址都是不同的。但是它們很有規(guī)律,就是中間有一段代碼是不一樣的,其他地方都一樣。中間那段代碼是不是很熟悉?沒(méi)錯(cuò),它就是我們之前抓包分析得到j(luò)son數(shù)據(jù)中的照片的id。我們只要解析出每個(gè)照片的id,就可以獲得圖片下載的請(qǐng)求地址,然后根據(jù)這個(gè)請(qǐng)求地址,我們就可以下載圖片了。那么,現(xiàn)在的首要任務(wù)就是解析json數(shù)據(jù)了。
json格式的數(shù)據(jù)也是分層的。可以看到next_page里存放的是下一頁(yè)的請(qǐng)求地址,很顯然Unsplash下一頁(yè)的內(nèi)容,也是動(dòng)態(tài)加載的。在photos下面的id里,存放著圖片的id,這個(gè)就是我們需要獲得的圖片id號(hào)。
怎么編程提取這些json數(shù)據(jù)呢?我們也是分步完成:
獲取整個(gè)json數(shù)據(jù)
解析json數(shù)據(jù)
編寫代碼,嘗試獲取json數(shù)據(jù):
# -*- coding:UTF-8 -*-
import requests
if __name__ == '__main__':
target = 'http://unsplash.com/napi/feeds/home'
req = requests.get(url=target)
print(req.text)
很遺憾,程序報(bào)錯(cuò)了,問(wèn)題出在哪里?通過(guò)錯(cuò)誤信息,我們可以看到SSL認(rèn)證錯(cuò)誤,SSL認(rèn)證是指客戶端到服務(wù)器端的認(rèn)證。一個(gè)非常簡(jiǎn)單的解決這個(gè)認(rèn)證錯(cuò)誤的方法就是設(shè)置requests.get()方法的verify參數(shù)。這個(gè)參數(shù)默認(rèn)設(shè)置為True,也就是執(zhí)行認(rèn)證。我們將其設(shè)置為False,繞過(guò)認(rèn)證不就可以了?
有想法就要嘗試,編寫代碼如下:
# -*- coding:UTF-8 -*-
import requests
if __name__ == '__main__':
target = 'http://unsplash.com/napi/feeds/home'
req = requests.get(url=target, verify=False)
print(req.text)
認(rèn)證問(wèn)題解決了,又有新問(wèn)題了:
可以看到,我們GET請(qǐng)求又失敗了,這是為什么?這個(gè)網(wǎng)站反爬蟲(chóng)的手段除了動(dòng)態(tài)加載,還有一個(gè)反爬蟲(chóng)手段,那就是驗(yàn)證Request Headers。接下來(lái),讓我們分析下這個(gè)Requests Headers:
我截取了Fiddler的抓包信息,可以看到Requests Headers里又很多參數(shù),有Accept、Accept-Encoding、Accept-Language、DPR、User-Agent、Viewport-Width、accept-version、Referer、x-unsplash-client、authorization、Connection、Host。它們都是什么意思呢?
專業(yè)的解釋能說(shuō)的太多,我挑重點(diǎn):
User-Agent:這里面存放瀏覽器的信息。可以看到上圖的參數(shù)值,它表示我是通過(guò)Windows的Chrome瀏覽器,訪問(wèn)的這個(gè)服務(wù)器。如果我們不設(shè)置這個(gè)參數(shù),用Python程序直接發(fā)送GET請(qǐng)求,服務(wù)器接受到的User-Agent信息就會(huì)是一個(gè)包含python字樣的User-Agent。如果后臺(tái)設(shè)計(jì)者驗(yàn)證這個(gè)User-Agent參數(shù)是否合法,不讓帶Python字樣的User-Agent訪問(wèn),這樣就起到了反爬蟲(chóng)的作用。這是一個(gè)最簡(jiǎn)單的,最常用的反爬蟲(chóng)手段。
Referer:這個(gè)參數(shù)也可以用于反爬蟲(chóng),它表示這個(gè)請(qǐng)求是從哪發(fā)出的。可以看到我們通過(guò)瀏覽器訪問(wèn)網(wǎng)站,這個(gè)請(qǐng)求是從https://unsplash.com/,這個(gè)地址發(fā)出的。如果后臺(tái)設(shè)計(jì)者,驗(yàn)證這個(gè)參數(shù),對(duì)于不是從這個(gè)地址跳轉(zhuǎn)過(guò)來(lái)的請(qǐng)求一律禁止訪問(wèn),這樣就也起到了反爬蟲(chóng)的作用。
authorization:這個(gè)參數(shù)是基于AAA模型中的身份驗(yàn)證信息允許訪問(wèn)一種資源的行為。在我們用瀏覽器訪問(wèn)的時(shí)候,服務(wù)器會(huì)為訪問(wèn)者分配這個(gè)用戶ID。如果后臺(tái)設(shè)計(jì)者,驗(yàn)證這個(gè)參數(shù),對(duì)于沒(méi)有用戶ID的請(qǐng)求一律禁止訪問(wèn),這樣就又起到了反爬蟲(chóng)的作用。
Unsplash是根據(jù)哪個(gè)參數(shù)反爬蟲(chóng)的呢?根據(jù)我的測(cè)試,是authorization。我們只要通過(guò)程序手動(dòng)添加這個(gè)參數(shù),然后再發(fā)送GET請(qǐng)求,就可以順利訪問(wèn)了。怎么什么設(shè)置呢?還是requests.get()方法,我們只需要添加headers參數(shù)即可。編寫代碼如下:
# -*- coding:UTF-8 -*-
import requests
if __name__ == '__main__':
target = 'http://unsplash.com/napi/feeds/home'
headers = {'authorization':'your Client-ID'}
req = requests.get(url=target, headers=headers, verify=False)
print(req.text)
headers參數(shù)值是通過(guò)字典傳入的。記得將上述代碼中your Client-ID換成諸位自己抓包獲得的信息。代碼運(yùn)行結(jié)果如下:
皇天不負(fù)有心人,可以看到我們已經(jīng)順利獲得json數(shù)據(jù)了,里面有next_page和照片的id。接下來(lái)就是解析json數(shù)據(jù)。根據(jù)我們之前分析可知,next_page放在了json數(shù)據(jù)的最外側(cè),照片的id放在了photos->id里。我們使用json.load()方法解析數(shù)據(jù),編寫代碼如下:
# -*- coding:UTF-8 -*-
import requests, json
if __name__ == '__main__':
target = 'http://unsplash.com/napi/feeds/home'
headers = {'authorization':'your Client-ID'}
req = requests.get(url=target, headers=headers, verify=False)
html = json.loads(req.text)
next_page = html['next_page']
print('下一頁(yè)地址:',next_page)
for each in html['photos']:
print('圖片ID:',each['id'])
解析json數(shù)據(jù)很簡(jiǎn)單,跟字典操作一樣,就是字典套字典。json.load()里面的參數(shù)是原始的json格式的數(shù)據(jù)。程序運(yùn)行結(jié)果如下:
圖片的ID已經(jīng)獲得了,再通過(guò)字符串處理一下,就生成了我們需要的圖片下載請(qǐng)求地址。根據(jù)這個(gè)地址,我們就可以下載圖片了。下載方式,使用直接寫入文件的方法。
(3)整合代碼
每次獲取鏈接加一個(gè)1s延時(shí),因?yàn)槿嗽跒g覽頁(yè)面的時(shí)候,翻頁(yè)的動(dòng)作不可能太快。我們要讓我們的爬蟲(chóng)盡量友好一些。
# -*- coding:UTF-8 -*-
import requests, json, time, sys
from contextlib import closing
class get_photos(object):
def __init__(self):
self.photos_id = []
self.download_server = 'https://unsplash.com/photos/xxx/download?force=trues'
self.target = 'http://unsplash.com/napi/feeds/home'
self.headers = {'authorization':'your Client-ID'}
"""
函數(shù)說(shuō)明:獲取圖片ID
Parameters:
無(wú)
Returns:
無(wú)
Modify:
2017-09-13
"""
def get_ids(self):
req = requests.get(url=self.target, headers=self.headers, verify=False)
html = json.loads(req.text)
next_page = html['next_page']
for each in html['photos']:
self.photos_id.append(each['id'])
time.sleep(1)
for i in range(4):
req = requests.get(url=next_page, headers=self.headers, verify=False)
html = json.loads(req.text)
next_page = html['next_page']
for each in html['photos']:
self.photos_id.append(each['id'])
time.sleep(1)
"""
函數(shù)說(shuō)明:圖片下載
Parameters:
無(wú)
Returns:
無(wú)
Modify:
2017-09-13
"""
def download(self, photo_id, filename):
headers = {'User-Agent':'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36'}
target = self.download_server.replace('xxx', photo_id)
with closing(requests.get(url=target, stream=True, verify = False, headers = self.headers)) as r:
with open('%d.jpg' % filename, 'ab+') as f:
for chunk in r.iter_content(chunk_size = 1024):
if chunk:
f.write(chunk)
f.flush()
if __name__ == '__main__':
gp = get_photos()
print('獲取圖片連接中:')
gp.get_ids()
print('圖片下載中:')
for i in range(len(gp.photos_id)):
print(' ?正在下載第%d張圖片' % (i+1))
gp.download(gp.photos_id[i], (i+1))
下載速度還行,有的圖片下載慢是因?yàn)閳D片太大。可以看到右側(cè)也打印了一些警報(bào)信息,這是因?yàn)槲覀儧](méi)有進(jìn)行SSL驗(yàn)證。
學(xué)會(huì)了爬取圖片,簡(jiǎn)單的動(dòng)態(tài)加載的網(wǎng)站也難不倒你了。趕快試試國(guó)內(nèi)的一些圖片網(wǎng)站吧!
3. 愛(ài)奇藝VIP視頻下載
(1)實(shí)戰(zhàn)背景
愛(ài)奇藝的VIP視頻只有會(huì)員能看,普通用戶只能看前6分鐘。比如加勒比海盜5的URL:http://www.iqiyi.com/v_19rr7qhfg0.html#vfrm=19-9-0-1
我們?cè)趺疵赓M(fèi)看VIP視頻呢?一個(gè)簡(jiǎn)單的方法,就是通過(guò)旋風(fēng)視頻VIP解析網(wǎng)站。URL:http://api.xfsub.com/
這個(gè)網(wǎng)站為我們提供了免費(fèi)的視頻解析,它的通用解析方式是:
http://api.xfsub.com/index.php?url=[播放地址或視頻id]
比如,對(duì)于繡春刀這個(gè)電影,我們只需要在瀏覽器地址欄輸入:
http://api.xfsub.com/index.php?url=http://www.iqiyi.com/v_19rr7qhfg0.html#vfrm=19-9-0-1
這樣,我們就可以在線觀看這些VIP視頻了:
但是這個(gè)網(wǎng)站只提供了在線解析視頻的功能,沒(méi)有提供下載接口,如果想把視頻下載下來(lái),我們就可以利用網(wǎng)絡(luò)爬蟲(chóng)進(jìn)行抓包,將視頻下載下來(lái)。
(2)實(shí)戰(zhàn)升級(jí)
分析方法相同,我們使用Fiddler進(jìn)行抓包:
我們可以看到,有用的請(qǐng)求并不多,我們逐條分析。我們先看第一個(gè)請(qǐng)求返回的信息。
可以看到第一個(gè)請(qǐng)求是GET請(qǐng)求,沒(méi)有什么有用的信息,繼續(xù)看下一條。
我們看到,第二條GET請(qǐng)求地址變了,并且在返回的信息中,我們看到,這個(gè)網(wǎng)頁(yè)執(zhí)行了一個(gè)POST請(qǐng)求。POST請(qǐng)求是啥呢?它跟GET請(qǐng)求正好相反,GET是從服務(wù)器獲得數(shù)據(jù),而POST請(qǐng)求是向服務(wù)器發(fā)送數(shù)據(jù),服務(wù)器再根據(jù)POST請(qǐng)求的參數(shù),返回相應(yīng)的內(nèi)容。這個(gè)POST請(qǐng)求有四個(gè)參數(shù),分別為time、key、url、type。記住這個(gè)有用的信息,我們?cè)谧グY(jié)果中,找一下這個(gè)請(qǐng)求,看看這個(gè)POST請(qǐng)求做了什么。
很顯然,這個(gè)就是我們要找的POST請(qǐng)求,我們可以看到POST請(qǐng)求的參數(shù)以及返回的json格式的數(shù)據(jù)。其中url存放的參數(shù)如下:
xfsub_api\/url.php?key=02896e4af69fb18f70129b6046d7c718&time=1505724557&url=http%3A%2F%2Fwww.iqiyi.com%2Fv_19rr7qhfg0.html&type=&xml=1
這個(gè)信息有轉(zhuǎn)義了,但是沒(méi)有關(guān)系,我們手動(dòng)提取一下,變成如下形式:
xfsub_api/url.php?key=02896e4af69fb18f70129b6046d7c718&time=1505724557&url=http://www.iqiyi.com/v_19rr7qhfg0.html&type=&xml=1
我們已經(jīng)知道了這個(gè)解析視頻的服務(wù)器的域名,再把域名加上:
http://api.xfsub.com/xfsub_api\url.php?key=02896e4af69fb18f70129b6046d7c718&time=1505724557&url=http://www.iqiyi.com/v_19rr7qhfg0.html&type=&xml=1
這里面存放的是什么東西?不會(huì)視頻解析后的地址吧?我們有瀏覽器打開(kāi)這個(gè)地址看一下:
果然,我們可以看到視頻地址近在眼前啊,URL如下:
http://disp.titan.mgtv.com/vod.do?fmt=4&pno=1121&fid=1FEA2622E0BD9A1CA625FBE9B5A238A6&file=/c1/2017/09/06_0/1FEA2622E0BD9A1CA625FBE9B5A238A6_20170906_1_1_705.mp4
我們?cè)俅蜷_(kāi)這個(gè)視頻地址:
瞧,我們就這樣得到了這個(gè)視頻在服務(wù)器上的緩存地址。根據(jù)這個(gè)地址,我們就可以輕松下載視頻了。
PS:需要注意一點(diǎn),這些URL地址,都是有一定時(shí)效性的,很快就會(huì)失效,因?yàn)槔锩姘瑫r(shí)間信息。所以,各位在分析的時(shí)候,要根據(jù)自己的URL結(jié)果打開(kāi)網(wǎng)站才能看到視頻。
接下來(lái),我們的任務(wù)就是編程實(shí)現(xiàn)我們所分析的步驟,根據(jù)不同的視頻播放地址獲得視頻存放的地址。
現(xiàn)在梳理一下編程思路:
用正則表達(dá)式匹配到key、time、url等信息。
根據(jù)匹配的到信息發(fā)POST請(qǐng)求,獲得一個(gè)存放視頻信息的url。
根據(jù)這個(gè)url獲得視頻存放的地址。
根據(jù)最終的視頻地址,下載視頻。
(3)編寫代碼
編寫代碼的時(shí)候注意一個(gè)問(wèn)題,就是我們需要使用requests.session()保持我們的會(huì)話請(qǐng)求。簡(jiǎn)單理解就是,在初次訪問(wèn)服務(wù)器的時(shí)候,服務(wù)器會(huì)給你分配一個(gè)身份證明。我們需要拿著這個(gè)身份證去繼續(xù)訪問(wèn),如果沒(méi)有這個(gè)身份證明,服務(wù)器就不會(huì)再讓你訪問(wèn)。這也就是這個(gè)服務(wù)器的反爬蟲(chóng)手段,會(huì)驗(yàn)證用戶的身份。
思路已經(jīng)給出,希望喜歡爬蟲(chóng)的人可以在運(yùn)行下代碼之后,自己重頭編寫程序,因?yàn)橹挥薪?jīng)過(guò)自己分析和測(cè)試之后,才能真正明白這些代碼的意義。上述代碼運(yùn)行結(jié)果如下:
我們已經(jīng)順利獲得了mp4這個(gè)視頻文件地址。根據(jù)視頻地址,使用 urllib.request.urlretrieve() 即可將視頻下載下來(lái)。編寫代碼如下:
urlretrieve()有三個(gè)參數(shù),第一個(gè)url參數(shù)是視頻存放的地址,第二個(gè)參數(shù)filename是保存的文件名,最后一個(gè)是回調(diào)函數(shù),它方便我們查看下載進(jìn)度。代碼量不大,很簡(jiǎn)單,主要在于分析過(guò)程。代碼運(yùn)行結(jié)果如下:
下載速度挺快的,幾分鐘視頻下載好了。
對(duì)于這個(gè)程序,感興趣的朋友可以進(jìn)行擴(kuò)展一下,設(shè)計(jì)出一個(gè)小軟件,根據(jù)用戶提供的url,提供PC在線觀看、手機(jī)在線觀看、視頻下載等功能。
四、總結(jié)
爬蟲(chóng)時(shí)效性低,同樣的思路過(guò)了一個(gè)月,甚至一周可能無(wú)法使用,但是爬取思路都是如此,完全可以自行分析。
本次實(shí)戰(zhàn)代碼,均已上傳我的Github,歡迎Follow、Star:
https://github.com/Jack-Cherish/python-spider
如有問(wèn)題,請(qǐng)留言。如有錯(cuò)誤,還望指正,謝謝!