小豬的Python學(xué)習(xí)之旅 —— 9.爬蟲(chóng)實(shí)戰(zhàn):爬取花瓣網(wǎng)的小姐姐

引言

關(guān)于爬小姐姐的腳本示例,在我的Gayhub倉(cāng)庫(kù):ReptileSomething
里已經(jīng)有好幾個(gè)了,基本都是沒(méi)什么技術(shù)含量的,直接解析HTML拿到
圖片的URL,然后下載,特別開(kāi)一篇寫爬取花瓣網(wǎng)的小姐姐的實(shí)戰(zhàn)教程,
是因?yàn)榕肋@個(gè)網(wǎng)站的時(shí)候會(huì)遇到好幾個(gè)問(wèn)題,第一感受到了反爬蟲(chóng)的套路,
(折騰了我將近2天):

  • 1.圖片是瀑布流布局,通過(guò)Ajax動(dòng)態(tài)加載數(shù)據(jù)的
  • 2.在處理圖片詳情頁(yè)的時(shí)候才發(fā)現(xiàn)了圖片鏈接規(guī)則,前面做
    了很多無(wú)謂的操作;
  • 3.最后獲得了圖片的正確url,但是根本下載不下來(lái),不知道
    是做了防盜鏈還是什么?或者要登錄之類的,瀏覽器打開(kāi)也無(wú)法下載,
    打開(kāi)超鏈接是這樣的內(nèi)容,但是當(dāng)你右鍵保存的時(shí)候發(fā)現(xiàn)并不能下載:

不信的話可以試試。
http://img.hb.aicdn.com/36b521f717741a4e3e024fd29606f61b8f960318f3763-WzUoLC_fw658

我覺(jué)得算是爬圖片里稍微有點(diǎn)難度的站點(diǎn)了,強(qiáng)烈建議跟著我
一起回顧這個(gè)過(guò)程!


1.問(wèn)題初現(xiàn):瀑布流和Ajax動(dòng)態(tài)加載數(shù)據(jù)

Chrome抓包的時(shí)候,抓到的數(shù)據(jù)和Elements的內(nèi)容不一樣,

js動(dòng)態(tài)加載數(shù)據(jù),前面已經(jīng)見(jiàn)識(shí)過(guò)這種反爬蟲(chóng)的套路了,
Selenium在手,根本不虛,模擬一波瀏覽器請(qǐng)求
加載下就能得到和Elements一樣的內(nèi)容了。


兩個(gè)問(wèn)題:瀑布流和Ajax動(dòng)態(tài)加載數(shù)據(jù)
怎么說(shuō)?且聽(tīng)我一一道來(lái):

沒(méi)事喜歡練手的我意外發(fā)現(xiàn)了花瓣網(wǎng),F(xiàn)12 Chrome抓一波包:

隨手寫個(gè)代碼看看:

Elements看下我們想扒的是什么:

這里盡管有個(gè)img,但是明顯是個(gè)小圖,應(yīng)該是要點(diǎn)開(kāi)a那個(gè)
/pins/1433175317鏈接里才有大圖,點(diǎn)開(kāi):
http://huaban.com/pins/1433175317/

看下Elements,這個(gè)就是我們想要的圖片url:

恩,一如既往的簡(jiǎn)單套路,搞到批量的列表url,然后下載圖片。
看回我們的利用Selenium得到的網(wǎng)頁(yè)代碼,可以很穩(wěn)。

接下來(lái)的事情本該就水到渠成的了,然后這時(shí)候發(fā)生了一件
令人猝不及防的事情:

在網(wǎng)頁(yè)那里滾到底部發(fā)現(xiàn)會(huì)加載更多的圖片,越滾越多,但是
我們的Selenium只抓到了30個(gè),咦,這種在之前抓某個(gè)網(wǎng)站
的時(shí)候就遇到過(guò)了,寫個(gè)簡(jiǎn)單的滾動(dòng)到底部的js,然后Selenium
循環(huán)執(zhí)行這個(gè)腳本圖片數(shù)/30次就好了,中途可以休眠1s給他加載,
執(zhí)行完畢后,再去調(diào)用page_source得到最終的頁(yè)面代碼,然后走波
BeautifulSoup把我們1000多個(gè)小姐姐扒出來(lái)就好。

但是實(shí)際運(yùn)行的結(jié)果卻出乎我們的意料,最后得到的小姐姐列表
還是30個(gè),臥槽,什么鬼,接著打開(kāi)我們的瀏覽器,滾動(dòng)的時(shí)候
發(fā)現(xiàn),列表內(nèi)容竟然是動(dòng)態(tài)變化的,打開(kāi)圖片列表節(jié)點(diǎn),滾動(dòng)網(wǎng)
頁(yè),不禁又發(fā)出一句,臥槽,什么鬼,列表是動(dòng)態(tài)變化的???

