selenium获取请求返回内容的解决方案

selenium获取请求返回内容的解决方案 有更新!

 zz:http://xiajl.cn/articles/2018/12/28/1546004181844.html

 

提出问题

之前我的一篇博客说的是怎么利用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接口呢?

嘿嘿,这是个很大的突破口。其实早该想到的,直接去看chromedriver的源码,找出所有暴露的接口:

# 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

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值