前言
如果想看實現可以跳到最后(python代碼)。注: 只是非常簡單的逆向js,大佬勿噴。
測試:
2020年5月25號 代碼運行正常
2020年8月16號 網站已經修改策略,已經寫好了更新 https://www.cnblogs.com/re-is-good/p/mafengwo_version2_ast_cookie.html
雖然下面的代碼已經對馬蜂窩已經無效了,但這種反爬並不是馬蜂窩網站獨有的。
網站抓取測試
首先上網址: https://www.mafengwo.cn/i/18252205.html
要是使用正常的python代碼(如下)來請求這個網址的話
import requests url = "https://www.mafengwo.cn/i/18252205.html" headers = { "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36", "referer": "https://www.mafengwo.cn/i/18252205.html", } r = requests.get(url, headers=headers) print(r.text) # print(r.status_code)
會返回以下結果
對,沒錯。返回的竟然是script標簽。里面包含着js代碼。看js代碼的樣子也不像是加密的html內容。
我們還可以打印一下狀態碼
print(r.status_code);
返回的是521,一般情況下都是200。這就非常奇怪了。
這時候我們就要去瀏覽器中打開這個頁面,按F12打開開發者頁面(mac是fn+F12)
好了,如果打開了開發者工具。那么就刷新下頁面吧。
我們會非常驚喜的發現,在瀏覽器中 這個url的請求(https://www.mafengwo.cn/i/18252205.html)完全正常,而且狀態碼還是200
那就去看看在瀏覽器中這個url請求到底發送了什么?
仔細對比的話,就會發現瀏覽器的請求中有一項cookie,而且cookie內容特別多。
我們可以試着將cookie中的內容放到headers中,然后再發送請求
url = "https://www.mafengwo.cn/i/18252205.html" headers = { "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36", "referer": "https://www.mafengwo.cn/i/18252205.html", "cookie": "_jsluid_s=...;__jsl_clearance=...;", # 瀏覽器中的cookie部分,因為太長我就沒有全部復制了 } r = requests.get(url, headers=headers) print(r.text)
然后就能正常請求了。
我們也可以慢慢刪除cookie中的字段,因為服務器不會驗證所有的cookie字段。
經過測試,只要 有__jsluid_s 和__jsl_clearance 字段便可以正常請求。
這,這就完了?no no no。
直接拷貝cookie多low啊。
我們要自己生成cookie,其實也就是想辦法構造合理的__jsluid_s和__jsl_clearance 字段, 然后想怎么請求就怎么請求。
cookie的生成邏輯
首先啊。我們需要知道怎么清除瀏覽器的cookie。
為啥子?
因為他的cookie可以重復使用,只有沒有cookie或者cookie失效時,才會重新請求。
如上圖,在開發者工具中選中 "Application" 工具欄。找到 "Cookie" 側邊欄。右鍵那個網址,就會有 "clear"選項了。點擊 "clear" 就可以清除改網址的所有cookie
做完這件事情后,就可以再次刷新下頁面了。
在刷新頁面之前,可以清除下之前的網絡請求log。不然會新舊請求會跑到一塊,不太好辨別
好了,下面是刷新后的網絡請求
除了那個正常的200請求。我們還會發現還有一個狀態碼為521的請求(第一個請求)。
這個不就是我們剛開始使用python代碼請求后返回的狀態碼碼?
點進去這個請求看看
你看,問題解決了一半了。cookie中的 __jsluid_s 字段直接在響應頭里了。只要我們搞定 cookie中的 __jsl_clearance 字段便可以發起正常請求了。
下面的代碼將獲取 __jsluid_s 字段。
import requests url = "https://www.mafengwo.cn/i/18252205.html" headers = { "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36", "referer": "https://www.mafengwo.cn/i/18252205.html", } # 請不要傳入cookie,不然瀏覽器不會在請求頭中寫入我們需要的字段 r = requests.get(url, headers=headers) print(r.headers) __jsluid_s = r.headers["Set-Cookie"].split(";")[0].split("=")[1] print(__jsluid_s) # 這個便是我們需要的cookie的字段
那剩下的50%,__jsl_clearance 字段怎么弄呢?仔細翻了下響應頭,發現毫無 __jsl_clearance 字段的痕跡。
或許 __jsl_clearance 不在這個 狀態碼為 521 的請求身上?
那我們從第二個請求開始看吧。
第二個請求
響應頭確實有cookie字段,但不是我們想要的
繼續看第三個請求
what? 我們需要的cookie字段在這個地方竟然已經被發送出去了。(注意,這里的cookie字段是請求頭那里的,而不是響應頭那里。
這說明在第三個請求之前, cookie中的__jsl_clearance 字段就已經被設置了。
第二個請求雖然設置了cookie,但不是__jsl_clearance。
那么cookie中的__jsl_clearance 字段只能在第一個請求(狀態碼為521)里被設置了。
那我們回看一下第一個請求。
有時候 Response 里不像上圖所示的那樣,有時會顯示 "failed to load response data"。不過沒有關系,因為接下來要做的事情與瀏覽器無關。
如果遇到了 "failed to load response data"。可以直接使用python請求。然后復制了響應內容。(就是上圖中的"<script>...")
然后選擇你最喜歡的一款代碼編輯器,新建一個html頁面。然后粘貼你之前復制的響應內容。
如上圖,添加一些東西。debugger的作用是當我們打開開發者工具時,代碼能在這里停住,以方便我們調試。
在瀏覽器中打開。按F12(mac為fn+F12)打開開發者工具
當我們一打開開發者工具,就會直接跳到 Source 選項卡。並在 "debugger"處停住
復制下的代碼只有一行,非常難以調試。我們只需點擊下上圖的 "{}" 便可以格式化代碼。
格式化后的代碼長這個樣子
我一眼就看到了第30行的 eval函數。這個函數能執行js字符串。
eval("console.log('hello world')"); // 這樣控制台就會輸出 "hello world"
eval執行邏輯
因為代碼不是很長,我們並沒有選擇下斷點。我們選擇復制js代碼,然后扔到console選項卡中運行。
首先復制這一部分代碼到console選項卡中
回車運行
第二段要執行的js代碼
z++
第三段要執行的js代碼
y.replace(/\b\w+\b/g, function(y) { return x[f(y, z) - 1] || ("_" + y) })
然后我們就可以看到eval函數要運行的字符串了
三次快速點擊返回的字符串,然后復制一下。選擇一個你喜歡的編輯器,再新建一個html文件,將復制的字符串放進去。
記得去除前后的雙引號及<script>和</script>。 debugger還是要加的。具體的如下所示。
同樣的,在瀏覽器中打開此html文件。並打開開發者工具,格式化下代碼("{}" 符號)
可以看到,cookie中的__jsl_clearance字段生成就在這部分的代碼了。
其實我們需要的那部分代碼只是這個
復制這部分代碼到console中運行。
可以發現返回的結果就是我們要的__jsl_clearance字段
為了保險起見,可以復制__jsl_clearance字段和前面生成好的__jsluid_s字段做個測試。(可自行測試)
python怎么調用js代碼?
答案是使用 execjs第三方模塊,需要pip安裝下。
execjs的簡單使用
import execjs jsContext = execjs.compile("var sToken='hello js'; function foo(){return sToken}") # 將字符串編譯一下 ret = jsContext.call("foo"); # 調用foo函數 print(ret); # 返回 'hello js'
解釋下eval執行的代碼
如何動態修改返回的js代碼
好了,現在我們就要想辦法動態修改返回的js代碼。因為直接執行是不行的。因為execjs的環境並不是瀏覽器環境
首先 eval執行的代碼(也就是上圖所示的代碼), 我們首先只需要設置cookie的那部分代碼。
如何去掉不需要的js代碼?(注意! eval執行的是一段字符串)
使用js中的正則即可
evalCode.replace(/^[\w\W]+__jsl_clearance=/, '') .replace(/\+';Expires=[\w\W]+$/, '')
這里的正則便可以幫我們去除多余的部分。
這個js正則其實需要動態的插入要執行的js代碼(最開始的那個響應內容中的js代碼,以<script>開頭的那個)
什么意思呢?
這是eval要執行的字符串
我們要給他換成這個樣子
這樣eval要執行的字符串就會變成這個樣子
這時候eval執行下這個字符串,就能得到最后的結果啦。(最好是先不eval這個字符串, 先返回這個要進行eval的字符串,然后再運行一次)。
注意!!!!(2020.4月27日網站邏輯增加)
有時候eval返回的字符串直接執行的話會有問題的。(顯示document is not defined), 類似的代碼如下
這里的做法比較聰明,因為只有瀏覽器才有document對象,並且它還調用了document對象的方法
如果不是瀏覽器環境,不做一定的處理的,就會運行報錯。
稍微解釋下,那一行代碼的含義吧
_4k=document.createElement('div'); // 創建一個div標簽。 <div></div> _4k.innerHTML="<a href=\'/\'>_i</a>"; // \' 是轉義。 // 其實就等於_4k.innerHTML="<a href='/'>_i</a>"; // 設置這個div標簽的 子內容(會被自動解析為html) // <div> // <a href='/'>_i</a> // </div> _4k=_4k.firstChild.href; // 獲取div標簽的第一個子元素的href屬性 // _4k 的結果便是 當前的網址的根路徑(即"https://www.mafengwo.cn/") var_5b=_4k.match(/https?:\/\//[0]; // re正則表達式。_5b的結果是 "https://" _4k=_4k.substr(_5b.length).toLowerCase(); // substr 用於切割字符串,接受兩個參數,第一個參數切割的開始位置。第二個參數是要切多少 // toLowerCase()方法是將字符串中的字母轉為小寫 // _4k 的結果是 "www.mafengwo.cn/"
貌似這個代碼其實是固定的,但變量名是不同的,我們需要定義一下document對象,然后給他一定的方法就可以
var document = { createElement: function(tag){ var innerHTML; return { firstChild: { href: "https://www.mafengwo.cn/" } } } };
上面定義的document便提供eval字符串中所需要的東西,接着我們將這段代碼插入到execjs中便可以正常運行了。
具體的代碼實現
import requests import re import execjs def changeJsRunTimeCodeAndGetClearance(content): # print(content) insertJsCode = """ .replace(/^[\w\W]+__jsl_clearance=/, '') .replace(/\+';Expires=[\w\W]+$/, '') """ evalJsCode = content.replace('("_"+y)})', '("_"+y)})' + insertJsCode) # 這里本來是要eval下這個js字符串的,但我們這里是返回了這個js字符串 evalJsCode = evalJsCode.replace("<script>", "").replace( "eval(y.replace(/", "return (y.replace(/" ) # print(evalJsCode) # # 這里先找到</script>的位置,是為了把</script>后面的字符串全部清除掉 index = evalJsCode.index("</script>") # # 記得要定義一下window對象。 documentCode = """ var document = { createElement: function(tag){ var innerHTML; return { firstChild: { href: "https://www.mafengwo.cn/" } } } }; """ evalJsCode = ( "var window = {};" + documentCode + "function exec(){" + evalJsCode[0:index] + "}" ) # print(evalJsCode) context = execjs.compile(evalJsCode) # print(context.call("exec")) finalContext = execjs.compile( # context.call("exec")是用來調用前面的exec函數的 "var window={};" + documentCode + "function final(){ return '" + context.call("exec") + "}" ) finalVal = finalContext.call("final") return finalVal def getContent( url, __jsluid_s="", __jsl_clearance="", ): url = "https://www.mafengwo.cn/i/18252205.html" headers = { "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/527.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36", "referer": "https://www.mafengwo.cn/i/18252205.html", } if __jsluid_s != "" and __jsl_clearance != "": cookie = "__jsluid_s=%s; __jsl_clearance=%s;" % (__jsluid_s, __jsl_clearance) headers.update({"cookie": cookie}) r = requests.get(url, headers=headers) print(r.headers) # print(r.text) print(r.status_code) if r.text.startswith("<script>"): # cookie錯誤 __jsluid_s = r.headers["Set-Cookie"].split(";")[0].split("=")[1] print("__jsluid_s", __jsluid_s) __jsl_clearance = changeJsRunTimeCodeAndGetClearance(r.text) print("__jsl_clearance", __jsl_clearance) # # 再請求一遍 getContent(url, __jsluid_s=__jsluid_s, __jsl_clearance=__jsl_clearance) if not r.text.startswith("<script>"): print(r.text) # content = r.text getContent("https://www.mafengwo.cn/i/18252206.html") # '__jsluid_s=9a91972ada0d6431c85c51308d9ca2d6 # changeJsRunTimeCode(content) # document.createElement('div'); # _4k.innerHTML="<a href=\'/\'>_i</a>"; # _4k=_4k.firstChild.href;
View Code