聽說這個爬蟲面試題很難?看完你就知道怎么做了

最近有一個爬蟲面試題(http://shaoq.com:7777/exam)在圈內(nèi)看起來挺火的,經(jīng)常在各個爬蟲群里看到它被提到,而幾乎所有提到這個面試題的人在題目限制的條件下就不知道該怎么辦了,但這題目其實真的并不難,甚至可以說應(yīng)該只是為了在招人時再過濾一遍只會寫解析,拿著Selenium和代理池硬懟的人罷了(之前招人的時候見過很多,甚至有很多2-3年經(jīng)驗還處于這個水平)。

造成爬蟲圈子現(xiàn)在這個情況的原因我覺得可能是因為各種爬蟲書籍/培訓(xùn)班/網(wǎng)課都沒有講到過關(guān)于逆向方面的知識,他們的教學(xué)更傾向于Python語法、正則表達式、XPath這些非常基礎(chǔ)的東西和常見爬蟲框架/工具的簡單用法,而讀者/學(xué)員學(xué)完之后的水平充其量也就只能爬爬豆瓣之類的簡單網(wǎng)站,面對有點簡單反爬的就一臉懵逼,只能拿著Selenium和代理池硬懟。那么為了提升一下爬蟲圈內(nèi)的平均水平,寫點別人沒講或者不想講的東西并分享出來就很有必要了,這個專欄也是因此而生的。

扯遠了,開始講這個面試題吧,請站穩(wěn)扶好,老司機要開始飆車了。首先做好以下準(zhǔn)備,等會兒會用上,括號內(nèi)是文中所使用的工具名或版本號:

  1. 瀏覽器(Chrome)
  2. Fiddler/Charles之類的抓包工具(Fiddler)
  3. Python和JavaScript的IDE或編輯器(Pycharm + WebStorm)
  4. Python3.x和NodeJS(Python3.6.5 + NodeJS10.15.1)
  5. Python庫:pyexecjs、aiohttp、aiohttp_requests、lxml(最新版本)
  6. NodeJS庫:jsdom(最新版本)

準(zhǔn)備好了之后就可以開始了,先抓個包看看題目是啥樣的。

image

先是一個跳轉(zhuǎn)頁

image

然后會跳轉(zhuǎn)到內(nèi)容頁,已經(jīng)可以看到需要的文字了


看起來好像只需要拿到跳轉(zhuǎn)后的HTML就行了?實際并不是,這里可以看到上面這一行字里除了“python”和“題”以外,其他的標(biāo)簽在HTML中都是沒有文本內(nèi)容的,對應(yīng)的內(nèi)容全都顯示在了右邊的CSS樣式中。

image
image

但是抓包的時候也沒看到CSS,是不是把CSS嵌在了HTML中呢?打開這個HTML的代碼看看,一大坨加密的JS一眼可見,也并沒有看到style標(biāo)簽,顯然這個CSS是通過JS生成后加進去的。

image

很多人對JS逆向毫無了解,看到這里已經(jīng)懵逼了,碰到這種情況還不讓用Selenium之類的工具,又要爬到內(nèi)容,似乎完全沒辦法了啊。那應(yīng)該怎么辦呢?其實很簡單,看完這篇文章你就知道應(yīng)該怎么做了,下面我將用代碼對這個面試題的考點逐個擊破(完整代碼將在文章結(jié)尾處放出)。


先請求一下這個URL看看會返回什么結(jié)果。

提示:aiohttp_requests庫能讓你在用aiohttp進行請求時能使用類似于requests庫的語法,并且能正常使用session功能,而不需要寫一層接一層的async with xxxxxxx

image

請求返回的結(jié)果是最開始的跳轉(zhuǎn)頁,距離真正的內(nèi)容頁還差一點距離

image

斷點斷下來看看resp,已經(jīng)可以看到一個名為session的Cookie被set了,之前抓包的時候也是有看到服務(wù)器返回這個Cookie的。那么直接帶著這個Cookie再次請求是不是就可以拿到那個內(nèi)容頁了呢?我們將代碼改一下,對這個URL再次請求:

image
image

咦?有了這個Cookie之后的請求怎么還是返回這個跳轉(zhuǎn)頁呢?

image

現(xiàn)在再回到抓包工具中仔細看看,是不是發(fā)現(xiàn)抓到的瀏覽器請求里這兩個請求之間是有一堆圖片的,且第二次請求時,請求頭里的東西也沒有啥變化?

是這樣的,其實它的服務(wù)端對客戶端是否加載了圖片進行了判斷,如果客戶端沒有加載圖片就直接開始取內(nèi)容,那除了網(wǎng)速慢和刻意關(guān)閉了圖片的人以外,基本就可以確定是爬蟲了,所以這是一個簡單粗暴的反爬措施。

知道了這個考點之后就很簡單了,取出圖片的URL并和瀏覽器一樣進行請求就好了。再次修改代碼:

提示:因為這里重用host部分的次數(shù)很多,我把host部分寫成了一個常量。

提示:f"{HOST}{image.get('src')}"是format string,python3的一個語法糖,最開始有這個語法糖的版本已經(jīng)記不清了,如果你發(fā)現(xiàn)這段代碼在你的環(huán)境里無法運行,可以把這里改成"{}{}".format(HOST, image.get("src"))

提示:asyncio.gather是asyncio庫的并發(fā)執(zhí)行任務(wù)函數(shù),傳入的是一個協(xié)程函數(shù)列表,所以里面的requests.get不需要加await。

image

可以看到已經(jīng)取到了內(nèi)容頁的HTML,第一個考點我們已經(jīng)跨過去了,接下來要想想怎么拿到那個CSS的部分了。


那么這個JS要怎么處理呢?其實我們可以使用Python調(diào)用JS的方式去執(zhí)行它頁面中的那段代碼,從而生成出標(biāo)簽中對應(yīng)文字部分的CSS。這里推薦使用pyexecjs庫 + NodeJS來執(zhí)行JS代碼,pyexecjs庫可以說是目前最好的Python執(zhí)行JS代碼的庫了,另外一個比較常見的庫——PyV8,存在嚴重的內(nèi)存泄漏BUG,不建議使用。

但是直接執(zhí)行這段JS代碼是不可能有用的,我們還需要分析一下它的內(nèi)容并按我們的使用方式修改一下。先把那段JS復(fù)制出來,打開JavaScript IDE/編輯器,并把它丟進去進行分析。

image

此處省略幾百行變量

image

可以看到script標(biāo)簽里是一個匿名函數(shù),傳入了一個document參數(shù)(函數(shù)內(nèi)的uH),而實際這個匿名函數(shù)的主要流程代碼非常地少,只有兩個部分。

image

一個是開頭的這里

image

一個是靠近結(jié)尾位置的這里

第一部分沒有做什么操作,只是創(chuàng)建了一個element,那么核心部分應(yīng)該就是第二部分,跳到它調(diào)用的jE_函數(shù)看看。

提示:WebStorm中可以用鼠標(biāo)中鍵或Ctrl+鼠標(biāo)左鍵點擊jE_,跳轉(zhuǎn)到對應(yīng)的函數(shù)位置

image

這個jE_是這么一坨看不懂的東西,看不懂就沒法搞了,怎么辦呢?仔細看看上面那些用到的變量,是不是都是那一坨給變量賦值的地方出來的?那么我們只需要把那一串加起來的東西寫成一個新的變量,打個斷點在下面然后運行一下,就能直接看出它是啥了。(更高級的加密JS在還原時需要用到AST解析庫和相關(guān)知識寫工具處理而非手動處理,這里暫時還不需要用)

image

等一等,現(xiàn)在你還不能運行這段代碼,因為你沒有document,document是瀏覽器中特有的一個全局變量,而NodeJS中是不存在document這東西的,是不是覺得事情有點麻煩了起來?沒關(guān)系,問題不大,既然NodeJS中沒有,那我們就自己造一個,這里使用jsdom庫來模擬瀏覽器中的dom部分,從而做到在NodeJS中使用document的操作。當(dāng)然你如果想要自己造也是可以的,只需要按著報錯提示一個一個地實現(xiàn)這段JS代碼中調(diào)用的document.xxx即可。

這個jsdom庫的使用方式很簡單,只需要按照文檔上的說明導(dǎo)入jsdom,再new一個dom實例就可以了。

Basic usage

const jsdom = require("jsdom");
const { JSDOM } = jsdom;

To use jsdom, you will primarily use the JSDOM constructor, which is a named export of the jsdom main module. Pass the constructor a string. You will get back a JSDOM object, which has a number of useful properties, notably window:

const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
console.log(dom.window.document.querySelector("p").textContent); // "Hello world"

注意了,這里的dom變量還并不是我們要的document變量,真正的document變量是dom.window.document,所以我們的代碼可以這樣寫:

image

執(zhí)行一下看看效果

image

原來上面的兩個參數(shù)分別是decodeURIComponent%E6%81%AF%E6%95%B0%E9%9D%A2%E7%88%AC%E8%99%AB%E4%BF%A1%E6%8A%80%E5%88%9B%E8%AF%95%E7%A7%91,我們把后面那段一眼就能看出是經(jīng)過urlencode的字符串還原一下看看。

image
image

嗯...其實就是頁面上的那句話了,只不過它是亂序的,我們接著往下執(zhí)行看看它還做了什么操作。

image

往下執(zhí)行時報錯了,看起來是缺少了decodeURIComponent這個函數(shù),那decodeURIComponent前面的那個uc_又是什么呢?用同樣的方式可以看到,其實是window

image

也就是說這句代碼還原成正常的樣子其實就是this.window.decodeURIComponent("%E6%81%AF%E6%95%B0%E9%9D%A2%E7%88%AC%E8%99%AB%E4%BF%A1%E6%8A%80%E5%88%9B%E8%AF%95%E7%A7%91"),而NodeJS的decodeURIComponent并不在this.window中,所以我們還是需要通過最開始造document的操作,再給它弄一個this.window.decodeURIComponent,代碼很簡單,改成這樣即可:

image

然后我們再執(zhí)行一遍

image

這次就能正常運行完畢了,但是我們要的東西去哪兒了呢?我們繼續(xù)往下打斷點看,vz_是亂序的文字,ti_是一個里面只有數(shù)字的數(shù)組,SE_則只有兩個空字符串,KI_函數(shù)沒有進行賦值,而最后的return其實是沒有任何作用的,因為jE_在主流程中是最后一個被執(zhí)行的函數(shù),它返回的值賦給了xe_后并不會被使用。所以這里似乎只有SE_KI_比較可疑了,斷點進入給SE_賦值的Er_函數(shù)看看。

image

看來這個Er_函數(shù)并不會做什么,那么我們要的核心部分可以確定就是KI_這個函數(shù)了。接著追到下面的KI_函數(shù)。

image

這里它又調(diào)用了一個叫Ks_的函數(shù),跟著它繼續(xù)往下跳。

image

又是熟悉的Er_,還記得剛剛看到的嗎,它只是做了一個split操作而已,ti_是前面那個只有數(shù)字的數(shù)組,這里的NL_只不過是按順序取了一個ti_里的元素罷了,下面沒見過的BD_Je_才是重點。

image

這里斷下來看出BD_其實是一個取前面那串亂序字符串中其中一個文字的東西,繼續(xù)往下執(zhí)行可以看到最終出來的YO_是一個字。

image

那么Je_呢?繼續(xù)往下執(zhí)行看看

image

Je_里調(diào)用了ee_.insertRule,而ee_是前面被賦值的

image
image

所以實際上它是新建了一個element并往里面寫了我們要的CSS。看到這里,其實這個考點已經(jīng)被破掉了,我們只需要讀出ee_返回給Python,就可以把那段文字給恢復(fù)出來了。

將JS代碼再修改一下:

image

然后我們試一下能不能用,記得將這里的html字符串替換成你請求時返回的。(通常這種用到瀏覽器內(nèi)特有的一些變量的JS都會埋下一些坑,建議讀者養(yǎng)成完全模擬瀏覽器環(huán)境的習(xí)慣,當(dāng)然如果不怕遇到坑的話只給JS中需要用到的東西也可以,而這個題目本身并沒有這種坑,所以只弄一個空的dom并且魔改一下只傳入字符串和數(shù)組部分也能用。)

image
image

boom!CSS成功地被我們拿到手了,左邊的codexx對應(yīng)右邊的content部分文字,與瀏覽器中的一模一樣,JS部分算是搞好了,我們要繼續(xù)寫我們的Python代碼,先把html=xxx開始的部分全部刪除掉,只保留上面導(dǎo)入包的部分和get_css這個函數(shù)的部分。

回到Python代碼部分,修改成調(diào)用JS得到CSS后處理一下CSS和HTML的對應(yīng)關(guān)系,并取出所有文字內(nèi)容再打印出來。

image

提示:這里的dict(list)是一個Python的語法糖,可以快速地將[[1,2],[3,4]]轉(zhuǎn)成{1:2, 3:4}

提示:這里可能會出現(xiàn)一個問題,之前直接用NodeJS執(zhí)行沒問題的代碼,經(jīng)過PyExecJS調(diào)用之后卻報錯了,這個問題似乎只有在Windows系統(tǒng)上才會出現(xiàn),主要原因應(yīng)該是Windows的編碼問題,碰到這種情況可以用Buffer.from(string).toString("base64");將返回的字符串編碼為Base64,在Python中再進行解碼。

image

執(zhí)行一下看看,是不是已經(jīng)拿到了需要的那行字了呢?

image

文中代碼傳送門


如果這篇文章有幫到你,請大力點贊,謝謝~~ 歡迎關(guān)注我的知乎賬號loco_z和我的知乎專欄《手把手教你寫爬蟲》,我會時不時地發(fā)一些爬蟲相關(guān)的干貨和黑科技,說不定能讓你有所啟發(fā)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。