引言:
關於爬小姐姐的指令碼示例,在我的Gayhub倉庫:ReptileSomething
裡已經有好幾個了,基本都是沒什麼技術含量的,直接解析HTML拿到
圖片的URL,然後下載,特別開一篇寫爬取花瓣網的小姐姐的實戰教程,
是因為爬這個網站的時候會遇到好幾個問題,第一感受到了反爬蟲的套路,
(折騰了我將近2天):
- 1.圖片是瀑布流佈局,通過Ajax動態載入資料的
- 2.在處理圖片詳情頁的時候才發現了圖片連結規則,前面做
了很多無謂的操作; - 3.最後獲得了圖片的正確url,但是根本下載不下來,不知道
是做了防盜鏈還是什麼?或者要登入之類的,瀏覽器開啟也無法下載,
開啟超連結是這樣的內容,但是當你右鍵儲存的時候發現並不能下載:
不信的話可以試試。
img.hb.aicdn.com/36b521f7177…
我覺得算是爬圖片裡稍微有點難度的站點了,強烈建議跟著我
一起回顧這個過程!
1.問題初現:瀑布流和Ajax動態載入資料
Chrome抓包的時候,抓到的資料和Elements的內容不一樣,
js動態載入資料,前面已經見識過這種反爬蟲的套路了,
有Selenium在手,根本不虛,模擬一波瀏覽器請求
載入下就能得到和Elements一樣的內容了。
兩個問題:瀑布流和Ajax動態載入資料。
怎麼說?且聽我一一道來:
沒事喜歡練手的我意外發現了花瓣網,F12 Chrome抓一波包:
隨手寫個程式碼看看:
先Elements看下我們想扒的是什麼:
這裡儘管有個img,但是明顯是個小圖,應該是要點開a那個
/pins/1433175317連結裡才有大圖,點開:
huaban.com/pins/143317…
看下Elements,這個就是我們想要的圖片url:
恩,一如既往的簡單套路,搞到批量的列表url,然後下載圖片。
看回我們的利用Selenium得到的網頁程式碼,可以很穩。
接下來的事情本該就水到渠成的了,然後這時候發生了一件
令人猝不及防的事情:
在網頁那裡滾到底部發現會載入更多的圖片,越滾越多,但是
我們的Selenium只抓到了30個,咦,這種在之前抓某個網站
的時候就遇到過了,寫個簡單的滾動到底部的js,然後Selenium
迴圈執行這個指令碼圖片數/30次就好了,中途可以休眠1s給他載入,
執行完畢後,再去呼叫page_source得到最終的頁面程式碼,然後走波
BeautifulSoup把我們1000多個小姐姐扒出來就好。
但是實際執行的結果卻出乎我們的意料,最後得到的小姐姐列表
還是30個,臥槽,什麼鬼,接著開啟我們的瀏覽器,滾動的時候
發現,列表內容竟然是動態變化的,開啟圖片列表節點,滾動網
頁,不禁又發出一句,臥槽,什麼鬼,列表是動態變化的???
這些剛還不在的,突然就蹦出來了,就好像你刷著即刻刷著
刷著就蹦出一個x子。動態載入?想想野路子,要不我們自己
量化下滾動偏移量,比如滾動100,我們抓一波頁面,存一下,
最後做下去重?這野路子不是一般的野:
單不說怎麼量化這個偏移量了,瀏覽器寬度不一樣時載入的
數目還不一定是30,然後那麼多畫板,你每個畫板這樣玩?
效率巨低。
苦苦尋覓後,發現了兩個關鍵詞:瀑布流和Ajax動態載入資料
2.解決問題
瀑布流:參差不齊的多欄佈局以及到達底部自動載入的方式
Ajax動態載入資料:在不重新載入整個網頁的情況下,對網頁的某
部分進行更新
簡單點說就是:
圖片通過JS載入成瀑布流的形式,當到達底部後,會請求後臺拿到更多
的資料,解析後通過Ajax,可以在不關閉不轉跳不重新整理瀏覽器的情況下
部分更新頁面內容。
So,我們我們重新抓抓包,在滾動到底部的時候看下抓到的資料,
點選篩選XHR(XMLHttpRequest),這個是瀏覽器後臺與服務之間
交換資料的檔案,一般為json格式:
點開,右側看看Headers,果然是json格式的:
發現有這樣的請求頭,先放著,等下再研究規律:
點開右側Previews,發現了傳回來的一大串Json,這裡的
pins應該就是新增加的妹子圖的相關資料了。
3.問題再現:猜測與試驗,一步步解密規則
為了方便研究,我又滾動了幾下,載入更多的資料,以方便
找出規律:
1.發起請求的規則
從上面我們可以得到一些這樣的資訊:
首先是Get請求,固定的基地址:http://huaban.com/boards/18907029/
這個18907029是畫板id,後面的引數,jcx1ki7y和wfl=1應該是固定的,
limit這個是載入數量,一般是分頁用的,就是一次載入20條新資料,
最後這個max:1348131400 暫時不知道是什麼?不過應該是某個什麼id
吧,看下第二個的max是1263961742,複製到第一個返回的json裡搜搜:
臥槽,剛好最後一個,不會那麼巧吧?然後把第三個max:1247629077複製
到第二個的Json裡看看:
果然,好傢伙,這個max就是每次拿到的最後一個圖片的pin_id
知道規則了,模擬一波請求,解析一波json,每次拿到最後這個
pin_id,用作下次請求,當返回的pins裡沒東西,說明已經載入
完所有的了,來,寫一波程式碼,先要處理剛進來時載入的列表,
得到最後的一個圖的pin_id,然後才能開始執行上面那個拿
json的操作。
在我準備用Selenium模擬請求主頁的時候,我發現了用urllib
模擬請求,裡面就能拿到最後一個pin_id,只不過他是寫在js裡
的,我們可以通過正規表示式拿到我們想要的pin_id們:
還有點開一個具體的大圖頁,發現他的url規則竟然是:
huaban.com/pins/926502…
就是http://huaban.com/pins/ + pin_id,所以我們只要獲得pin_id就可以了!
2.一步步寫程式碼
規則清楚了,接下來一步步寫程式碼來獲取我們想要的資料吧!
1)首頁資料的獲取
進來的時候會載入一次,不是通過Ajax載入,預設是30個,需要處理
一波網頁獲得這個30個資料,然後30個資料的最後一個用於請求Ajax。
通過正則拿到pins這段json。
程式碼如下:
測試下程式碼:
開啟網頁載入更多確認下這個最後的pid是否正確:
可以正確,接著就來處理json資料了~
(這要注意正則匹配用的是search,我一開始沒留意用的是match,
用一些真這個校驗工具測試自己的正則一直是對的,但是丟程式
裡缺一直不匹配,返回None,要注意!!!)
2)處理Ajax資料
首先是請求頭的設定,和我們普通的請求不一樣,如果你直接
用瀏覽器開啟ajax載入資料的那個url發現返回的並不是json!!!
隨手找個連結試試,就是解析json而已,
簡單測試下:
執行結果:
可以,拿到資料了,接下來要優化下點東西,如果每次都要去拼接:
這串東西不就很麻煩了,可以通過正則來進行替換:
這裡用到前面沒有細講的re.sub()替換方法和向前和向後界定
這兩個東西什麼時候用到,當前這個場景就能用到,比如我們想替換
pin_id,用一個括號括著想替換的部分,感覺應該就能替換了:
結果:
是的,前面一大段東西沒了,如果你用了向前(?<=…)和向後界定(?=),
就可以讓正則只匹配和替換這兩個中間部分的字串了~
好的,替換成功,小小整理一下程式碼:
好的,pin_id都能夠拿到了,接下來通過這些想辦法拿到圖片啦,
點開一個圖片的詳情頁,比如:
檢視頁面結構,得到圖片url:
然後寫個簡單的模擬請求下這個網址,看下返回的資料有沒有這個圖片Url相匹配
的內容,全部搜沒有,然後把後面的分成三段,一段段搜:
先搜:36b521f717741a4e3e024fd29606f61b8f960318f3763,秒找到,
有六處匹配的,這後面跟著-WzUoLC,就剩下最後的fw658,全域性搜搜不到
難道是固定的?開啟另一個詳情頁看看,果然,fw658是固定的:
臥槽,key???前面拿json的資料就能拿到key,臥槽,該不會就
這樣拼接就可以了吧,找到前面一個:
…令人無法接受,折騰了那麼舊,原來拿到key就可以拼接得出圖片url了
3.問題還現:我還是太naive了
嘖嘖,正確的圖片url拿到了,瀏覽器開啟也是沒問題的,接著就是無腦
拼url,然後一個個下載了,正當我以為一切已經結束的時候,才發現了
最後的隱藏關卡:”圖片並不能下載“???右鍵另存為儲存直接失敗,
py程式碼直接崩潰。
然後,在那個圖片的詳情頁倒是可以下載,我的天,難不成要我每個
圖片都用Selenium載入,然後處理頁面資料,在這裡下?
講真,我是非常抗拒用Selenium的,慢不說,還耗記憶體,
代理也不怎麼好設定,而且別人會說我low,百度谷歌搜了
一大堆,基本都是說了等於沒說…正在我萬念俱灰,想用回
Selenium這種Low比方式的時候,我萌生了一個想法:
會不會是需要登入後才能下載,於是乎我把連結發給我組
的UI,然後她預設瀏覽器竟然是ie,然後奇蹟發現了,沒有
登入,直接用ie瀏覽器開啟了,然後他麼的,可以右鍵儲存
到本地?接著我又把連結發給我隔壁的後臺小哥,同樣用ie
開啟,可以。此時熟悉的BGM響起:
真相:花瓣沒有做ie系列瀏覽器的相容
所以,模擬ie瀏覽器的User-Agent就可以下載圖片了!!!
`User-Agent`: `Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0)`
複製程式碼
立馬試試,當看到第一張圖片下載到了我的倉庫裡的時候,
我就知道我猜對了:
剩下的就是組織一波程式碼,批量下載了~
4.完整程式碼
import urllib.request
import urllib.error
import re
import json
import coderpig
import os
# 圖片拼接url後,分別是字首字尾
img_start_url = `http://img.hb.aicdn.com/`
img_end = `_fw658`
# 獲取pins的正則
boards_pattern = re.compile(r`pins":(.*)};`)
# 修改pin_id的正則
max_pattern = re.compile(r`(?<=max=)d*(?=&limit)`)
# 圖片輸出檔案
pin_ids_file = `pin_ids.txt`
# 圖片輸出路徑
pic_download_dir = `output/Picture/HuaBan/`
json_headers = {
`Host`: `huaban.com`,
`Accept`: `application/json`,
`X-Request`: `JSON`,
`X-Requested-With`: `XMLHttpRequest`
}
# 獲得borads頁資料,提取key列表寫入到檔案裡,並返回最後一個pid用於後續查詢
def get_boards_index_data(url):
print(url)
proxy_ip = coderpig.get_proxy_ip()
resp = coderpig.get_resp(url, proxy=proxy_ip).decode(`utf-8`)
result = boards_pattern.search(resp)
json_dict = json.loads(result.group(1))
for item in json_dict:
coderpig.write_str_data(item[`file`][`key`], pin_ids_file)
# 返回最後一個pin_id
pin_id = json_dict[-1][`pin_id`]
return pin_id
# 模擬Ajax請求更多資料
def get_json_list(url):
proxy_ip = coderpig.get_proxy_ip()
print("獲取json:" + url)
resp = coderpig.get_resp(url, headers=json_headers, proxy=proxy_ip).decode(`utf-8`)
if resp is None:
return None
else:
json_dict = json.loads(resp)
pins = json_dict[`board`][`pins`]
if len(pins) == 0:
return None
else:
for item in pins:
coderpig.write_str_data(item[`file`][`key`], pin_ids_file)
return pins[-1][`pin_id`]
# 下載圖片的方法
def download_pic(key):
proxy_ip = coderpig.get_proxy_ip()
coderpig.is_dir_existed(pic_download_dir)
url = img_start_url + key + img_end
resp = coderpig.get_resp(url, proxy=proxy_ip, ie_header=True)
try:
print("下載圖片:" + url)
pic_name = key + ".jpg"
with open(pic_download_dir + pic_name, "wb+") as f:
f.write(resp)
except (OSError, urllib.error.HTTPError, urllib.error.URLError, Exception) as reason:
print(str(reason))
if __name__ == `__main__`:
coderpig.init_https()
if os.path.exists(pin_ids_file):
os.remove(pin_ids_file)
# 一個畫板連結,可自行替換
boards_url = `http://huaban.com/boards/27399228/`
board_last_pin_id = get_boards_index_data(boards_url)
board_json_url = boards_url + `?jcx38c3h&max=354569642&limit=20&wfl=1`
while True:
board_last_pin_id = get_json_list(max_pattern.sub(str(board_last_pin_id), board_json_url))
if board_last_pin_id is None:
break
pic_url_list = coderpig.load_data(pin_ids_file)
for key in pic_url_list:
download_pic(key)
print("下載完成~")
複製程式碼
輸出結果:
參見吾王~
5.小結:
磕磕碰碰,總算是把程式碼給擼出來了,成功又收穫了一大波小姐姐,
爬蟲技能點+1,建議還是少用無腦Selenium吧,另外剛發現,
Chrome直接支援右鍵匯出XPath,就不用自己慢慢扣了(如果你用lxml的話)。
好的,就說那麼多~
====== 修正 ======
- 關於圖片無法下載的問題,留言區 China卷柏 告知了原因:
請求頭裡的Referer,Chrome下載的時候自動填寫為當前頁面的URL
所以只需在請求頭裡設定Referer為空或者 http://huaban.com/
就可以了,實測可以,gayhub程式碼已更新,感謝~
閱讀中遇到問題或者有寫錯的,歡迎提出一起討論!
本節原始碼下載:
來啊,Py交易啊
想加群一起學習Py的可以加下,智障機器人小Pig,驗證資訊裡包含:
Python,python,py,Py,加群,交易,屁眼 中的一個關鍵詞即可通過;
驗證通過後回覆 加群 即可獲得加群連結(不要把機器人玩壞了!!!)~~~
歡迎各種像我一樣的Py初學者,Py大神加入,一起愉快地交流學♂習,van♂轉py。