這些剛還不在的,突然就蹦出來(lái)了,就好像你刷著即刻刷著
刷著就蹦出一個(gè)x子。動(dòng)態(tài)加載?想想野路子,要不我們自己
量化下滾動(dòng)偏移量,比如滾動(dòng)100,我們抓一波頁(yè)面,存一下,
最后做下去重?這野路子不是一般的野:

單不說(shuō)怎么量化這個(gè)偏移量了,瀏覽器寬度不一樣時(shí)加載的
數(shù)目還不一定是30,然后那么多畫板,你每個(gè)畫板這樣玩?
效率巨低。

苦苦尋覓后,發(fā)現(xiàn)了兩個(gè)關(guān)鍵詞:瀑布流和Ajax動(dòng)態(tài)加載數(shù)據(jù)


2.解決問(wèn)題

瀑布流參差不齊的多欄布局以及到達(dá)底部自動(dòng)加載的方式
Ajax動(dòng)態(tài)加載數(shù)據(jù)在不重新加載整個(gè)網(wǎng)頁(yè)的情況下,對(duì)網(wǎng)頁(yè)的某
部分進(jìn)行更新

簡(jiǎn)單點(diǎn)說(shuō)就是:

圖片通過(guò)JS加載成瀑布流的形式,當(dāng)?shù)竭_(dá)底部后,會(huì)請(qǐng)求后臺(tái)拿到更多
的數(shù)據(jù),解析后通過(guò)Ajax,可以在不關(guān)閉不轉(zhuǎn)跳不刷新瀏覽器的情況下
部分更新頁(yè)面內(nèi)容。

So,我們我們重新抓抓包,在滾動(dòng)到底部的時(shí)候看下抓到的數(shù)據(jù),
點(diǎn)擊篩選XHR(XMLHttpRequest),這個(gè)是瀏覽器后臺(tái)與服務(wù)之間
交換數(shù)據(jù)的文件,一般為json格式:

點(diǎn)開(kāi),右側(cè)看看Headers,果然是json格式的:

發(fā)現(xiàn)有這樣的請(qǐng)求頭,先放著,等下再研究規(guī)律:

點(diǎn)開(kāi)右側(cè)Previews,發(fā)現(xiàn)了傳回來(lái)的一大串Json,這里的
pins應(yīng)該就是新增加的妹子圖的相關(guān)數(shù)據(jù)了。


3.問(wèn)題再現(xiàn):猜測(cè)與試驗(yàn),一步步解密規(guī)則

為了方便研究,我又滾動(dòng)了幾下,加載更多的數(shù)據(jù),以方便
找出規(guī)律:

1.發(fā)起請(qǐng)求的規(guī)則

從上面我們可以得到一些這樣的信息:
首先是Get請(qǐng)求,固定的基地址:http://huaban.com/boards/18907029/
這個(gè)18907029是畫板id,后面的參數(shù),jcx1ki7y和wfl=1應(yīng)該是固定的,
limit這個(gè)是加載數(shù)量,一般是分頁(yè)用的,就是一次加載20條新數(shù)據(jù),
最后這個(gè)max:1348131400 暫時(shí)不知道是什么?不過(guò)應(yīng)該是某個(gè)什么id
吧,看下第二個(gè)的max是1263961742,復(fù)制到第一個(gè)返回的json里搜搜:

臥槽,剛好最后一個(gè),不會(huì)那么巧吧?然后把第三個(gè)max:1247629077復(fù)制
到第二個(gè)的Json里看看:

果然,好家伙,這個(gè)max就是每次拿到的最后一個(gè)圖片的pin_id
知道規(guī)則了,模擬一波請(qǐng)求,解析一波json,每次拿到最后這個(gè)
pin_id,用作下次請(qǐng)求,當(dāng)返回的pins里沒(méi)東西,說(shuō)明已經(jīng)加載
完所有的了,來(lái),寫一波代碼,先要處理剛進(jìn)來(lái)時(shí)加載的列表,
得到最后的一個(gè)圖的pin_id,然后才能開(kāi)始執(zhí)行上面那個(gè)拿
json的操作。

在我準(zhǔn)備用Selenium模擬請(qǐng)求主頁(yè)的時(shí)候,我發(fā)現(xiàn)了用urllib
模擬請(qǐng)求,里面就能拿到最后一個(gè)pin_id,只不過(guò)他是寫在js里
的,我們可以通過(guò)正則表達(dá)式拿到我們想要的pin_id們:

