python selenium chrome获取每个请求内容_selenium 获取请求返回内容的解决方案

本文详细介绍了如何在使用Python的selenium时,通过ChromeDriver获取每个网络请求的完整内容,包括请求头、响应头、请求体和响应体。在遇到系统繁忙等异常情况时,这种方法能够帮助诊断问题。作者分析了selenium不直接支持此功能的原因,并通过查阅资料和源码,找到了使用chromedriver的接口来实现这一目标,最终通过HttpClient调用实现了获取网络请求返回内容的功能。
部署运行你感兴趣的模型镜像

提出问题

之前我的一篇博客说的是怎么利用 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).

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

然后我继续翻网络,发现谷歌的devtools-protocol明确是支持的:

那我有没有什么办法能调用这两个方法呢?这就很麻烦了,我根据这篇文章的思路去直连谷歌的 Remote Port。

看这篇文章真的很美,但实际上到我这个项目并不可行,为什么?

原因在于这篇文章所用的PyChromeDevTools是基于 websocket 的,而且是在请求一个链接后,立即去读取 Chrome 吐出来的响应数据。

而在监控这种场景下,是在请求已经完成之后才会收集 PerformanceLog,然后根据其中的请求 ID 去问 Chrome 要数据。一个是推,一个是拉,这是两种模式。所以非常不幸,解决不了我的问题。

但是给我了我一个思路,我去找找有没有类似 Java 的组件。这时候,我从 GitHub 上找到了cdp4j,这是一个跟 Chrome 打交道的包,它有一个很迷人的 API:

// 获取请求返回内容

session.getCommand().getNetwork().getResponseBody("requestIdxxxxx");

// 获取请求返回内容 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 W3C WebDriver spec

*/

public abstract class AbstractHttpCommandCodec implements CommandCodec {

//...

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"));

}

// ...

}

/** * A command codec that adheres to the W3C's WebDriver wire protocol. * * @see W3C WebDriver spec */ public abstract class AbstractHttpCommandCodec implements CommandCodec { //... 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))),

//...

# 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<: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);

}

Status ExecuteSendCommandAndGetResult(Session* session, WebView* web_view, const base::DictionaryValue& params, std::unique_ptr<: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 里试一试:

总算成功了!一天已经过去了,不过没有白费。

接下来我们只要转化到代码里就行了。一开始我试图集成进 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 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 saveHttpTransferDataIfNecessary(ChromeDriverProxy driver) {

Logs logs = driver.manage().logs();

Set availableLogTypes = logs.getAvailableLogTypes();

if(availableLogTypes.contains(LogType.PERFORMANCE)) {

LogEntries logEntries = logs.get(LogType.PERFORMANCE);

List 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 responses) {

List 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");

}

}

// 写文件至本地

}

public static List saveHttpTransferDataIfNecessary(ChromeDriverProxy driver) { Logs logs = driver.manage().logs(); Set availableLogTypes = logs.getAvailableLogTypes(); if(availableLogTypes.contains(LogType.PERFORMANCE)) { LogEntries logEntries = logs.get(LogType.PERFORMANCE); List 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 responses) { List 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 也是类似的逻辑,这样不再赘述。

参考资料

作者:xjlnjut730

链接:https://hacpai.com/article/1546004185689

您可能感兴趣的与本文相关的镜像

AutoGPT

AutoGPT

AI应用

AutoGPT于2023年3月30日由游戏公司Significant Gravitas Ltd.的创始人Toran Bruce Richards发布,AutoGPT是一个AI agent(智能体),也是开源的应用程序,结合了GPT-4和GPT-3.5技术,给定自然语言的目标,它将尝试通过将其分解成子任务,并在自动循环中使用互联网和其他工具来实现这一目标

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值