聲明:只是因為工作中需要,且基本不會對別人的網(wǎng)站構(gòu)成什么不好的影響,做個思路記錄?。?!
尊重網(wǎng)站所有者、控制請求頻率、遵守網(wǎng)站規(guī)則、尊重個人隱私
平常工作中難免會遇到需要爬取別人網(wǎng)站的需求,例如爬取兌換之類的流程,把接口爬取下來封裝到項目中,這種一般不會對別人的網(wǎng)站有什么的影響,因為也是正常流程下單,例如下面的活動截圖:
- 1、先理解需求,從需求和界面上看,很容易知道我們基本只需要獲取兩個接口,一個是發(fā)送驗證碼接口,一個是兌換接口。當然也可能存在前置接口,具體情況具體分析
- 2、先不要急著分析網(wǎng)站源碼,可以先嘗試找個手機號發(fā)送驗證碼,看看請求了哪些接口,接口的參數(shù)是什么,返回的數(shù)據(jù)是什么,例如:
- 3、從請求接口的參數(shù)的返回分析,這個網(wǎng)站的數(shù)據(jù)部分接口需要加密,部分不需要,如果不需要加密的接口,基本很容易就獲取請求參數(shù),這里可以大膽猜測accessId、reqId可能為uuid,reqMethod為請求的方法,encrypted表示接口請求參數(shù)是否需要加密,但是光這樣看有點難以看出data和dataKey是什么加密算法加密而成,dataKey大概率是解密data的關(guān)鍵密鑰。這里建議一般情況下看一下加密后的數(shù)據(jù)的長度,是不是一些比較特殊的長度,比如36位、512位之類的長度,可以根據(jù)這個長度大膽猜測是否是平常常用的算法。
- 4、對網(wǎng)站有個大概的數(shù)據(jù)交互認識之后,我們就可以知道難點基本在數(shù)據(jù)加密這一塊,這時候就可以來看源碼了,這里的思路我一般是直接定位到“發(fā)送驗證碼”的按鈕上,然后找到F12控制臺元素的事件監(jiān)聽器的click事件上,直接進入相關(guān)的代碼中,就可以看到這個按鈕的點擊事件的代碼了。
- 5、網(wǎng)站的代碼都是經(jīng)過編譯的,且一般線上是不會直接開放源代碼的,這種時候如何光看編譯過后的代碼,有時候是比較難以理解,這種時候就需要打斷點,一步步調(diào)試了,看函數(shù)的參數(shù)和返回值,以及大概的處理過程。
- 6、前面已經(jīng)分析過了,難點就在加密算法上,經(jīng)過代碼分析找到加密部分的代碼,如下圖:
從這段的代碼中,存在RSA和AES的字眼,且打斷點都走了這些函數(shù),可以看出這個網(wǎng)站是AES+RSA結(jié)合使用進行加密的。
AES+RSA:使用RSA秘鑰生成工具生成一對公鑰(A)和私鑰(B),前端保留A,后端保留B。
前端發(fā)送數(shù)據(jù)時,先生成一串隨機16位字符串作為AES的秘鑰(C),然后使用A使用RSA算法對C進行加密,得到加密后的AES秘鑰(D)。將要發(fā)送的數(shù)據(jù)(E)用C使用AES加密,得到密文(F)。將D和F一同發(fā)給后端進行處理。
后端處理數(shù)據(jù)時,先用B對D使用RSA進行解密得到C,用C對F使用AES進行解密得到E,處理后得到結(jié)果G,再用C對G進行AES加密得到H,將H返回給前端。
前端接收到H后用C進行解密,得到處理的結(jié)果G。
根據(jù)AES和RSA的配合流程來看,這里沒有直接返回RSA公鑰,而是根據(jù)摸和指數(shù)來生成RSA公鑰然后加密AES私鑰,用AES私鑰去加密數(shù)據(jù)進行傳輸,但是從前面請求參數(shù)來看,跟我們平常看到的AES和RSA字符串不一樣,顯然還做了相關(guān)的轉(zhuǎn)換,dataKey字段只有數(shù)字和小寫字母,平??吹降募用軘?shù)據(jù)還有大寫字母、+、/等。文章來源:http://www.zghlxwxcb.cn/news/detail-729028.html
- 7、這里有一個比較難的地方就是“new no.RSAKeyPair(e,“”,t,2048)”這個對象,這個對象去加密隨機生成的AES密鑰生成dataKey字段,這里我也是看了半天,打斷點半天,也難以理解,因為編譯后很多變量是單個字母,且你需要換算成你自己項目的語言,如JAVA、Python等,有時候是比較難的,這時候你可能會想我能不能反編譯一下js文件,那你可以嘗試一下,比如找一些在線的網(wǎng)站上傳文件看看能不能反編譯,希望你能成功??
- 8、遇到難點無法繼續(xù)下的時候怎么辦呢?給大家提供一種思路,就是嘗試直接拷貝它的js文件下來,然后在項目去執(zhí)行它的函數(shù),獲取結(jié)果,我這里是使用JAVA、jdk1.8版本,由JavaScript引擎去執(zhí)行js文件中的函數(shù),根據(jù)上面的截圖,我只需要執(zhí)行Fp()函數(shù)即可拿到dataKey,和隨機生成的AES私鑰。
當然你如果直接執(zhí)行拷貝下來的js文件大概率是失敗,會報各種錯誤,例如jdk1.8不支持ES6語法,你需要先轉(zhuǎn)化成ES5語法格式(es6轉(zhuǎn)es5在線網(wǎng)站:https://babeljs.io/repl,需要勾選左側(cè)下面的"FORCE ALL TRANSFORMS"),其次你需要把不需要用到的代碼都刪掉,只留加密部分的代碼,可以根據(jù)入口函數(shù)一步步找到所有需要的變量和函數(shù)等,可以根據(jù)報錯一步步處理,這也是個小挑戰(zhàn)~
public static void main(String[] args) {
// 創(chuàng)建JavaScript引擎
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("js");
try {
// 加載JavaScript文件
engine.eval(new FileReader("C:\\Users\\win\\Desktop\\index2.js"));
// 執(zhí)行JavaScript函數(shù)
Object result = engine.eval("Fp()");
// 這里是我習慣用serr打印測試日志,因為標紅比較顯眼
System.err.println("Function Result: " + JSONObject.toJSONString(result));
// 拿到Fp()函數(shù)返回的dataKey等數(shù)據(jù)
JSONObject object = JSONObject.parseObject(JSONObject.toJSONString(result));
// 要加密的數(shù)據(jù)
String content = "{\"appId\":\"xxxx\"}";
System.err.println("加密前的數(shù)據(jù):" + content);
// 加密后的數(shù)據(jù)
String encryptAES = encryptAES(content, object.getString("key"), object.getString("iv"));
System.err.println("加密后的數(shù)據(jù):" + encryptAES);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* AES加密
* @param plaintext 要加密的數(shù)據(jù)
* @param key
* @param iv
*/
public static String encryptAES(String plaintext, String key, String iv) {
try {
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec);
byte[] encryptedBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
return bytesToHex(encryptedBytes).toUpperCase();
} catch (Exception e) {
log.error("AES加密異常,{}", e.getMessage());
return "";
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02X", b));
}
return result.toString();
}
- 9、終極方案:無頭瀏覽器?。?!如果上面還不能解決你的問題,你可以嘗試使用無頭瀏覽器,模擬人工點擊操作,當然這比接口慢很多,但你不需要考慮網(wǎng)站的加密和接口調(diào)用等等,按照人工操作的流程執(zhí)行即可。注意,無頭瀏覽器你可能需要考慮代理ip和請求頭問題,盡可能模擬真實的操作,一直使用同一個ip和userAgent有可能會被檢測到。
/**
* 獲取瀏覽器驅(qū)動
*/
private WebDriver getDriver(Param pm) {
// 設置隨機的設備 user-agent 和屏幕大小
Map<String, Object> mobileEmulation = new HashMap<>();
Map<String, Object> map = new HashMap<>();
Dimension randomScreenSize = getRandomScreenSize();
map.put("width", randomScreenSize.getWidth());
map.put("height", randomScreenSize.getHeight());
mobileEmulation.put("deviceMetrics", map);
if (pm != null && Func.isNotBlank(pm.getUserAgent())) {
// 設置UA
mobileEmulation.put("userAgent", pm.getUserAgent());
}
//谷歌瀏覽器設置
ChromeOptions options = new ChromeOptions();
// 隱藏瀏覽器、無頭瀏覽器
options.addArguments("-headless");
options.setExperimentalOption("mobileEmulation", mobileEmulation);
// 打開F12
// options.addArguments("--auto-open-devtools-for-tabs");
// 啟動無沙盒模式運行,以最高權(quán)限運行
options.addArguments("--no-sandbox");
// 不加載圖片, 提升速度
options.addArguments("blink-settings=imagesEnabled=false");
options.addArguments("--disable-dev-shm-usage");
// 禁用gpu渲染
options.addArguments("--disable-gpu");
// 禁用阻止彈出窗口
options.addArguments("--disable-popup-blocking");
// 禁用擴展
options.addArguments("disable-extensions");
// 禁用默認瀏覽器檢查
options.addArguments("no-default-browser-check");
// 忽略不信任證書
options.addArguments("–ignore-certificate-errors");
// 禁用自動化控制特性
options.addArguments("--disable-blink-features=AutomationControlled");
// 禁用通知
options.addArguments("--disable-notifications");
// 禁用信息欄
options.addArguments("--disable-infobars");
// 禁用日志記錄
options.addArguments("--disable-logging");
LoggingPreferences logPrefs = new LoggingPreferences();
logPrefs.enable(LogType.PERFORMANCE, Level.ALL);
options.setCapability("goog:loggingPrefs", logPrefs);
// 禁用默認應用程序
options.addArguments("--disable-default-apps");
// 靜音
options.addArguments("--mute-audio");
// 禁用自動化標志
options.setExperimentalOption("excludeSwitches", new String[]{"enable-automation"});
// 設置代理ip
if (pm != null && Func.isNotBlank(pm.getProxyIp())) {
String proxyAddress = pm.getProxyIp() + ":" + pm.getProxyPort();
Proxy proxy = new Proxy();
proxy.setHttpProxy(proxyAddress);
proxy.setSslProxy(proxyAddress);
options.setProxy(proxy);
}
return new ChromeDriver(options);
}
谷歌瀏覽器驅(qū)動路徑:https://googlechromelabs.github.io/chrome-for-testing/#stable文章來源地址http://www.zghlxwxcb.cn/news/detail-729028.html
public void handle(Param pm) {
WebDriver driver = null;
try {
if (Func.isBlank(pm.getUserAgent())) {
// 這個工具類只是隨機生成userAgent的工具類
pm.setUserAgent(UserAgentUtil.getUserAgent());
}
// 谷歌瀏覽器驅(qū)動路徑,注意驅(qū)動版本問題
System.setProperty("webdriver.chrome.driver", "F:/chromedriver-win64/chromedriver.exe");
driver = getDriver(pm);
// driver.manage().window().maximize();
// 進入頁面
driver.get("https://xxx");
JavascriptExecutor js = (JavascriptExecutor) driver;
//添加cookie
if (pm.getCookies() != null && pm.getCookies().size() > 0) {
setCookie(js, pm.getCookies());
}
// 隱式等待
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
// 其他相關(guān)處理代碼
...
} catch (Exception e) {
e.printStackTrace();
} finally {
if (driver != null) {
driver.quit();
}
}
}
到了這里,關(guān)于關(guān)于工作中爬取網(wǎng)站的一些思路記錄的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!