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
来源:黑客派

posted @   小强找BUG  阅读(22449)  评论(3编辑  收藏  举报
编辑推荐:
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示