你好,我是悅創。關注公衆號:AI悅創,搶先閱讀優質文章。
公衆號原文:https://mp.weixin.qq.com/s/PYEiSMgP2LT0DmcHX08PCw
部落格原文:https://www.aiyc.top/798.html
通過本文的學習,在你寫爬蟲時,你應該會對呼叫 JavaScript 有一個更清晰的瞭解,並且你還要瞭解到一些你平時可能見不到的騷操作。
大家如果接觸過 JavaScript 逆向的話,應該都知道,通常來說碰到 JS 逆向網站時會有這兩種情況:
對於簡單的 JS 來說,我們可以通過 Python 程式碼,直接重寫,輕輕鬆鬆的就能搞定。
而對於復的 JS 程式碼而言呢,由於程式碼過於複雜,重寫太費時費力,且碰到對方更新這就比較麻煩了。所以,我們一般直接使用程式去呼叫 JS,在 Python 層面就只是獲取一個執行結果,這樣做相比於重寫而言就方便多了。
那麼,接下來我帶大家看一下兩種比較簡單的 JS 程式碼重寫。
本文涉及的所有演示程式碼請到公衆號後臺回覆 回復:PJS 。來獲取即可!
首先,我們先來看一下 Base64 ,Base64 是我們再寫爬蟲過程中經常看到的一種編碼方式。這邊我們來寫兩個例子。
// 原字元
NightTeam
// 編碼之後的:
TmlnaHRUZWFt
第一個例子如上,是 NightTeam 經過編碼是如上面的結果(TmlnaHRUZWFt
),如果我們只是通過這個結果來分析的話,它的特徵不是很明顯。如果是見的不多或者是新手小白的同學,並不會把它往 Base64 方向去想。
然後,我們來看一下第二個例子:
// 原字元
aiyuechuang
// 編碼之後的:
YWl5dWVjaHVhbmc=
// 原字元
Python3
// 編碼之後
UHl0aG9uMw==
第二個例子是 aiyuechuang 編碼之後的結果,它的末尾有一個等號,Python3 編碼之後末尾有兩個等號,這個特徵相對第一個就比較明顯了。一般我們看到尾號有兩個等號時應該大概可以猜到這個就是 Base64 了。
然後,直接解碼看一看,如果沒有什麼特別的話,就可以使用 Python 進行重寫了。
同學可以使用以下鏈接 Base64 編碼解碼的測試學習:http://tool.alixixi.com/base64/
不過 Base64 也會有一些騷操作,碰到那種情況的時候,我們如果用 Python 重寫可能有點麻煩。具體的內容我會在後面的的課程中,單獨的跟大家詳細的講解。
第二個的話就是 MD5 ,MD5 在 Javascript 中並沒有標準的庫,一般我們都是使用開源庫去操作。
注意:md5 的話是 雜湊 並不是加密。
下面 下麪我來看一個 js 實現 md5 的一個例子:
上面的程式碼時被混淆過的,但是它的主要一個特徵還是比較明顯的,有一個入口函數:console.log(hex_md5("aiyuechuang"))
我們可以使用命令列執行一下結果,命令如下:
node md5.js
上面的程式碼自行復制儲存爲 md5.js 然後執行。
執行結果:
$ node md5.js
e55babec7f5d5cf7bac7872f0481bec1
我們數一下輸出的結果的話,會發現這正好 是 32位元,通常我們看到 32 位的一個英文數位混合的字串,應該馬上就能想到時 md5 了,這兩個操作的話,因爲在 Python 中都有對應的庫,分別是:Base64 和 hashlib ,大家應該都知道這個我就不多說了。
例程:Base64 和 hashlib
import base64
str1 = b'aiyuechuang'
str2 = base64.b64encode(str1)
print(str2)
str3 = base64.b64decode('YWl5dWVjaHVhbmc=')
print(str3)
輸出
b'YWl5dWVjaHVhbmc='
b'aiyuechuang'
[Finished in 0.2s]
import hashlib
data = "aiyuechuang"
result = hashlib.md5(data.encode(encoding = "UTF-8")).hexdigest()
print(result)
輸出
e55babec7f5d5cf7bac7872f0481bec1
[Finished in 0.1s]
像我們前面看到的那些程式碼,都是比較簡單的,他們的演算法部分也沒有經過修改,所以我們可以使用其他語言和對應的庫進行重寫。
但是如果對方把演算法部分做了一些改變呢?
如果程式碼量比較大也被混淆到看不出特徵了,連操作後產生的字串都看不出,我們就無法直接使用一個現成的庫來複寫操作了。
而且這種情況下的程式碼量太大了,直接對着程式碼重寫成 Python 版本也不太現實,對方一更新你就得再重新看一遍,這樣顯然時非常麻煩的,也非常耗時。
那麼有沒有一種更高效的方法呢?
顯然是有的,接下來我們來講如何通過程式來直接呼叫 JavaScript 程式碼,也就是碰到複雜的 JS 時候的處理。
首先,我會分享一些使用 Python 呼叫 JavaScript 的方式,然後會介紹一種效能更高的呼叫。以及具體使用哪種呼叫方式以及怎麼選擇性的使用,最後我會總結一下這些方案存在的小問題。並且會告訴你如何踩坑。
我們接下來首先講一下 Python 中呼叫 JavaScript。
Python 呼叫 JS 庫的話,光是我瞭解的話,目前就有這麼一堆,接下來我們就來依次來介紹這些庫。
首先來看一下什麼是 PyV8,V8 是谷歌開源的 JavaScript 引擎,被使用在了 Chrome 瀏覽器中,後來因爲有人想在 Python 上呼叫它(V8),於是就有了 PyV8。
那 PyV8 實際上是 V8 引擎的一個 Python 層的包裝,可以用來呼叫 V8 引擎執行 JS 程式碼,但是這個我不推薦使用它,那我既然不推薦大家使用,我爲什麼又要講它呢?
其實,是這樣的:
雖然目前網上有很多文章使用它執行 JS 程式碼,但是這個 PyV8 實際上已經年久失修了,而且它最新的一個正式版本還是 2010年的,可見是有多久遠了,鏈接在上方可以執行存取檢視。而且,如果你實際使用過的話,你應該會發現它存在一些記憶體漏失的問題。
所以,這邊我拿出來說一下,避免有人踩坑。接下來我們來說一下第二個 JS2Py。
Js2Py 是一個純 Python 實現的 JavaScript 直譯器和翻譯器,它和 PyV8 一樣,也是有挺多文章提到這個庫,然後來呼叫 JS 程式碼。
但是,Js2Py 雖然在2019年仍然更新,但那也是 6月份的事情了,而且它的 issues 裏面有很多的 bug 沒有修復(https://github.com/PiotrDabkowski/Js2Py/issues)。另外,Js2Py 本身也存在一些問題,就直譯器部分來說:
直譯器部分:
那不僅僅就直譯器部分,還有翻譯器部分:
總之來講,它在各個方面來說都不太適合我們的工作場景,所以也是不建議大家使用的。
這個庫也是一個 PyV8 引擎包裝,它的效果和 PyV8 的效果一樣的。
而且作者號稱這是一個繼任 PyExecJS 和 PyramidV8 的庫,乍眼一看挺唬人的,不過由於它是一個比較新的庫,我這邊就沒有過多的嘗試了,也沒有再實際生產環境中使用過,所以不太清楚會有什麼坑,感興趣的朋友,大家可以自己去嘗試一下。
接下來我要說的是 PyExecJS ,這個庫一個最開始誕生於 Ruby 中的庫,後來人被移植到了 Python 上,目前看到一些比較新的文章都是用它來執行 JS 程式碼的,然後它是有多個引擎可以選擇的,我們一般選擇 NodeJS 作爲它的一個引擎執行程式碼,畢竟 NodeJS 的速度是比較快的而且設定起來比較簡單,那我帶大家來看一下 PyExecjs 的使用。
安裝 JS 執行環境
這裏推薦安裝 Node.js,安裝方便,執行效率也高。
首先我們就是要安裝引擎了,這個引擎指的就是 JS 的一個執行環境,這邊推薦使用 Node.js。
注意:雖然 Windows 上有個系統自帶的 JScript,可以用來作爲 PyExecjs 的引擎,但是這個 JScript 很容易與其他的引擎有一個不一樣的地方,容易踩到一些奇奇怪怪的坑。所以請大家務必要安裝一個其他的引擎。比如說我們這裏安裝 Node.js 。
那上面裝完 Nodejs 之後呢,我們就需要執行安裝 PyExecjs 了:
pip install pyexecjs
這邊我們使用上面的 pip 就可以進行安裝了。
那麼我們現在環境就準備好了,可以開始執行了。
首先,我們開啓 IPython 終端,執行一下一下兩行程式碼,以下也給出了執行結果:
In [1]: import execjs
In [2]: execjs.get().name # 檢視呼叫環境
Out[2]: 'Node.js (V8)'
execjs.get() # 檢視呼叫的環境
用此來看看我們的庫能不能檢測到 nodejs,如果不能的話那就需要手動設定一下,不過一般像我上面一樣正常輸出 node.js
就可以了。
如果,你檢測出來的引擎不是 node.js
的話,那你就需要手動設定一下了,這裏有兩種設定形式,我在下方給你寫出來了:
選擇不同引擎進行解析
# 長期使用
os.environ["EXECJS_RUNTIME"]="Node"
# 臨時使用
import execjs.runtime_names
node=execjs.get(execjs.runtime_names.Node)
由上邊可知,我們有兩種形式:一種是長期使用的,通過環境變數的形式,通過把環境變數改成大寫的 EXECJS_RUNTIME 然後將其值賦值爲 Node。
另一種的話,將它改成臨時使用的一種方式,這種是直接使用 get,這種做法的話,你在使用的時候就需要使用 node 變數了,不能直接匯入 PyExecjs 來直接開始使用,相對麻煩一些。
接下來,就讓我們正式使用 PyExecJS 這個包吧。
In [8]: import execjs
In [9]: e = execjs.eval('a = new Array(1, 2, 3)') # 可以直接執行 JS 程式碼
In [10]: print(e)
[1, 2, 3]
PyExecjs 最簡單的用法就是匯入包,然後通過 eval 這個方法並傳入簡單的 JS 程式碼來執行。但是我們正常情況下肯定不會這麼使用,因爲我們的 JS 程式碼是比較複雜的而且 JS 程式碼內容也是比較多的。
# -*- coding: utf-8 -*-
# @Author: clela
# @Date: 2020-03-24 13:54:27
# @Last Modified by: aiyuechuang
# @Last Modified time: 2020-04-03 08:44:15
# @公衆號:AI悅創
In [12]: import execjs
In [13]: jstext = """
...: function hello(str){return str;}
...: """
In [14]: ctx = execjs.compile(jstext) # 編譯 JS 程式碼
In [15]: a = ctx.call("hello", "hello aiyc")
In [16]: print(a)
hello aiyc
這樣的話,我們一般通過使用第二種方式,第二種方式是通過使用 compile 對 JS 字串進行編譯,這個編譯操作其實就是把參數(jstext)裏面的那段 JS 程式碼給放到一個叫 Context 的上下文中,它並不是我們平時編譯程式所說的編譯。然後我們 呼叫 call 方法進行執行。
第一個參數是我們呼叫 JS 中的的函數名,也就是 hello。然後後面跟着的 hello aiyc 就是參數,也就是我們 JS 中需要傳入到 str 的參數。如果 JS 中存在多個參數,我們就直接在後面打個逗號,然後接着寫下一個參數就好了。
接下來我們來看一個具體的程式碼:
這邊我準備了一個 CryptoJS 的一個 JS 檔案,CryptoJS 它是一個包含各種加密雜湊編碼演算法的一個開源庫,很多網站都會用它提供的函數來生成參數,那麼這邊我是寫了如上面這樣的程式碼,用來呼叫它裏面的 AES 加密參數,來加密一下我提供的字串。
注意:JS 程式碼不要放在和 Python 程式碼同一個檔案中,儘量放在單獨的 js 檔案中,因爲我們的 JS 檔案內容比較多。然後通過讀取檔案的方式,
# Python 檔案:run_aes.py
# -*- coding: utf-8 -*-
# @時間 : 2020-04-06 00:00
# @作者 : AI悅創
# @檔名 : run_aes.py
# @公衆號: AI悅創
from pprint import pprint
import execjs
import pathlib
import os
js_path = pathlib.Path(os.path.abspath(os.path.dirname(__file__)))
js_path = js_path / "crypto.js"
with js_path.open('r', encoding="utf-8") as f:
script = f.read()
c = "1234"
# 傳入python中的變數
add = ('''
aesEncrypt = function() {
result={}
var t = CryptoJS.MD5("login.xxx.com"),
i = CryptoJS.enc.Utf8.parse(t),
r = CryptoJS.enc.Utf8.parse("1234567812345678"),
u = CryptoJS.AES.encrypt(''' + "'{}'".format(c) + ''',i, {
iv: r
});
result.t=t.toString()
result.i =i.toString()
result.r =r.toString()
result.u =u.toString()
return result
};
''')
script = script + add
print("script",script)
x = execjs.compile(script)
result = x.call("aesEncrypt")
print(result)
這裏我通過讀取檔案的方式,將 js 檔案讀取進來,把程式碼讀取到我們的字串裏面,這樣一方面方便我們管理,另一方面也可以直接通過程式碼檢測自動補全功能,使用起來會比較方便。
然後,這裏我們有一個小技巧,我們可以通過 format 字串拼接的形式,將 Python 中的變數,也就是上面的變數 c
然後將這個變數寫入到 Js 程式碼中,從而變相的實現了通過呼叫 JS 函數,在沒有參數的情況下修改 JS 程式碼中的特定變數的值。最後我們拼接好了我我們的 JS 程式碼(add 和 script)。
拼完 JS 程式碼之後,我們這邊再常規的進行一個操作,呼叫 Call 方法執行 aesEncrypt 這樣一個函數,需要注意的是,這個程式碼裏面 return 出來的 JS,它是一個 object,JS 中的 object 也就是 Python 中的字典。
我們實際使用時,如果需要在 Python 中拿到 object 的話,建議把它轉換成一個 json 字串,而不是直接的把結果 return 出來。
因爲,有些時候 PyExecjs 對 object 的轉換會出現問題,所以我們可能會拿到一些類似於將字典直接用 str 函數包裹後轉爲字串的一個東西,這樣的話它是無法通過正常的方式去解析的。
或者說你也可能會遇到其情況的報錯,總之大家最好先轉一下 json 字串,然後再 return 避免踩坑。這是我們的一個程式碼。
接下來我們來說一下,PyExecJS 存在的一些問題主要有以下兩點:
而如果參數不充分導致的話,有個很簡單的方法:就是把參數使用 Base64 編碼一下,因爲編碼之後出來的字串,我們知道 Base64 編碼之後是生成英文和數位組成的。這樣就沒有特殊符號了。所以就不會出現問題了。)
關於 PyExecejs 的相關東西就介紹到這裏了,我們來看一些其他的內容。
前面說的都是非瀏覽器環境下直接呼叫 JS 的操作,但是還有一些市面上根本沒人提到的騷操作,其實也挺好用的,接下來我給大家介紹一下:
這個大家是比較熟悉的,它是一個外部自動化的測試框架,可以驅動各種瀏覽器進行模擬人工操作,很多文章或者培訓班的課程,都會提到它在爬蟲方面的一個使用,比如用它採集一些動態頁面,或者用來過一些滑動驗證碼之類的。
不過我們這裏不用它來做這些事,我們要做的是用它來執行 JS 程式碼,因爲這樣的話是直接在瀏覽器環境下執行的 ,所以的話它是省了很多事,那麼 Selenium 執行 JS 的核心程式碼,實際上就下面 下麪一行:
js = "一大段 JS"
result = browser.execute_script(js)
我們來看一下實際的例子:
進入專案根目錄,輸入:python server.py
$ python server.py
* Serving Flask app "server" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Restarting with stat
* Debugger is active!
* Debugger PIN: 262-966-819
* Running on http://0.0.0.0:5002/ (Press CTRL+C to quit)
存取 localhost:5002
我們進入網頁之後,有這樣的一句話:
每次重新整理都會顯示不同的內容,檢視原始碼的話,會發現這個頁面中的原始碼裏面沒有對應頁面顯示的那句話,而是隻有一個 input 標籤。
我還能觀察到,input 標籤裏面有兩個屬性,一個是 id、一個是 data,這兩個是比較關鍵的屬性,然後我們還發現這裏面參照了一個 js 檔案,所以這個網頁最終結果實際上是通過 JS 檔案,然後一系列的操作生成的,那接下來我就來看看 JS 檔案,做了什麼工作。
我們可以看見,這個 JS 檔案最後一句,有一個 window.onload =doit
的這樣一程式碼,這個我們知道,當頁面載入完成之後,立即執行這個 JS 方法。
function doit() {
let browser_type=BrowserType();
console.log(browser_type)
let supporter =browser_type.supporter
if(supporter==="chrome"){
Base64.run('base64', 'data',supporter)
}
}
然後這個方法裏面做了一個這樣一個操作:let browser_type=BrowserType();
首先去判斷 supporter
是否等於 Chrome
這個 supporter
實際上有一個 browser_type
這個 browser_type
實際上就是檢測瀏覽器等一系列參數,然後我們獲取它裏面的 supporter
屬性,當 supporter
( supporter =browser_type.supporter
)等於 Chrome 的時候,我們再去執行這個 run 函數。
run: function (id, attr,supporter) {
let all_str = $(id).getAttribute(attr)
let end_index=supporter.length+58
Base64._keyStr = all_str.substring(0, end_index)
let charset = all_str.substring(64, all_str.length)
let encoded = Base64.decode(charset,supporter);
$(id).value = encoded;
}
也就是 run 函數裏面做了一系列操作,然後我傳入的 id 可以通過看一下上面的函數 doit
可知傳入的是 Base64
也就是說,實際上對 input 這個標籤做了一個取值的操作,然後到這邊我們就這整體一個過程將會用 JS 去模擬,所以這邊我就不細說了。
最終會把這樣的一個結果去通過 input.value 屬性把值複製到 input 中,也就是我們最終看到的那樣一個結果,到目前我就把這個 js 大概做了一件什麼樣的事情就已經講的差不多了。接下來我們去看一下 Selenium 這邊。
程式碼如下:
# -*- coding: utf-8 -*-
# @Time : 2020-04-01 20:56
# @Author : aiyuehcuang
# @File : demo.py
# @Software: PyCharm
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options
import time
def get_text(id,attr):
### 拼接字串注意{}要寫出{{}}
script=("""
let bt=BrowserType();
let id='{id}';
let attr='{attr}';
let supporter =bt.supporter;
const run=function(){{
let all_str = $(id).getAttribute(attr)
let end_index=supporter.length+58
Base64._keyStr = all_str.substring(0, end_index)
let charset = all_str.substring(64, all_str.length)
let encoded = Base64.decode(charset,supporter);
return encoded
}}
return run()
""").format(id=id,attr=attr)
return script
chrome_option = Options()
chrome_option.add_argument("--headless")
chrome_option.add_argument("--disable-gpu")
chrome_option.add_argument('--ignore-certificate-errors') # SSL儲存
browser = webdriver.Chrome(options=chrome_option)
wait = WebDriverWait(browser, 10)
# 啓動瀏覽器,獲取網頁原始碼
mainUrl = "http://127.0.0.1:5002/"
browser.get(mainUrl)
result=browser.execute_script(get_text("base64","data"))
print(result)
time.sleep(10)
browser.quit()
這邊關鍵的一行程式碼是:通過 execute_script(get_text("base64","data"))
這樣的一句話去執行這個函數,這個函數實際上就是返回一段 JS 程式碼,這邊實際上就是去模擬構造 run 所需要的一些參數,然後把最終的結果返回回去。
這裏有兩點需要注意:
我們可以執行一下程式碼,輸出結果如下:
$ python demo.py
DevTools listening on ws://127.0.0.1:59507/devtools/browser/edbe51d8-744d-447d-9304-e9551a2a6421
[0407/184920.601:INFO:CONSOLE(286)] "[object Object]", source: http://127.0.0.1:5002/static/js/base64.js (286)
生活不是等待暴風雨過去,而是要學會在雨中跳舞。
我們可以看到,我們程夠獲取到了結果。
這個例子因爲它用到了檢測瀏覽器的屬性,而且它檢測完屬性之後會把屬性值一直往下傳,我們可以從上面的程式碼中看到它有很多地方使用。
所以,如果我們用 PyExecjs 來寫的話,就需要修改很多參數,這樣就很不方便了。因爲我們需要去模擬這些瀏覽器參數,我這邊寫的例子比較簡單,像那種更加複雜的。像獲取更多的瀏覽器的一個屬性的話,用 PyExecjs 再去寫的時候,可能沒有瀏覽器這樣的一個環境,所以 PyExecjs 沒有 Selenium 有優勢。
當然,除了 Selenium 以爲,還有一個叫做 Pyppeteer 的庫,也是比較常見。
爲了控制文章篇幅,咱們下次再續咯,記得關注公衆號:AI悅創!