還有點(diǎn)開(kāi)一個(gè)具體的大圖頁(yè),發(fā)現(xiàn)他的url規(guī)則竟然是:
http://huaban.com/pins/926502853/
就是http://huaban.com/pins/ + pin_id,所以我們只要獲得pin_id就可以了!


2.一步步寫代碼

規(guī)則清楚了,接下來(lái)一步步寫代碼來(lái)獲取我們想要的數(shù)據(jù)吧!

1)首頁(yè)數(shù)據(jù)的獲取

進(jìn)來(lái)的時(shí)候會(huì)加載一次,不是通過(guò)Ajax加載,默認(rèn)是30個(gè),需要處理
一波網(wǎng)頁(yè)獲得這個(gè)30個(gè)數(shù)據(jù),然后30個(gè)數(shù)據(jù)的最后一個(gè)用于請(qǐng)求Ajax。
通過(guò)正則拿到pins這段json。

代碼如下

測(cè)試下代碼

打開(kāi)網(wǎng)頁(yè)加載更多確認(rèn)下這個(gè)最后的pid是否正確:

可以正確,接著就來(lái)處理json數(shù)據(jù)了~
(這要注意正則匹配用的是search,我一開(kāi)始沒(méi)留意用的是match,
用一些真這個(gè)校驗(yàn)工具測(cè)試自己的正則一直是對(duì)的,但是丟程序
里缺一直不匹配,返回None,要注意?。?!)

2)處理Ajax數(shù)據(jù)

首先是請(qǐng)求頭的設(shè)置,和我們普通的請(qǐng)求不一樣,如果你直接
用瀏覽器打開(kāi)ajax加載數(shù)據(jù)的那個(gè)url發(fā)現(xiàn)返回的并不是json!??!

隨手找個(gè)鏈接試試,就是解析json而已,

簡(jiǎn)單測(cè)試下

運(yùn)行結(jié)果

可以,拿到數(shù)據(jù)了,接下來(lái)要優(yōu)化下點(diǎn)東西,如果每次都要去拼接:
這串東西不就很麻煩了,可以通過(guò)正則來(lái)進(jìn)行替換:

這里用到前面沒(méi)有細(xì)講的re.sub()替換方法和向前和向后界定
這兩個(gè)東西什么時(shí)候用到,當(dāng)前這個(gè)場(chǎng)景就能用到,比如我們想替換
pin_id,用一個(gè)括號(hào)括著想替換的部分,感覺(jué)應(yīng)該就能替換了:

結(jié)果

是的,前面一大段東西沒(méi)了,如果你用了向前(?<=...)和向后界定(?=)
就可以讓正則只匹配和替換這兩個(gè)中間部分的字符串了~

好的,替換成功,小小整理一下代碼:

好的,pin_id都能夠拿到了,接下來(lái)通過(guò)這些想辦法拿到圖片啦,
點(diǎn)開(kāi)一個(gè)圖片的詳情頁(yè),比如:

http://huaban.com/pins/1272982736/

查看頁(yè)面結(jié)構(gòu),得到圖片url:

http://img.hb.aicdn.com/36b521f717741a4e3e024fd29606f61b8f960318f3763-WzUoLC_fw658

然后寫個(gè)簡(jiǎn)單的模擬請(qǐng)求下這個(gè)網(wǎng)址,看下返回的數(shù)據(jù)有沒(méi)有這個(gè)圖片Url相匹配
的內(nèi)容,全部搜沒(méi)有,然后把后面的分成三段,一段段搜:
先搜:36b521f717741a4e3e024fd29606f61b8f960318f3763,秒找到,
有六處匹配的,這后面跟著-WzUoLC,就剩下最后的fw658,全局搜搜不到

難道是固定的?打開(kāi)另一個(gè)詳情頁(yè)看看,果然,fw658是固定的:

http://img.hb.aicdn.com/7dc8cfc5e00f4cd78efba3f61c3b0b00f345a01a13b901-vaX0ho_fw658

臥槽,key???前面拿json的數(shù)據(jù)就能拿到key,臥槽,該不會(huì)就
這樣拼接就可以了吧,找到前面一個(gè):

...令人無(wú)法接受,折騰了那么舊,原來(lái)拿到key就可以拼接得出圖片url了


3.問(wèn)題還現(xiàn):我還是太naive了

嘖嘖,正確的圖片url拿到了,瀏覽器打開(kāi)也是沒(méi)問(wèn)題的,接著就是無(wú)腦
拼url,然后一個(gè)個(gè)下載了,正當(dāng)我以為一切已經(jīng)結(jié)束的時(shí)候,才發(fā)現(xiàn)了
最后的隱藏關(guān)卡:"圖片并不能下載"???右鍵另存為保存直接失敗,
py代碼直接崩潰。

然后,在那個(gè)圖片的詳情頁(yè)倒是可以下載,我的天,難不成要我每個(gè)
圖片都用Selenium加載,然后處理頁(yè)面數(shù)據(jù),在這里下?
講真,我是非常抗拒用Selenium的,慢不說(shuō),還耗內(nèi)存,
代理也不怎么好設(shè)置,而且別人會(huì)說(shuō)我low,百度谷歌搜了
一大堆,基本都是說(shuō)了等于沒(méi)說(shuō)...正在我萬(wàn)念俱灰,想用回
Selenium這種Low比方式的時(shí)候,我萌生了一個(gè)想法:
會(huì)不會(huì)是需要登錄后才能下載,于是乎我把鏈接發(fā)給我組
的UI,然后她默認(rèn)瀏覽器竟然是ie,然后奇跡發(fā)現(xiàn)了,沒(méi)有
登錄,直接用ie瀏覽器打開(kāi)了,然后他么的,可以右鍵保存
到本地?接著我又把鏈接發(fā)給我隔壁的后臺(tái)小哥,同樣用ie
打開(kāi),可以。此時(shí)熟悉的BGM響起:

真相花瓣沒(méi)有做ie系列瀏覽器的兼容

所以,模擬ie瀏覽器的User-Agent就可以下載圖片了?。?!

'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0)'

立馬試試,當(dāng)看到第一張圖片下載到了我的倉(cāng)庫(kù)里的時(shí)候,
我就知道我猜對(duì)了:

剩下的就是組織一波代碼,批量下載了~


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頁(yè)數(shù)據(jù),提取key列表寫入到文件里,并返回最后一個(gè)pid用于后續(xù)查詢
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)
    # 返回最后一個(gè)pin_id
    pin_id = json_dict[-1]['pin_id']
    return pin_id


# 模擬Ajax請(qǐng)求更多數(shù)據(jù)
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)
    # 一個(gè)畫板鏈接,可自行替換
    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("下載完成~")

輸出結(jié)果

參見(jiàn)吾王~


5.小結(jié):

磕磕碰碰,總算是把代碼給擼出來(lái)了,成功又收獲了一大波小姐姐,
爬蟲(chóng)技能點(diǎn)+1,建議還是少用無(wú)腦Selenium吧,另外剛發(fā)現(xiàn),
Chrome直接支持右鍵導(dǎo)出XPath,就不用自己慢慢扣了(如果你用lxml的話)。

好的,就說(shuō)那么多~


本節(jié)源碼下載

https://github.com/coder-pig/ReptileSomething


來(lái)啊,Py交易啊

想加群一起學(xué)習(xí)Py的可以加下,智障機(jī)器人小Pig,驗(yàn)證信息里包含:
Python,pythonpy,Py加群,交易,屁眼 中的一個(gè)關(guān)鍵詞即可通過(guò);

驗(yàn)證通過(guò)后回復(fù) 加群 即可獲得加群鏈接(不要把機(jī)器人玩壞了?。?!)~~~
歡迎各種像我一樣的Py初學(xué)者,Py大神加入,一起愉快地交流學(xué)♂習(xí),van♂轉(zhuǎn)py。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,695評(píng)論 19 139
  • 5 分享內(nèi)容到你的網(wǎng)站 上一章中,你在網(wǎng)站中構(gòu)建了用戶注冊(cè)和認(rèn)證。你學(xué)會(huì)了如何為用戶創(chuàng)建自定義的個(gè)人資料模型,并添...
    lakerszhy閱讀 1,731評(píng)論 5 16
  • "When you look up at the sky at night.since I'll be livin...
    SHEHAOJIE閱讀 309評(píng)論 0 0
  • 有一種愛(ài)只能欣賞! 我們生活在一個(gè)五彩斑斕的世界,在這個(gè)世界里不光有著美麗的風(fēng)景,同樣也有著不同個(gè)性、不...
    曉曉明天未來(lái)跑快點(diǎn)閱讀 334評(píng)論 0 0
  • 之前在讀書會(huì)當(dāng)中有一個(gè)書友跟我講了王爾德的“公主的生日”,他說(shuō)他非常喜歡他的文學(xué)淳樸簡(jiǎn)單一些宮廷的小故事,可是滿滿...
    平平無(wú)奇小個(gè)子閱讀 1,379評(píng)論 0 0

友情鏈接更多精彩內(nèi)容