最近有一個爬蟲面試題(http://shaoq.com:7777/exam)在圈內看起來挺火的,常常在各個爬蟲羣裏看到它被提到,而幾乎全部提到這個面試題的人在題目限制的條件下就不知道該怎麼辦了,但這題目其實真的並不難,甚至能夠說應該只是爲了在招人時再過濾一遍只會寫解析,拿着Selenium和代理池硬懟的人罷了(以前招人的時候見過不少,甚至有不少2-3年經驗還處於這個水平)。css
形成爬蟲圈子如今這個狀況的緣由我以爲多是由於各類爬蟲書籍/培訓班/網課都沒有講到過關於逆向方面的知識,他們的教學更傾向於Python語法、正則表達式、XPath這些很是基礎的東西和常見爬蟲框架/工具的簡單用法,而讀者/學員學完以後的水平充其量也就只能爬爬豆瓣之類的簡單網站,面對有點簡單反爬的就一臉懵逼,只能拿着Selenium和代理池硬懟。那麼爲了提高一下爬蟲圈內的平均水平,寫點別人沒講或者不想講的東西並分享出來就頗有必要了,這個專欄也是所以而生的。html
扯遠了,開始講這個面試題吧,請站穩扶好,老司機要開始飆車了。首先作好如下準備,等會兒會用上,括號內是文中所使用的工具名或版本號:python
準備好了以後就能夠開始了,先抓個包看看題目是啥樣的。git
先是一個跳轉頁github
而後會跳轉到內容頁,已經能夠看到須要的文字了web
看起來好像只須要拿到跳轉後的HTML就好了?實際並非,這裏能夠看到上面這一行字裏除了「python」和「題」之外,其餘的標籤在HTML中都是沒有文本內容的,對應的內容全都顯示在了右邊的CSS樣式中。面試
可是抓包的時候也沒看到CSS,是否是把CSS嵌在了HTML中呢?打開這個HTML的代碼看看,一大坨加密的JS一眼可見,也並無看到style標籤,顯然這個CSS是經過JS生成後加進去的。正則表達式
不少人對JS逆向毫無瞭解,看到這裏已經懵逼了,碰到這種狀況還不讓用Selenium之類的工具,又要爬到內容,彷佛徹底沒辦法了啊。那應該怎麼辦呢?其實很簡單,看完這篇文章你就知道應該怎麼作了,下面我將用代碼對這個面試題的考點逐個擊破(完整代碼將在文章結尾處放出)。數組
先請求一下這個URL看看會返回什麼結果。瀏覽器
提示:aiohttp_requests庫能讓你在用aiohttp進行請求時能使用相似於requests庫的語法,而且能正常使用session功能,而不須要寫一層接一層的async with xxxxxxx
。
請求返回的結果是最開始的跳轉頁,距離真正的內容頁還差一點距離
斷點斷下來看看resp,已經能夠看到一個名爲session的Cookie被set了,以前抓包的時候也是有看到服務器返回這個Cookie的。那麼直接帶着這個Cookie再次請求是否是就能夠拿到那個內容頁了呢?咱們將代碼改一下,對這個URL再次請求:
咦?有了這個Cookie以後的請求怎麼仍是返回這個跳轉頁呢?
如今再回到抓包工具中仔細看看,是否是發現抓到的瀏覽器請求裏這兩個請求之間是有一堆圖片的,且第二次請求時,請求頭裏的東西也沒有啥變化?
是這樣的,其實它的服務端對客戶端是否加載了圖片進行了判斷,若是客戶端沒有加載圖片就直接開始取內容,那除了網速慢和刻意關閉了圖片的人之外,基本就能夠肯定是爬蟲了,因此這是一個簡單粗暴的反爬措施。
知道了這個考點以後就很簡單了,取出圖片的URL並和瀏覽器同樣進行請求就行了。再次修改代碼:
提示:由於這裏重用host部分的次數不少,我把host部分寫成了一個常量。
提示:f"{HOST}{image.get('src')}"
是format string,python3的一個語法糖,最開始有這個語法糖的版本已經記不清了,若是你發現這段代碼在你的環境裏沒法運行,能夠把這裏改爲"{}{}".format(HOST, image.get("src"))
。
提示:asyncio.gather
是asyncio庫的併發執行任務函數,傳入的是一個協程函數列表,因此裏面的requests.get
不須要加await。
能夠看到已經取到了內容頁的HTML,第一個考點咱們已經跨過去了,接下來要想一想怎麼拿到那個CSS的部分了。
那麼這個JS要怎麼處理呢?其實咱們可使用Python調用JS的方式去執行它頁面中的那段代碼,從而生成出標籤中對應文字部分的CSS。這裏推薦使用pyexecjs庫 + NodeJS來執行JS代碼,pyexecjs庫能夠說是目前最好的Python執行JS代碼的庫了,另一個比較常見的庫——PyV8,存在嚴重的內存泄漏BUG,不建議使用。
可是直接執行這段JS代碼是不可能有用的,咱們還須要分析一下它的內容並按咱們的使用方式修改一下。先把那段JS複製出來,打開JavaScript IDE/編輯器,並把它丟進去進行分析。
此處省略幾百行變量
能夠看到script標籤裏是一個匿名函數,傳入了一個document
參數(函數內的uH
),而實際這個匿名函數的主要流程代碼很是地少,只有兩個部分。
一個是開頭的這裏
一個是靠近結尾位置的這裏
第一部分沒有作什麼操做,只是建立了一個element,那麼核心部分應該就是第二部分,跳到它調用的jE_
函數看看。
提示:WebStorm中能夠用鼠標中鍵或Ctrl+鼠標左鍵點擊jE_
,跳轉到對應的函數位置
這個jE_
是這麼一坨看不懂的東西,看不懂就無法搞了,怎麼辦呢?仔細看看上面那些用到的變量,是否是都是那一坨給變量賦值的地方出來的?那麼咱們只須要把那一串加起來的東西寫成一個新的變量,打個斷點在下面而後運行一下,就能直接看出它是啥了。(更高級的加密JS在還原時須要用到AST解析庫和相關知識寫工具處理而非手動處理,這裏暫時還不須要用)
等一等,如今你還不能運行這段代碼,由於你沒有document,document是瀏覽器中特有的一個全局變量,而NodeJS中是不存在document這東西的,是否是以爲事情有點麻煩了起來?不要緊,問題不大,既然NodeJS中沒有,那咱們就本身造一個,這裏使用jsdom庫來模擬瀏覽器中的dom部分,從而作到在NodeJS中使用document的操做。固然你若是想要本身造也是能夠的,只須要按着報錯提示一個一個地實現這段JS代碼中調用的document.xxx便可。
這個jsdom庫的使用方式很簡單,只須要按照文檔上的說明導入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 aJSDOM
object, which has a number of useful properties, notablywindow
: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
,因此咱們的代碼能夠這樣寫:
執行一下看看效果
原來上面的兩個參數分別是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
,咱們把後面那段一眼就能看出是通過urlencode的字符串還原一下看看。
嗯...其實就是頁面上的那句話了,只不過它是亂序的,咱們接着往下執行看看它還作了什麼操做。
往下執行時報錯了,看起來是缺乏了decodeURIComponent
這個函數,那decodeURIComponent
前面的那個uc_
又是什麼呢?用一樣的方式能夠看到,實際上是window
。
也就是說這句代碼還原成正常的樣子其實就是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
,代碼很簡單,改爲這樣便可:
而後咱們再執行一遍
此次就能正常運行完畢了,可是咱們要的東西去哪兒了呢?咱們繼續往下打斷點看,vz_
是亂序的文字,ti_
是一個裏面只有數字的數組,SE_
則只有兩個空字符串,KI_
函數沒有進行賦值,而最後的return實際上是沒有任何做用的,由於jE_
在主流程中是最後一個被執行的函數,它返回的值賦給了xe_
後並不會被使用。因此這裏彷佛只有SE_
和KI_
比較可疑了,斷點進入給SE_
賦值的Er_
函數看看。
看來這個Er_
函數並不會作什麼,那麼咱們要的核心部分能夠肯定就是KI_
這個函數了。接着追到下面的KI_
函數。
這裏它又調用了一個叫Ks_
的函數,跟着它繼續往下跳。
又是熟悉的Er_
,還記得剛剛看到的嗎,它只是作了一個split操做而已,ti_
是前面那個只有數字的數組,這裏的NL_
只不過是按順序取了一個ti_
裏的元素罷了,下面沒見過的BD_
和Je_
纔是重點。
這裏斷下來看出BD_
實際上是一個取前面那串亂序字符串中其中一個文字的東西,繼續往下執行能夠看到最終出來的YO_
是一個字。
那麼Je_
呢?繼續往下執行看看
Je_
裏調用了ee_.insertRule
,而ee_
是前面被賦值的
因此實際上它是新建了一個element並往裏面寫了咱們要的CSS。看到這裏,其實這個考點已經被破掉了,咱們只須要讀出ee_
返回給Python,就能夠把那段文字給恢復出來了。
將JS代碼再修改一下:
而後咱們試一下能不能用,記得將這裏的html字符串替換成你請求時返回的。(一般這種用到瀏覽器內特有的一些變量的JS都會埋下一些坑,建議讀者養成徹底模擬瀏覽器環境的習慣,固然若是不怕遇到坑的話只給JS中須要用到的東西也能夠,而這個題目自己並無這種坑,因此只弄一個空的dom而且魔改一下只傳入字符串和數組部分也能用。)
boom!CSS成功地被咱們拿到手了,左邊的codexx對應右邊的content部分文字,與瀏覽器中的如出一轍,JS部分算是搞好了,咱們要繼續寫咱們的Python代碼,先把html=xxx
開始的部分所有刪除掉,只保留上面導入包的部分和get_css
這個函數的部分。
回到Python代碼部分,修改爲調用JS獲得CSS後處理一下CSS和HTML的對應關係,並取出全部文字內容再打印出來。
提示:這裏的dict(list)是一個Python的語法糖,能夠快速地將[[1,2],[3,4]]轉成{1:2, 3:4}
提示:這裏可能會出現一個問題,以前直接用NodeJS執行沒問題的代碼,通過PyExecJS調用以後卻報錯了,這個問題彷佛只有在Windows系統上纔會出現,主要緣由應該是Windows的編碼問題,碰到這種狀況能夠用Buffer.from(string).toString("base64");
將返回的字符串編碼爲Base64,在Python中再進行解碼。
執行一下看看,是否是已經拿到了須要的那行字了呢?
若是這篇文章有幫到你,請大力點贊,謝謝~~ 歡迎關注個人知乎帳號loco_z和個人知乎專欄《手把手教你寫爬蟲》,我會時不時地發一些爬蟲相關的乾貨和黑科技,說不定能讓你有所啓發。