selenium 獲取請求返回內容的解決方案


提出問題

之前我的一篇博客說的是怎么利用 selenium 來做自動化監控。當出現異常時,我們需要記錄頁面源碼、網絡請求數據、截圖等信息來方便我們診斷問題,基本上就夠用了。但是,這兩天遇到一個棘手的異常,時不時頁面會彈出:“系統繁忙,請稍候再試!”,這時候我們去看網絡請求數據,結果狀態碼全部都是 200,沒有其它信息,這壓根沒法定位不了問題。

這就說明:網絡出現異常的時候,僅靠狀態碼是不夠的。我們最好能拿到 http 所有數據,包括:請求頭、響應頭、請求體、響應體。其中請求頭、響應頭,可以通過 PERFORMANCE_LOG 拿到,問題都不大。但是請求體與響應體,我們可以拿到么?

分析過程

這個問題困擾了我整整一天的時間,終於解決了。為什么這么困難?

我們先來看 selenium,它為什么不直接支持這個功能呢?因為開發人員覺得這不是他們目標:

we will not be adding this feature to the WebDriver API as it falls outside of our current scope (emulating user actions). 

然后我繼續翻網絡,發現谷歌的devtools-protocol明確是支持的:

imagepng

那我有沒有什么辦法能調用這兩個方法呢?這就很麻煩了,我根據這篇文章的思路去直連谷歌的 Remote Port。

看這篇文章真的很美,但實際上到我這個項目並不可行,為什么?
原因在於這篇文章所用的PyChromeDevTools是基於 websocket 的,而且是在請求一個鏈接后,立即去讀取 Chrome 吐出來的響應數據。

而在監控這種場景下,是在請求已經完成之后才會收集 PerformanceLog,然后根據其中的請求 ID 去問 Chrome 要數據。一個是推,一個是拉,這是兩種模式。所以非常不幸,解決不了我的問題。

但是給我了我一個思路,我去找找有沒有類似 Java 的組件。這時候,我從 GitHub 上找到了cdp4j,這是一個跟 Chrome 打交道的包,它有一個很迷人的 API:

// 獲取請求返回內容 session.getCommand().getNetwork().getResponseBody("requestIdxxxxx"); 

這個方法我試驗了很久,結果仍然不行,調用時一直返回的是:
No resource with given identifier found

我確認了很久,確認 requestId 是沒有問題的,為什么拿不到數據?我試了很久,最后放棄了,因為我發現是這樣的:

Java 的 Selenium 通過 chromedriver 開啟了一個與 Chrome 的 session,cdp4j 是沒有辦法直接綁到這個 session 上面的(理論上是可能的,但是 cdp4j 的擴展性太差,我實在懶得去改)。這就意味着 chromdriver 的請求數據無法通過 cdp4j 來獲取到。

既然 Java 的 Selenium 其實沒並有直連 Chrome,而是通過 chromedriver 去跟 Chrome 打交道的。我們能不能從 chromedriver 上看看有沒有直接獲取 responseBody 的接口呢?

所以,我開始找 chromedriver 的文檔,文檔真的非常少。不知道從哪里我了解到 chromedriver 是根據 w3c 的協議開發的,我看看 w3c 的webdriver協議里能不能找到答案。

結果仍然很讓人沮喪,我翻了很久,發現 w3c 的 webdriver 協議沒有定義 Network 相關的操作。

然后我就開始仔細分析 selenium 的源碼,發現了 AbstractHttpCommandCodec 里有與 chromedriver 相關的操作定義。

