1.背景
最近接手了爬蟲的項(xiàng)目, 爬取對象是天貓和京東的價(jià)格相關(guān)的數(shù)據(jù), 其中對于天貓的優(yōu)惠券的爬取需要有已登錄的cookie才能成功爬到數(shù)據(jù)。
之前對于這塊的cookie都是我們手動(dòng)用自己淘寶賬號登錄淘寶m站,并獲取cookie存到服務(wù)端。并且cookie的最長有效時(shí)間是24小時(shí),也就是說我們每天都要有人去手動(dòng)操作一下。
問題:
- 每天都需要手動(dòng)操作,費(fèi)時(shí)費(fèi)力,而且容易忘記容易出錯(cuò)。
- 作為一個(gè)程序員,每天都干同樣的事又無法改變這種事是忍不了的。
- 經(jīng)常半夜驚醒, 想起來cookie忘了設(shè)置, 搞得神經(jīng)衰弱..
思路很簡單: 用程序去模擬一個(gè)真實(shí)的登錄操作
2.曾經(jīng)嘗試過的方案
核心是使用無頭瀏覽器去操作
- selenium(Java API) + headless chrome
- puppeteer(Node API) + headless chrome
PS:無頭瀏覽器最開始在voc中的聲音專題項(xiàng)目中有用過,用的是phantomjs,不過這個(gè)東西的API太難用了,剛開始就沒考慮。
以上方案最終都是以失敗而告終。原因是最終淘寶都會彈出一個(gè)驗(yàn)證的組件讓你去操作
比如下面這樣的:
剛開始以為,這種驗(yàn)證碼不就是"點(diǎn)一下"的事嗎,找到這個(gè)dom元素,去點(diǎn)就好了。
結(jié)果發(fā)現(xiàn)找是找到了,點(diǎn)也點(diǎn)中了,卻是怎么點(diǎn)都“驗(yàn)證失敗”。
懷疑是JavaScript去做click跟用戶真實(shí)點(diǎn)擊可能不太一樣,所以嘗試手動(dòng)去點(diǎn)了一下,結(jié)果還是“驗(yàn)證失敗”。
然后就懷疑淘寶對于這種headless chrome是不是做了什么識別,并且是限制了這種瀏覽器的行為。
因?yàn)閾Q成正常安裝的chrome,都是可以正常登錄,連驗(yàn)證組件都不會跳出來。
3.為什么用chrome插件
既然headless chrome都被淘寶風(fēng)控了,那么之后的思路就變成了“能不能控制現(xiàn)有安裝的chrome瀏覽器來做一些自動(dòng)化操作”
這里我和小伙伴有幾次都想到了“按鍵精靈”,但是后來放棄了。一方面是因?yàn)閷Π存I精靈不熟悉,學(xué)習(xí)成本比較高,第二是按鍵精靈太依賴于呈現(xiàn)在眼前的畫面,個(gè)人理解很容易被一些意外情況打斷。
使用chrome插件的靈感其實(shí)是來源于我的小伙伴(這里感謝@俊杰)。
不過真正用了才發(fā)現(xiàn)好處還是比較多的,列了以下4點(diǎn):
- 可以依附在現(xiàn)有的瀏覽器中,只跟瀏覽器有關(guān)系,平臺無關(guān)
- 主要使用JavaScript來開發(fā),學(xué)習(xí)成本低,并且有成熟的調(diào)試方案
- 通過js來控制頁面,不需要頁面必須是呈現(xiàn)在你面前(比如最小化瀏覽器也ok),運(yùn)行更可靠
- chrome插件內(nèi)置了很多底層的庫,可以模擬真實(shí)的用戶點(diǎn)擊操作 (這個(gè)是后來才知道的,具體下面會說)
4.chrome插件入門及基本結(jié)構(gòu)
4.1 chrome插件開關(guān)入門
入門參考鏈接(第1個(gè)鏈接是入門demo, 第2個(gè)是詳細(xì)教程。)
注意
入門教程中說加載自己開發(fā)的插件的時(shí)候,要先打包,再安裝。實(shí)際上我打包安裝好后,這個(gè)插件是不讓啟用的(應(yīng)該是chrome的安全機(jī)制), 我沒找到可以讓它啟用的地方, 后面用的是另一個(gè)方案:只需要加載"已解壓的擴(kuò)展程序", 選擇你的插件所在的文件夾即可啟用你的插件。具體操作見下圖。
在開發(fā)的過程中,如果更新代碼,重新啟用插件即可生效,非常方便。
4.2 chrome插件基本結(jié)構(gòu)
具體可以參考上面的第2個(gè)參考鏈接。實(shí)際上,我最后只用到了3個(gè):
- manifest.json
- content-script
- background
所以說一下我個(gè)人對于這3個(gè)東西的理解:
可以看到content-script是針對具體的某個(gè)頁面生效的,而background是瀏覽器這個(gè)層面的,即對所有的頁面都生效。manifest.json只是一個(gè)配置文件
實(shí)際上background是包含background.html和background.js,但是我這次的插件完全不需要頁面交互,所以只涉及到了js的部分。
5."尖兵一號"實(shí)現(xiàn)過程及踩坑實(shí)錄
5.1 manifest.json配置
首先創(chuàng)建基本的配置文件, 配置代碼如下:
{
"name" : "尖兵一號",
"version" : "0.8",
"description" : "淘寶m站自動(dòng)刷新cookie插件",
"permissions": [ "cookies", "debugger", "http://*/*", "https://*/*" ],
"browser_action": {
"default_icon": "icon.png"
},
"background": {
"scripts": ["background.js"]
},
"content_scripts": [ {
"matches": ["https://login.m.taobao.com/login.htm*","https://h5.m.taobao.com/mlapp/mytaobao.html*"],
"js": [ "jquery.min.js","jquery.cookie.min.js","contentJs.js" ],
"run_at": "document_end"
} ],
"manifest_version": 2
}
這里有2個(gè)注意點(diǎn):
- 需要調(diào)用的模塊必須在permissions中聲明, 有點(diǎn)像導(dǎo)包的感覺
- content_scripts中的matches必須配置正確, 否則可能不生效或者打開所有頁面都生效(剛開始我配的是
"matches": ["<all_urls>"], 即對所有頁面生效, 后面腳本跑瘋了...)
5.2 content-script腳本編碼
5.2.1 jQuery的click()和原生dom的click()
剛開始我還不認(rèn)為需要用到background.js, 因?yàn)闉g覽器已經(jīng)是"正規(guī)軍"了, 就想說是不是直接用JavaScript去觸發(fā)click登錄就可以了。我對jQuery比較熟, 就用jQuery去觸發(fā)了登錄按鈕的click事件。
結(jié)果還是會跳出來驗(yàn)證組件讓我去點(diǎn)擊,雖然這次人去點(diǎn)還是能驗(yàn)證通過的,但是還是沒解決問題的。
查了一下jQuery的click()和真實(shí)點(diǎn)擊的區(qū)別, 結(jié)果發(fā)現(xiàn)有一個(gè)答案是:
jQuery 的 .click() 只是 jQuery 的,并不是大家的,觸發(fā)點(diǎn)擊事件的話還是用原生 .click() 的好。
參考鏈接: https://segmentfault.com/q/1010000002491025
結(jié)論我沒有去深究,因?yàn)楸旧砦覍avaScript還沒到這種階段,所以我只是去試了一把原生的click()。
原代碼:
$("#btn-submit").click()
修改為:
document.getElementById("btn-submit").click()
結(jié)果很喜人,就是一把過,直接登錄成功。(單純的我以為到這里已經(jīng)找到真相了...)
5.2.2 獲取cookie
已經(jīng)能夠順利登錄了(至少當(dāng)時(shí)看起來是的),那么接下來的問題就剩下拿到cookie并上傳了。
拿cookie很簡單,在content-script中調(diào)用document.cookie一把拿到當(dāng)前域下的所有cookie, 首先我只是先用console.log打印cookie到控制臺, 然后在本地測試了一下這個(gè)cookie的可用性。
這一試, 果然不能用, 心都涼了半截。
因?yàn)橹笆菑?code>Request Headers中拿到cookie再手動(dòng)上傳服務(wù)器的, 所以把Request Headers中的cookie和document.cookie做了一下對比
注: 為了方便對比, 我把cookie中的分號都換成了回車
可以看到左邊Request Headers中拿到的cookie要多出6個(gè)key-value對。
突然反應(yīng)過來, 標(biāo)識了httpOnly的cookie是通過js拿不到的, F12打開了瀏覽器的調(diào)試界面, 驗(yàn)證了下, 果然如此。
5.3 background腳本編碼
接下來的問題,就變成了怎么(能不能)通過chrome插件拿到httpOnly的cookie。
5.3.1 cookie API
最后找到了chrome extension cookie api
這里有2個(gè)注意點(diǎn):
- cookie api 必須在manifest中有聲明
- cookie api 只能在background中使用, 而不能在content-script中使用
到了這里, 我們才真正第一次使用上了background.js的功能。
關(guān)鍵代碼如下:
chrome.cookies.getAll({'domain':'.taobao.com'},function(cookie){
var needKeys = ['enc','cookie2','unb','skt','cookie1','uc3','cookie17'];
var needCookie = '';
for(i=0;i<cookie.length;i++){
if(needKeys.includes(cookie[i].name)){
//do something..
}
}
});
至此, 順利拿到所有cookie
5.3.2 content-script和background的通信
cookie是拿到了, 接下來怎么上傳cookie呢, 而且是什么時(shí)候上傳呢?
剛開始想到一種方案是background去定時(shí)刷cookie(比如1個(gè)小時(shí)拿一次), 一旦發(fā)現(xiàn)所有需要的cookie都能拿到, 就調(diào)用服務(wù)端接口上傳。
這個(gè)方案也不是不可行,但是感覺太low了點(diǎn),程序員都是很執(zhí)著的,總想著用優(yōu)雅的方式去解決問題。實(shí)際上只有在重新登錄之后,我才需要再去重新上傳一遍cookie。于是想到了,content-script和background之間是否可以通信?;镜乃悸肥沁@樣的:
通信機(jī)制可以參考鏈接:
https://www.cnblogs.com/liuxianan/p/chrome-plugin-develop.html#%E6%B6%88%E6%81%AF%E9%80%9A%E4%BF%A1
關(guān)鍵代碼:
//content-script
chrome.runtime.sendMessage(request, function(response,r2,r3) {
console.log('收到來自background的回復(fù):' + response);
//do something
});
//background
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
console.log('收到來自content-script的消息:');
console.log(request, sender, sendResponse);
//do something
sendResponse(response);
return true;
});
這里有1個(gè)注意點(diǎn):
- background中最后的return true是必須的, 否則content-script將收不到background給它的response.
參考鏈接: https://blog.csdn.net/anjingshen/article/details/75579521
5.3.3 上傳cookie(服務(wù)端接口)
服務(wù)端接口是對我來說是最簡單的一步了, 用im-api網(wǎng)關(guān)暴露一個(gè)爬蟲系統(tǒng)(im-spider)存儲cookie的接口出去, 配置一下就行了。
到此整個(gè)流程都完整了。
5.3.4 原來還有這種事..
我設(shè)置了1小時(shí)刷新一次cookie(即1小時(shí)就重新跳轉(zhuǎn)到login頁面進(jìn)行重新登錄并重新上傳cookie)。
1小時(shí)后,等待我的確實(shí)login頁又彈出了驗(yàn)證組件。難道是我的1小時(shí)太短了,把時(shí)間設(shè)置成了10個(gè)小時(shí),結(jié)果第二天發(fā)現(xiàn)還是有驗(yàn)證組件,又陷入了絕望中...
搜索了一下:
js模擬點(diǎn)擊和真實(shí)點(diǎn)擊有什么區(qū)別
然后知道了, 點(diǎn)擊之后產(chǎn)生的event對象原來還有一個(gè)叫isTrusted的屬性, true表示用戶真實(shí)點(diǎn)擊, false表示是用js觸發(fā)的click
寫了一個(gè)demo測了一把:
<!DOCTYPE html>
<html>
<head>
<title>demo</title>
<script type="text/javascript">
function bodyonclick(e) {
alert(e.isTrusted);
}
</script>
</head>
<body style="width: 1000px;height: 800px;background-color: black;" onclick="bodyonclick(event);document.body.click()">
</body>
</html>
我用鼠標(biāo)點(diǎn)擊body之后, 第1次彈出true, 第2次彈出false。先不管這個(gè)代碼寫的有沒有問題,結(jié)果確實(shí)驗(yàn)證了通過event對象可以區(qū)分出是js模擬點(diǎn)擊還是真實(shí)點(diǎn)擊。
此時(shí)的問題又變成了怎么樣(能不能)讓chrome插件模擬用戶真實(shí)的點(diǎn)擊?
這里感謝公司開放了Google,最終讓我搜索到了一個(gè)在stackoverflow上的方案, 要不然我用百度估計(jì)搜不到結(jié)果。
這個(gè)參考方案還很人性化的給了chrome插件對應(yīng)的官方API文檔URL。
關(guān)鍵代碼:
chrome.debugger.sendCommand(target, "Input.dispatchMouseEvent", {
type:'mousePressed',
x:position.x,
y:position.y,
button:'left',
clickCount:1
}, function(s){
});
chrome.debugger.sendCommand(target, "Input.dispatchMouseEvent", {
type:'mouseReleased',
x:position.x,
y:position.y,
button:'left',
clickCount:1
}, function(s){
});
用這個(gè)方法測試了一下, 果然event對象的isTrusted就變成了true
這里有3個(gè)注意點(diǎn):
- 用到debugger模塊, 必須在manifest中聲明, 并且只能在background中使用
- 鼠標(biāo)的一次點(diǎn)擊必須是sendCommand兩次(即一次下壓,一次釋放), 之前我也是沒注意, 導(dǎo)致點(diǎn)擊都不生效
- 鼠標(biāo)的點(diǎn)擊只能根據(jù)坐標(biāo)來定位, 但是不會伴隨鼠標(biāo)的移動(dòng), 所以到底準(zhǔn)不準(zhǔn)只有點(diǎn)完之后才知道的(推薦先用右鍵點(diǎn)擊來模擬, 會彈出右鍵菜單, 所以可以測試坐標(biāo)的準(zhǔn)確性)
5.3.5 優(yōu)化
以上, 問題基本都已經(jīng)解決, 穩(wěn)定運(yùn)行1天(定時(shí)1小時(shí)刷新cookie), 沒有再彈出驗(yàn)證組件了。
接下來還做了3個(gè)優(yōu)化的點(diǎn):
- 隨機(jī)8-20小時(shí)刷新一次cookie
- 增加了智能驗(yàn)證塊的點(diǎn)擊邏輯(因?yàn)榘l(fā)現(xiàn)偶爾還是會彈出來, 但是用上面模擬點(diǎn)擊的方案也能通過)
- 增加了控制臺的輸出日志, 方便觀察
6.總結(jié)
寫了很多, 但是最關(guān)鍵的其實(shí)就2個(gè)點(diǎn):
- 如何獲取httpOnly的cookie
- 如果模擬用戶的真實(shí)點(diǎn)擊
寫這篇文章的目的, 并不是讓大家能掌握什么技能, 而是想把這次解決問題的思路給大家分享一下, 可能里面有很多不嚴(yán)謹(jǐn)?shù)牡胤? 也有很多代碼不規(guī)范的地方, 但是還是那句話:
思路比結(jié)論重要 -- 58沈老師經(jīng)常講的一句話
目前這個(gè)插件穩(wěn)定運(yùn)行2天。不管后面是否能持續(xù)穩(wěn)定運(yùn)行下去,光這次技術(shù)方案的調(diào)研就讓我學(xué)到了很多:chrome插件的開發(fā)流程,httpOnly對于cookie的安全性考慮,模擬點(diǎn)擊和真實(shí)點(diǎn)擊的關(guān)鍵點(diǎn)...
如果這個(gè)插件可以一直用下去, 也會后面其他網(wǎng)站(目前只支持天貓爬蟲)的爬蟲打下來基礎(chǔ)。
目前爬蟲的架構(gòu)圖如下:
"尖兵一號"的流程圖如下:
6.1 "尖兵一號"的未來展望
為什么叫“尖兵一號”?我當(dāng)時(shí)取名字想到了《最強(qiáng)狂兵》中的蘇銳,emmm,好像也沒什么太大關(guān)系...
實(shí)際上目前這個(gè)插件還是有一些缺陷以及可以優(yōu)化的點(diǎn), 比如刷新的時(shí)間可以控制在白天, 比如需要依賴瀏覽器(目前是用公司電腦7*24小時(shí)開機(jī)刷...)
不過我覺得從0到1的問題我都解決了, 后面的這些問題無非就是從1到n的問題了
6.2 對于chrome插件的理解及思考
chrome插件的優(yōu)勢就在于充分的利用了瀏覽器資源, 可以在現(xiàn)有網(wǎng)頁上進(jìn)行擴(kuò)展。因?yàn)閲?yán)格來說,應(yīng)該叫chrome擴(kuò)展程序。劣勢也很明顯,只能單體安裝,多用戶升級困難。
暫時(shí)沒想到業(yè)務(wù)上什么場景能用chrome插件來解決,但是個(gè)人工作上還是能作為工具的開發(fā),比如:
- ELK沒有日志的導(dǎo)出(用于日志線下分析), 用chrome插件擴(kuò)展一下
- DBPlus查詢不方便(只能查前35條), 用chrome插件擴(kuò)展一下
最后希望看完這篇文章的讀者們都有收獲, 如有不對的地方, 歡迎拍磚~