前言
在GitHub上維護了一個代理池的項目,代理來源是抓取一些免費的代理發布網站。上午有個小哥告訴我說有個代理抓取接口不能用了,返回狀態521。抱着幫人解決問題的心態去跑了一遍代碼。發現果真是這樣。
通過Fiddler抓包比較,基本可以確定是JavaScript生成加密Cookie導致原來的請求返回521。
發現問題
打開Fiddler軟件,用瀏覽器打開目標站點(http://www.kuaidaili.com/proxylist/2/) 。可以發現瀏覽器對這個頁面加載了兩次,第一次返回521,第二次才正常返回數據。很多沒有寫過網站或是爬蟲經驗不足的童鞋,可能就會覺得奇怪為什么會這樣?為什么瀏覽器可能正常返回數據而代碼卻不行?

仔細觀察兩次返回的結果可以發現:


1、第二次請求比第一次請求的Cookie內容多了個這個_ydclearance=0c316df6ea04c5281b421aa8-5570-47ae-9768-2510d9fe9107-1490254971
2、第一次返回的內容一些復雜看不懂的JS代碼,第二次返回的就是正確的內容
其實這是網站反爬蟲的常用手段。大致過程是這樣的:首次請求數據時,服務端返回動態的混淆加密過的JS,而這段JS的作用是給Cookie添加新的內容用於服務端驗證,此時返回的狀態碼是521。瀏覽器帶上新的Cookie再次請求,服務端驗證Cookie通過返回數據(這也是為嘛代碼不能返回數據的原因)。
解決問題
其實我第一次遇到這樣的問題是,一開始想的就是既然你是用JS生成的Cookie, 那么我也可以將JS函數翻譯成Python運行。但是最后還是發現我太傻太天真,因為現在的JS都流行混淆加密,原始的JS這樣的:
function lq(VA) {
var qo, mo = "", no = "", oo = [0x8c, 0xcd, 0x4c, 0xf9, 0xd7, 0x4d, 0x25, 0xba, 0x3c, 0x16, 0x96, 0x44, 0x8d, 0x0b, 0x90, 0x1e, 0xa3, 0x39, 0xc9, 0x86, 0x23, 0x61, 0x2f, 0xc8, 0x30, 0xdd, 0x57, 0xec, 0x92, 0x84, 0xc4, 0x6a, 0xeb, 0x99, 0x37, 0xeb, 0x25, 0x0e, 0xbb, 0xb0, 0x95, 0x76, 0x45, 0xde, 0x80, 0x59, 0xf6, 0x9c, 0x58, 0x39, 0x12, 0xc7, 0x9c, 0x8d, 0x18, 0xe0, 0xc5, 0x77, 0x50, 0x39, 0x01, 0xed, 0x93, 0x39, 0x02, 0x7e, 0x72, 0x4f, 0x24, 0x01, 0xe9, 0x66, 0x75, 0x4e, 0x2b, 0xd8, 0x6e, 0xe2, 0xfa, 0xc7, 0xa4, 0x85, 0x4e, 0xc2, 0xa5, 0x96, 0x6b, 0x58, 0x39, 0xd2, 0x7f, 0x44, 0xe5, 0x7b, 0x48, 0x2d, 0xf6, 0xdf, 0xbc, 0x31, 0x1e, 0xf6, 0xbf, 0x84, 0x6d, 0x5e, 0x33, 0x0c, 0x97, 0x5c, 0x39, 0x26, 0xf2, 0x9b, 0x77, 0x0d, 0xd6, 0xc0, 0x46, 0x38, 0x5f, 0xf4, 0xe2, 0x9f, 0xf1, 0x7b, 0xe8, 0xbe, 0x37, 0xdf, 0xd0, 0xbd, 0xb9, 0x36, 0x2c, 0xd1, 0xc3, 0x40, 0xe7, 0xcc, 0xa9, 0x52, 0x3b, 0x20, 0x40, 0x09, 0xe1, 0xd2, 0xa3, 0x80, 0x25, 0x0a, 0xb2, 0xd8, 0xce, 0x21, 0x69, 0x3e, 0xe6, 0x80, 0xfd, 0x73, 0xab, 0x51, 0xde, 0x60, 0x15, 0x95, 0x07, 0x94, 0x6a, 0x18, 0x9d, 0x37, 0x31, 0xde, 0x64, 0xdd, 0x63, 0xe3, 0x57, 0x05, 0x82, 0xff, 0xcc, 0x75, 0x79, 0x63, 0x09, 0xe2, 0x6c, 0x21, 0x5c, 0xe0, 0x7d, 0x4a, 0xf2, 0xd8, 0x9c, 0x22, 0xa3, 0x3d, 0xba, 0xa0, 0xaf, 0x30, 0xc1, 0x47, 0xf4, 0xca, 0xee, 0x64, 0xf9, 0x7b, 0x55, 0xd5, 0xd2, 0x4c, 0xc9, 0x7f, 0x25, 0xfe, 0x48, 0xcd, 0x4b, 0xcc, 0x81, 0x1b, 0x05, 0x82, 0x38, 0x0e, 0x83, 0x19, 0xe3, 0x65, 0x3f, 0xbf, 0x16, 0x88, 0x93, 0xdd, 0x3b];
qo = "qo=241; do{oo[qo]=(-oo[qo])&0xff; oo[qo]=(((oo[qo]>>3)|((oo[qo]<<5)&0xff))-70)&0xff;} while(--qo>=2);";
eval(qo);
qo = 240;
do {
oo[qo] = (oo[qo] - oo[qo - 1]) & 0xff;
} while (--qo >= 3);
qo = 1;
for (; ;) {
if (qo > 240) break;
oo[qo] = ((((((oo[qo] + 2) & 0xff) + 76) & 0xff) << 1) & 0xff) | (((((oo[qo] + 2) & 0xff) + 76) & 0xff) >> 7);
qo++;
}
po = "";
for (qo = 1; qo < oo.length - 1; qo++) if (qo % 6) po += String.fromCharCode(oo[qo] ^ VA);
eval("qo=eval;qo(po);");
}
看到這樣的JS代碼,我只能說原諒我JS能力差,還原不了。。。
但是前端經驗豐富的童鞋馬上就能想到還有種方法可解,那就是利用瀏覽器的JS代碼調試功能。這樣一切就迎刃而解,新建一個html文件,將第一次返回的html原文復制進去,保存用瀏覽器打開,在eval之前打上斷點,看到這樣的輸出:

可以看到這個變量po為document.cookie='_ydclearance=0c316df6ea04c5281b421aa8-5570-47ae-9768-2510d9fe9107-1490254971; expires=Thu, 23-Mar-17 07:42:51 GMT; domain=.kuaidaili.com; path=/'; window.document.location=document.URL,下面還有個eval("qo=eval;qo(po);")。JS里面的eval和Python的差不多,第二句的意思就是將eval方法賦給qo。然后去eval字符串po。而字符串po的前半段的意思是給瀏覽器添加Cooklie,后半段window.document.location=document.URL是刷新當前頁面。
這也印證了我上面的說法,首次請求沒有Cookie,服務端回返回一段生成Cookie並自動刷新的JS代碼。瀏覽器拿到代碼能夠成功執行,帶着新的Cookie再次請求獲取數據。而Python拿到這段代碼就只能停留在第一步。
那么如何才能使Python也能執行這段JS呢,答案是PyV8。V8是Chromium中內嵌的javascript引擎,號稱跑的最快。PyV8是用Python在V8的外部API包裝了一個python殼,這樣便可以使python可以直接與javascript操作。PyV8的安裝大家可以自行百度。
代碼
分析完成,下面切入正題擼代碼。
首先是正常請求網頁,返回帶加密的JS函數的html:
由於返回的是html,並不單純的JS函數,所以需要用正則提取JS函數的參數的參數。

還有一點需要注意,在JS函數中並沒有返回cookie,而是直接將cookie set到瀏覽器,所以我們需要將eval("qo=eval;qo(po);")替換成return po。這樣就能成功返回po中的內容。
這樣返回的cookie是字符串格式,但是用requests.get()需要字典形式,所以將其轉換成字典:
最后帶上解析出來的Cookie再次訪問網頁,成功獲取數據:
下面是完整代碼:
轉載請注明來源: 開源中國https://my.oschina.net/jhao104/blog/865966
歡迎關注微信公眾號: Pythoner每日一報