/** * A command codec that adheres to the W3C's WebDriver wire protocol. * * @see <a href="https://w3.org/tr/webdriver">W3C WebDriver spec</a> */ public abstract class AbstractHttpCommandCodec implements CommandCodec<HttpRequest> { //... public AbstractHttpCommandCodec() { defineCommand(STATUS, get("/status")); defineCommand(GET_ALL_SESSIONS, get("/sessions")); defineCommand(NEW_SESSION, post("/session")); defineCommand(GET_CAPABILITIES, get("/session/:sessionId")); defineCommand(QUIT, delete("/session/:sessionId")); // ... // Mobile Spec defineCommand(GET_NETWORK_CONNECTION, get("/session/:sessionId/network_connection")); defineCommand(SET_NETWORK_CONNECTION, post("/session/:sessionId/network_connection")); defineCommand(SWITCH_TO_CONTEXT, post("/session/:sessionId/context")); defineCommand(GET_CURRENT_CONTEXT_HANDLE, get("/session/:sessionId/context")); defineCommand(GET_CONTEXT_HANDLES, get("/session/:sessionId/contexts")); } // ... } 

解讀源碼后發現,其實這些操作就是發送 get/post 請求到 chromedriver,由 chromedriver 來處理,這里沒有我們想要的接口。但是給我一個思路,如果我能拿到 chromedriver 的所有接口,是不是就可以確認有沒有我們想要的 getResponseBody 接口呢?

嘿嘿,這是個很大的突破口。其實早該想到的,直接去看的源碼,找出所有暴露的接口:

# https://github.com/bayandin/chromedriver/blob/master/server/http_handler.cc //... CommandMapping(kDelete, "session/:sessionId", base::BindRepeating( &ExecuteSessionCommand, &session_thread_map_, "Quit", base::BindRepeating(&ExecuteQuit, false), true)), // No W3C equivalent. CommandMapping(kDelete, "session/:sessionId/session_storage", WrapToCommand("ClearSessionStorage", base::BindRepeating(&ExecuteClearStorage, kSessionStorage))), CommandMapping(kPost, "session/:sessionId/chromium/send_command", WrapToCommand("SendCommand", base::BindRepeating(&ExecuteSendCommand))), CommandMapping( kPost, "session/:sessionId/goog/cdp/execute", WrapToCommand("ExecuteCDP", base::BindRepeating(&ExecuteSendCommandAndGetResult))), CommandMapping( kPost, "session/:sessionId/chromium/send_command_and_get_result", WrapToCommand("SendCommandAndGetResult", base::BindRepeating(&ExecuteSendCommandAndGetResult))), //... 

看到上面的"session/:sessionId/goog/cdp/execute"了么,興不興奮?
雖然沒能找到我們想要的 Network.getResponseBody,但是我們得到了一個可以執行所有 Chrome Devtool 協議的通用接口!真是不枉費我花了這么久,然后我們看看要傳什么參數,找 ExecuteSendCommandAndGetResult 的實現:

Status ExecuteSendCommandAndGetResult(Session* session, WebView* web_view, const base::DictionaryValue& params, std::unique_ptr<base::Value>* value, Timeout* timeout) { std::string cmd; if (!params.GetString("cmd", &cmd)) { return Status(kInvalidArgument, "command not passed"); } const base::DictionaryValue* cmdParams; if (!params.GetDictionary("params", &cmdParams)) { return Status(kInvalidArgument, "params not passed"); } return web_view->SendCommandAndGetResult(cmd, *cmdParams, value); } 

根據代碼,我只要傳 cmd 與 params 命令就可以調用這個接口了。我們在 Postman 里試一試:

imagepng

總算成功了!一天已經過去了,不過沒有白費。

接下來我們只要轉化到代碼里就行了。一開始我試圖集成進 Selenium 的 AbstractHttpCommandCodec,結果沒能成功。原因有兩個,一個是 Selenium 擴展性太差,沒有辦法直接增加進去; 另一個原因,我修改源碼覆蓋的時候發現有一些奇奇怪怪的問題。

解決方案

最后,我就用 HttpClient 調用的方式來實現了。源碼如下:

public class ChromeDriverProxy extends ChromeDriver { private static final int COMMAND_TIMEOUT = 5000; // 必須固定端口,因為ChromeDriver沒有實時獲取端口的接口; private static final int CHROME_DRIVER_PORT = 9999; private static ChromeDriverService driverService = new ChromeDriverService.Builder().usingPort(CHROME_DRIVER_PORT).build(); public ChromeDriverProxy(ChromeOptions options) { super(driverService, options); } // 根據請求ID獲取返回內容 public ResponseBodyVo getResponseBody(String requestId) { ResponseBodyVo result = null; try { // CHROME_DRIVER_PORT chromeDriver提供的端口 String url = String.format("http://localhost:%s/session/%s/goog/cdp/execute", CHROME_DRIVER_PORT, getSessionId()); HttpPost httpPost = new HttpPost(url); JSONObject object = new JSONObject(); JSONObject params = new JSONObject(); params.put("requestId", requestId); object.put("cmd", "Network.getResponseBody"); object.put("params", params); httpPost.setEntity(new StringEntity(object.toString())); RequestConfig requestConfig = RequestConfig .custom() .setSocketTimeout(COMMAND_TIMEOUT) .setConnectTimeout(COMMAND_TIMEOUT).build(); CloseableHttpClient httpClient = HttpClientBuilder.create() .setDefaultRequestConfig(requestConfig).build(); HttpResponse response = httpClient.execute(httpPost); JSONObject data = JSONObject.parseObject(EntityUtils.toString(response.getEntity())); return JSONObject.toJavaObject(data, ResponseBodyVo.class); } catch (IOException e) { logger.error("getResponseBody failed!", e); } return result; } } 

這樣就完成了網絡請求返回內容的處理。

調用方法:

 public static List<String> saveHttpTransferDataIfNecessary(ChromeDriverProxy driver) { Logs logs = driver.manage().logs(); Set<String> availableLogTypes = logs.getAvailableLogTypes(); if(availableLogTypes.contains(LogType.PERFORMANCE)) { LogEntries logEntries = logs.get(LogType.PERFORMANCE); List<ResponseReceivedEvent> responseReceivedEvents = new ArrayList<>(); for(LogEntry entry : logEntries) { JSONObject jsonObj = JSON.parseObject(entry.getMessage()).getJSONObject("message"); String method = jsonObj.getString("method"); String params = jsonObj.getString("params"); if (method.equals(NETWORK_RESPONSE_RECEIVED)) { ResponseReceivedEvent response = JSON.parseObject(params, ResponseReceivedEvent.class); responseReceivedEvents.add(response); } } doSaveHttpTransferDataIfNecessary(driver, responseReceivedEvents); } } // 保存網絡請求 private static void saveHttpTransferDataIfNecessary(ChromeDriverProxy driver, List<ResponseReceivedEvent> responses) { List<String> content = new ArrayList<>(1024); for(ResponseReceivedEvent response : responses) { String url = response.getResponse().getUrl(); boolean staticFiles = url.endsWith(".png") || url.endsWith(".jpg") || url.endsWith(".css") || url.endsWith(".ico") || url.endsWith(".js") || url.endsWith(".gif"); if(!staticFiles && url.startsWith("http")) { content.add(url); content.add(response.getResponse().getRequestHeadersText()); content.add(response.getResponse().getHeadersText()); // 使用上面開發的接口獲取返回數據 ResponseBodyVo body = driver.getResponseBody(response.getRequestId()); if(body != null && body.getStatus() == 0) { content.add("base64Encoded:" + body.getValue().getBase64Encoded()); content.add("body:\n" + body.getValue().getBody()); } content.add("\n"); } } // 寫文件至本地 } 

至於 getRequestPostData 也是類似的邏輯,這樣不再贅述。

參考資料

https://github.com/ChromeDevTools/awesome-chrome-devtools#developing-with-the-protocol
https://github.com/marty90/PyChromeDevTools/blob/master/PyChromeDevTools
https://yq.aliyun.com/articles/656018
https://github.com/webfolderio/cdp4j
https://stackoverflow.com/questions/6509628/how-to-get-http-response-code-using-selenium-webdriver
https://stackoverflow.com/questions/28430479/using-google-chrome-remote-debugging-protocol
https://chromedevtools.github.io/devtools-protocol/tot/Network
https://github.com/bayandin/chromedriver/
https://github.com/seleniumhq/selenium-google-code-issue-archive/issues/141
https://www.w3.org/TR/webdriver/#take-element-screenshot



作者:xjlnjut730
鏈接:https://hacpai.com/article/1546004185689
來源:黑客派


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM