老司機教你看妹子——你的第一個知乎爬蟲(2)
序
“老師快教我怎么才能獲取完整圖片?”
別急,還記得上回說的,設置UA的事情么
“嗯, 要把自己偽裝成一個瀏覽器 ”
我們先看看瀏覽器做了什么。
瀏覽器做了什么
你已經知道了,知乎的網頁是通過 Ajax 進行 異步加載, 證據就是那個大大的“更多”按鈕。
點一下,就加載更多。
那么點了按鈕之后究竟發生了什么呢?瀏覽器可以告訴我們究竟發生了什么。
使用Chrome瀏覽器,在頁面上單擊鼠標右鍵->審查元素(檢查),就可以喚起開發者工具。
切換到Network一欄里,然后點擊“更多”,就可以看到點擊了“更多”后面發生了什么網絡請求。
你看到了什么?
“一堆圖片請求,然后上面的那個看圖標應該是文件之類的,而且有29k,加載了630ms。而且,從時間順序上看,先執行獲取到了這個數據,然后再加載的圖片。”
Ok,那我們點開看一下。
啊哈!
“這里是具體的回答數據!那我只需要調用這個接口,就可以拿到所有的答案了!“
Copy as cURL
你迫不及待的寫下了,r = requests.get(url,headers=ua_header)
這樣的代碼,卻發現了我在旁邊笑而不語。
“有什么問題么?”
不要急著動手寫,先分析下這個請求都帶了那些參數。
“是指的url里的參數么?”
不是。選中那個請求,然后右鍵->Copy->Copy as cURL
cURL是一個利用URL語法在命令行下工作的文件傳輸工具,絕大多數的linux 和unix都會內置的一個軟件.
但是,注意windows下命令行里調用的curl并不是這里提到的那個curl,具體的討論可以看這篇:PowerShell 為什么 alias 了 curl 就引起了如此大的爭議?
“那我現在要去裝curl?”
別急啊,我們先看看我們復制出來了什么。
curl 'https://www.zhihu.com/api/v4/questions/22212644/answers?sort_by=default&include=data%5B%2A%5D.is_normal%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Cmark_infos%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cupvoted_followees%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&limit=721&offset=0' -H 'Cookie: aliyungf_tc=AQAAADvok2VGQgEAiE1NfJc49LzrUk7p; q_c1=ef155c9cc9124e1f951447b3b6fd875f|1500990779000|1500990779000; _zap=84936da1-3813-4842-a846-56214e791bd0; q_c1=70bd5b2623c14a3d9079630a483b0aa3|1500990778000|1500990778000; l_n_c=1; l_cap_id="NDczNThhZjExZWU1NDIzYWJhMDllNjBkZTJiNDUwZGI=|1500990807|075d561c18784c75946ae101e6f7135b5b271cf3"; r_cap_id="Y2MwNzdlMjMxMjc0NDViMGE0NWJhOTI3NGQzNjQwNGY=|1500990807|769b04b121d8b52602f6d7f3e86ca62bf5f80d33"; cap_id="ZTFkYmRiMjZiZTgyNDdjY2I3YTk3ZDExNDZjZDUyYmY=|1500990807|2f85fb4342ee06cfe26e5ba006d699c778a7787c"; n_c=1; _xsrf=b967f17b-4577-428e-906d-1c306c67decd' -H 'Accept-Encoding: gzip, deflate, br' -H 'Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.6,en;q=0.4' -H 'authorization: oauth c3cef7c66a1843f8b3a9e6a1e3160e20' -H 'accept: application/json, text/plain, */*' -H 'Referer: https://www.zhihu.com/question/22212644' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36' -H 'Connection: keep-alive' --compressed
嗯,這么長一行確實挺惡心的,我們來換個行。
curl
'https://www.zhihu.com/api/v4/questions/22212644/answers
?sort_by=default
&include=data%5B%2A%5D.is_normal%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Cmark_infos%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cupvoted_followees%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics
&limit=20
&offset=3'
-H
'Cookie:
aliyungf_tc=AQAAADvok2VGQgEAiE1NfJc49LzrUk7p; q_c1=ef155c9cc9124e1f951447b3b6fd875f|1500990779000|1500990779000; _zap=84936da1-3813-4842-a846-56214e791bd0;
l_n_c=1;
l_cap_id="NDczNThhZjExZWU1NDIzYWJhMDllNjBkZTJiNDUwZGI=|1500990807|075d561c18784c75946ae101e6f7135b5b271cf3";
r_cap_id="Y2MwNzdlMjMxMjc0NDViMGE0NWJhOTI3NGQzNjQwNGY=|1500990807|769b04b121d8b52602f6d7f3e86ca62bf5f80d33"; cap_id="ZTFkYmRiMjZiZTgyNDdjY2I3YTk3ZDExNDZjZDUyYmY=|1500990807|2f85fb4342ee06cfe26e5ba006d699c778a7787c";
n_c=1;
_xsrf=b967f17b-4577-428e-906d-1c306c67decd'
-H 'Accept-Encoding: gzip, deflate, br'
-H 'Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.6,en;q=0.4'
-H 'authorization: oauth c3cef7c66a1843f8b3a9e6a1e3160e20'
-H 'accept: application/json, text/plain, */*'
-H 'Referer: https://www.zhihu.com/question/22212644'
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36'
-H 'Connection: keep-alive'
curl的第一參數就是要請求的url, -H表示一個Header,所以,這個請求總共有8個Header,其中一個是cookie。
“老師,cookie是啥”
(如果你知道cookie是啥,可以跳過下面的小節)
cookie與session
我們從一個故事開始。
你去柜臺買東西,付錢拿貨走人。第二天你又去柜臺,說說好的贈品還沒給呢。店員說,你誰啊,要啥贈品啊,買東西那邊排隊去。卒。后來為了避免這種情況,柜臺會給個你一個收據,同時也會記錄那些東西賣出去了,還欠著贈品。你下次來的時候,帶著收據,柜臺根據收據,一查賬,誒發現確實欠你個贈品,然后給你贈品。大家都美滋滋。
“店員就這么蠢,一點都不記事么”
對,最開始的http協議就是這樣,一次請求結束后,連接就斷開了。后來有了cookie的誕生,發送一些額外的信息,用來標記用戶。
“那cookie就是收據,session就是店家的記錄?”
對,cookie和session分別表示存在瀏覽器和服務器的某些數據,這些數據被用來標識用戶和記錄狀態。而且cookie是通常都是服務器設置的。
Request with cookie
“那我就把這些cookie直接寫近代碼里就可以了么?”
cookie會過期,所以,最好還是要知道這些cookie從何而來。
我們從頭開始,看看服務器給我們設置了那些cookie
在做這個之前,你要先進入Chrome的隱身模式,隱身模式會清除你以前瀏覽器留下的cookie,其實之前我們所有的請求都是在隱身模式下發起的。
點開第一條請求,我們看到了_xsrf和aliyungf_tc這兩個cookie
_xsrf這樣的字段通常是用來防止XSRF(跨站請求偽造)的,而且aliyungf_tc,從名稱上來看,應該是阿里云高防塞的。
“那q_c1, l_n_c, l_cap_id,r_cap_id,cap_id, n_c這些呢?”
一點小經驗:這種充斥著大量連字符、意義不明的cookie,都可以忽略,這些通常是在某些中間步驟被設置進去的。
我們分析了這么多,終于可以開始寫了。
我們上次用到的requests庫,提供了一個requests.Session()對象,可以用來很方面的模擬一次會話,它會設置根據服務器的返回自動cookie。
def init(url):
ua = {'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36'}
s = requests.Session()
s.headers.update(ua)
ret=s.get(url)
return s
“老師,這個get的這個url是干啥用的,為啥返回值直接不要了”
會想一下我們剛剛分析的整個流程,首先打開了某個網頁,然后JS調用了"https://www.zhihu.com/api/v4/questions/22212644/answers"這樣一個接口。所以,我們要模擬打開網頁的那一步,讓服務器給我們設置cookie。
“然后接下來模擬js調用?”
然也
def fetch_answer(s,qid,limit,offset):
params={
'sort_by':'default',
'include':'data[*].is_normal,is_collapsed,annotation_action,annotation_detail,collapse_reason,is_sticky,collapsed_by,suggest_edit,comment_count,can_comment,content,editable_content,voteup_count,reshipment_settings,comment_permission,mark_infos,created_time,updated_time,review_info,relationship.is_authorized,is_author,voting,is_thanked,is_nothelp,upvoted_followees;data[*].author.follower_count,badge[?(type=best_answerer)].topics',
'limit':limit,
'offset':offset
}
url ="https://www.zhihu.com/api/v4/questions/"+qid+"/answers"
return s.get(url,params=params)
qid,就是url后面的那一串數字,question_id,limit表示一次拉去多少個回答、offset表示從第幾個開始。
接下來你就可以運行一下了。
url = "https://www.zhihu.com/question/29814297"
session = init(url)
q_id = url.split('/')[-1]
offset = 0
limit=20
ret=fetch_answer(session,q_id,limit,offset)
如果不出意外的話,你可能會看到這樣一個結果:
“呃,老師這個\u8bf7這一串是什么”
最簡單的方式,把這一串copy出來,貼到交互式窗口里:
請求頭錯誤,我們可以通過ret.request.headers查看我們個發出的請求,究竟包含了那些header
對比之后,發現,少了authorization: oauth c3cef7c66a1843f8b3a9e6a1e3160e20
我們在init函數里把它加上
def init(url):
ua = {'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36'}
s = requests.Session()
s.headers.update(ua)
ret=s.get(url)
s.headers.update({"authorization":"oauth c3cef7c66a1843f8b3a9e6a1e3160e20"})
return s
在執行一次,我們終于成功了
獲取所有答案
“等一下,老師,剛剛那個fetch_answer,limit我們可以直接設置到99999啊,然后不久可以一口氣拿回來所有的么,或者我們找下總共有多少個回答,然后設置成那個數字不就好了嘛。”
你一天吃三頓飯,但是你會一口氣吃光三頓飯么!這么做當然可以,但是這樣的行為十分反常,而且很可能觸發某些反爬蟲機制,因為這個特征太明顯了。牢記一點,你是一個瀏覽器。
(實際上知乎會把大于20的limit當做20去處理)
這里我們因為不關心總數,所以就利用fetch_answer返回的paging里的is_end字段,來判斷是否獲取完畢。
def fetch_all_answers(url):
session = init(url)
q_id = url.split('/')[-1]
offset = 0
limit=20
answers=[]
is_end=False
while not is_end:
ret=fetch_answer(session,q_id,limit,offset)
#total = ret.json()['paging']['totals']
answers+=ret.json()['data']
is_end= ret.json()['paging']['is_end']
print("Offset: ",offset)
print("is_end: ",is_end)
offset+=limit
return answers
最后,我們拿到了一個answer的數組,從每個answer里的'content'字段里找出url,下載就好了。
url = "https://www.zhihu.com/question/29814297"
answers=fetch_all_answers(url)
folder = '29814297'
for ans in answers:
imgs = grep_image_urls(ans['content'])
for url in imgs:
download(folder,url)
完整的代碼在這里
“老師圖呢?”
最后爬取了“日常穿JK制服是怎樣一種體驗?”這樣一個問題,拿到了970張圖
“老師,我們下節課干啥”
關注我,下節課你就知道了。