地球人都知道,如果使用selenium时要修改user-agent可以在启动浏览器时添加配置项,如chromeOptions.addArguments("user-agent=xxx");。但是如何在每次请求的时候动态更改user-agent呢?

经过我的不懈努力,终于在网上找到一个相关的信息使用python3和selenium4修改chrome的user-agent

这里面提到了使用driver.execute_cdp_cmd来切换,这让我了解了一下cdp命令。简单的来说,cdp命令时chrome支持的一种基于websocket的协议,通过这个协议可以与浏览器内核通信。平常使用的F12浏览器开发工具就是基于cdp的。cpd命令可以实现的功能很多,可以参考Chrome DevTools Protocol。再这里面我找到了一个Network.setUserAgentOverride命令可以修改请求user-agent。

命令的参数如下:



但是,在我的项目中目前使用的selenium-java的版本是3.141.59,这个版本还没用提供对于cdp命令的支持,前面信息中提到了是在selenium4中使用使用的cdp命令。于是我又去maven仓库搜索有没有selenium4的jar包可以用。



这里面已经有5个alpha测试的版本了,虽然还不是稳定版本,但是为了实现新功能先试一试,在maven中添加依赖:

<!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.0.0-alpha-5</version>
</dependency>

然后尝试调用ChromeDriverexecuteCdpCommand方法



可以看到,第一个参数是commandName,对于修改user-agent的需求,这个地方应该填写Network.setUserAgentOverride,后面是参数的键值对,现在只需要填写userAgent就可以了,其他都是不需要的可选参数。

一般情况下,问题到这里就解决了,但是在我的项目中却没有这么简单。因为各种原因,我的项目中使用的是Selenium-Server来提供浏览器环境的,也就是说,创建的都是RemoteWebDriver,虽然ChromeDriver是继承自RemoteWebDriver的,但是cdp命令是chrome浏览器独有的, 因此RemoteWebDriver也就没有提供相关的支持了。那么如何在确定RemoteWebDriver调用chrome浏览器的情况下提供cpd命令的支持呢?为了实现这个功能还是费了一些时间,因此在这里把过程记录下来。

首先看一下ChromeWebDriver是如何实现cdp命令的:

public Map<String, Object> executeCdpCommand(String commandName, Map<String, Object> parameters) {
Objects.requireNonNull(commandName, "Command name must be set.");
Objects.requireNonNull(parameters, "Parameters for command must be set.");
Map<String, Object> toReturn = (Map)this.getExecuteMethod().execute("executeCdpCommand", ImmutableMap.of("cmd", commandName, "params", parameters));
return ImmutableMap.copyOf(toReturn);
}

在Selenium4中,ChromeDriver继承自ChromiumDriver,二者其实是一模一样的。ChromiumDriver提供了cdp命令的支持,利用executeMethod运行命令executeCdpCommand,将要运行的具体命令和参数一并传入。于是我又开始找这个ExecuteMethod是什么东西,发现ChromiumWebDriver并没有对这个参数进行任何设置,因此应该是在ChromiumDriver继承的RemoteWebDriver来设置的。果然,在RemoteWebDriver中有this.executeMethod = new RemoteExecuteMethod(this);,在ChromiumWebDriver中获取到的一定也是这个对象。那么很容易想到,继承一个RemoteWebDriver并编写一个方法调用这个executeMethod不就行了吗?

public class CdpRemoteWebDriver extends RemoteWebDriver {

    public CdpRemoteWebDriver(URL remoteAddress, Capabilities capabilities) {
super(remoteAddress, capabilities);
} public Map<String, Object> executeCdpCommand(String commandName, Map<String, Object> parameters) {
Objects.requireNonNull(commandName, "Command name must be set.");
Objects.requireNonNull(parameters, "Parameters for command must be set.");
Map<String, Object> toReturn = (Map)this.getExecuteMethod().execute("executeCdpCommand", ImmutableMap.of("cmd", commandName, "params", parameters));
return ImmutableMap.copyOf(toReturn);
} }

然后再创建CdpRemoteWebDriver实例,在访问网页之前设置user-agent

Map uaMap = new HashMap(){{
put("userAgent", "customUserAgent");
}};
((CdpRemoteWebDriver) driver).executeCdpCommand("Network.setUserAgentOverride",
uaMap
);
driver.get(url);

运行试一下!

org.openqa.selenium.UnsupportedCommandException: executeCdpCommand
Build info: version: '4.0.0-alpha-5', revision: 'b3a0d621cc'
System info: host: 'DESKTOP-BM176Q1', ip: '192.168.137.1', os.name: 'Windows 10', os.arch: 'amd64', os.version: '10.0', java.version: '1.8.0_161'
Driver info: driver.version: CdpRemoteWebDriver
at org.openqa.selenium.remote.codec.AbstractHttpCommandCodec.encode(AbstractHttpCommandCodec.java:246)
at org.openqa.selenium.remote.codec.AbstractHttpCommandCodec.encode(AbstractHttpCommandCodec.java:129)
at org.openqa.selenium.remote.HttpCommandExecutor.execute(HttpCommandExecutor.java:155)
at org.openqa.selenium.remote.RemoteWebDriver.execute(RemoteWebDriver.java:582)
at org.openqa.selenium.remote.RemoteWebDriver.execute(RemoteWebDriver.java:639)
at org.openqa.selenium.remote.RemoteExecuteMethod.execute(RemoteExecuteMethod.java:36)
at com.zju.edu.eagle.accessibilitycheck.a11ycheck.executor.impl.CdpRemoteWebDriver.executeCdpCommand(CdpRemoteWebDriver.java:24)

结果不行,说executeCdpCommand是不支持的命令,为什么一样的executeMethod结果不一样呢?定位到错误的地方看一下

public HttpRequest encode(Command command) {
String name = (String)this.aliases.getOrDefault(command.getName(), command.getName());
AbstractHttpCommandCodec.CommandSpec spec = (AbstractHttpCommandCodec.CommandSpec)this.nameToSpec.get(name);
if (spec == null) {
throw new UnsupportedCommandException(command.getName());
}
...
}

运行命令时,先从(AbstractHttpCommandCodec.CommandSpec)this.nameToSpec中获取命令的相关信息了,而要运行的executeCdpCommand没有事先定义,所以就出现异常了。

public AbstractHttpCommandCodec() {
this.defineCommand("status", get("/status"));
this.defineCommand("getAllSessions", get("/sessions"));
this.defineCommand("newSession", post("/session"));
this.defineCommand("getCapabilities", get("/session/:sessionId"));
...
}

这些命令是在AbstractHttpCommandCodec中定义的,而executeCdpCommand不在其中。这说明虽然ChromeDriver和RemoteWebDriver有相同的executeMethod,但后续调用还是涉及到了不同的类,于是我又回头查看ChromeDriver中的代码,发现有这样一个构造函数

public ChromeDriver(ChromeDriverService service, Capabilities capabilities) {
super(new ChromiumDriverCommandExecutor(service), capabilities, "goog:chromeOptions");
}

这里面创建了一个ChromiumDriverCommandExecutor,再点进来看一下

static {
CHROME_COMMAND_NAME_TO_URL.put("launchApp", new CommandInfo("/session/:sessionId/chromium/launch_app", HttpMethod.POST));
CHROME_COMMAND_NAME_TO_URL.put("getNetworkConditions", new CommandInfo("/session/:sessionId/chromium/network_conditions", HttpMethod.GET));
CHROME_COMMAND_NAME_TO_URL.put("setNetworkConditions", new CommandInfo("/session/:sessionId/chromium/network_conditions", HttpMethod.POST));
CHROME_COMMAND_NAME_TO_URL.put("deleteNetworkConditions", new CommandInfo("/session/:sessionId/chromium/network_conditions", HttpMethod.DELETE));
CHROME_COMMAND_NAME_TO_URL.put("executeCdpCommand", new CommandInfo("/session/:sessionId/goog/cdp/execute", HttpMethod.POST));
CHROME_COMMAND_NAME_TO_URL.put("getCastSinks", new CommandInfo("/session/:sessionId/goog/cast/get_sinks", HttpMethod.GET));
CHROME_COMMAND_NAME_TO_URL.put("selectCastSink", new CommandInfo("/session/:sessionId/goog/cast/set_sink_to_use", HttpMethod.POST));
CHROME_COMMAND_NAME_TO_URL.put("startCastTabMirroring", new CommandInfo("/session/:sessionId/goog/cast/start_tab_mirroring", HttpMethod.POST));
CHROME_COMMAND_NAME_TO_URL.put("getCastIssueMessage", new CommandInfo("/session/:sessionId/goog/cast/get_issue_message", HttpMethod.GET));
CHROME_COMMAND_NAME_TO_URL.put("stopCasting", new CommandInfo("/session/:sessionId/goog/cast/stop_casting", HttpMethod.POST));
CHROME_COMMAND_NAME_TO_URL.put("setPermission", new CommandInfo("/session/:sessionId/permissions", HttpMethod.POST));
}

可以看到这里面也定义了一些命令,executeCdpCommand也在其中。而RemoteWebDriver没有这个命令的信息,自然也就无法执行了。经过进一步查看源码,我发现ChromiumDriverCommandExecutorHttpCommandExecutor的子类,HttpCommandExecutor是RemoteWebDriver中真正的命令执行者。

ChromeWebDriver能够提供自定义的CommandExecutor来增加额外命令,自然我们自己继承的类也可以。在HttpCommandExecutor中有这样一个构造函数HttpCommandExecutor(Map<String, CommandInfo> additionalCommands, URL addressOfRemoteServer),只要把添加的命令的键值对传入,就可以支持额外的命令了。

最终版本的代码如下

public class CdpRemoteWebDriver extends RemoteWebDriver {

    private static final HashMap<String, CommandInfo> CHROME_COMMAND_NAME_TO_URL = new HashMap();

    public CdpRemoteWebDriver(URL remoteAddress, Capabilities capabilities) {
super((CommandExecutor)(new HttpCommandExecutor(ImmutableMap.copyOf(CHROME_COMMAND_NAME_TO_URL), remoteAddress)), capabilities);
} public Map<String, Object> executeCdpCommand(String commandName, Map<String, Object> parameters) {
Objects.requireNonNull(commandName, "Command name must be set.");
Objects.requireNonNull(parameters, "Parameters for command must be set.");
Map<String, Object> toReturn = (Map)this.getExecuteMethod().execute("executeCdpCommand", ImmutableMap.of("cmd", commandName, "params", parameters));
return ImmutableMap.copyOf(toReturn);
} static {
CHROME_COMMAND_NAME_TO_URL.put("launchApp", new CommandInfo("/session/:sessionId/chromium/launch_app", HttpMethod.POST));
CHROME_COMMAND_NAME_TO_URL.put("getNetworkConditions", new CommandInfo("/session/:sessionId/chromium/network_conditions", HttpMethod.GET));
CHROME_COMMAND_NAME_TO_URL.put("setNetworkConditions", new CommandInfo("/session/:sessionId/chromium/network_conditions", HttpMethod.POST));
CHROME_COMMAND_NAME_TO_URL.put("deleteNetworkConditions", new CommandInfo("/session/:sessionId/chromium/network_conditions", HttpMethod.DELETE));
CHROME_COMMAND_NAME_TO_URL.put("executeCdpCommand", new CommandInfo("/session/:sessionId/goog/cdp/execute", HttpMethod.POST));
CHROME_COMMAND_NAME_TO_URL.put("getCastSinks", new CommandInfo("/session/:sessionId/goog/cast/get_sinks", HttpMethod.GET));
CHROME_COMMAND_NAME_TO_URL.put("selectCastSink", new CommandInfo("/session/:sessionId/goog/cast/set_sink_to_use", HttpMethod.POST));
CHROME_COMMAND_NAME_TO_URL.put("startCastTabMirroring", new CommandInfo("/session/:sessionId/goog/cast/start_tab_mirroring", HttpMethod.POST));
CHROME_COMMAND_NAME_TO_URL.put("getCastIssueMessage", new CommandInfo("/session/:sessionId/goog/cast/get_issue_message", HttpMethod.GET));
CHROME_COMMAND_NAME_TO_URL.put("stopCasting", new CommandInfo("/session/:sessionId/goog/cast/stop_casting", HttpMethod.POST));
CHROME_COMMAND_NAME_TO_URL.put("setPermission", new CommandInfo("/session/:sessionId/permissions", HttpMethod.POST));
} }

再测试一下效果



可以看到user-agent已经被成功替换了。

总结一下解决问题的流程

  1. 继承RemoteWebDriver类
  2. 参考ChromeDriver实现executeCdpCommand方法
  3. 参考ChromeDriver创建自定义的commandExecutor增加命令

事实上,因为cdp命令是chrome浏览器提供的支持,与selenium无关,在selenium4中只是内置了这个命令的参数和地址,调用的原理与原来支持的方法是一样的。在自己实现的CdpRemoteWebDriver中已经自己添加了参数,并不需要将依赖升级到4.0.0就可以调用cdp命令了。

Selenium RemoteWebDriver 利用CDP修改User-Agent的更多相关文章

  1. 利用Photoshop修改图片以达到投稿要求

    摘自:http://www.dxy.cn/bbs/thread/8602152#8602152 利用Photoshop修改图片以达到投稿要求 软件版本为Photoshop CS V8.0.1(中文版) ...

  2. 利用phpmyadmin修改mysql的root密码及如何进入修改密码后的phpmyadmin

    1.利用phpmyadmin修改mysql的root密码 很多人利用phpmyadmin或者命令行来修改了mysql的root密码,重启后发现mysql登录错误,这是为什么呢?修改mysql的root ...

  3. UIWebView使用时的问题,包含修改user agent

    1.①像普通controller那样实现跳转到webview的效果,而不是直接加到当前controller②隐藏webview的某些元素③webview跳往原生app④给webview添加进度条 解决 ...

  4. 利用脚本修改SQL SERVER排序规则

    利用脚本修改SQL SERVER排序规则 编写人:CC阿爸 2014-3-1 l  今年的一项重要工作是对公司所用系统进行繁简的转换,程序转成简体基本很容易解决,但数据库转换成简体,就没那么容易了.经 ...

  5. Selenium之利用Excel实现参数化

    Selenium之利用Excel实现参数化 说明:我是通过Workbook方式来读取excel文件的,这次以登陆界面为例 备注:使用Workbook读取excel文件,前提是excel需要2003版本 ...

  6. [Python爬虫] 之二十:Selenium +phantomjs 利用 pyquery通过搜狗搜索引擎数据

    一.介绍 本例子用Selenium +phantomjs 利用 pyquery通过搜狗搜索引擎数据()的资讯信息,输入给定关键字抓取资讯信息. 给定关键字:数字:融合:电视 抓取信息内如下: 1.资讯 ...

  7. 利用反射修改final数据域

    当final修饰一个数据域时,意义是声明该数据域是最终的,不可修改的.常见的使用场景就是eclipse自动生成的serialVersionUID一般都是final的. 另外还可以构造线程安全(thre ...

  8. shell编程系列12--文本处理三剑客之sed利用sed修改文件内容

    shell编程系列12--文本处理三剑客之sed利用sed修改文件内容 修改命令对照表 编辑命令 1s/old/new/ 替换第1行内容old为new ,10s/old/new/ 替换第1行到10行的 ...

  9. [唐胡璐]Selenium技巧 - 利用MonteScreenRecorder录制视频

    我们可以用以下方式在Selenium Webdriver中capture video. 基本步骤: 从 http://www.randelshofer.ch/monte/,下载“MonteScreen ...

  10. 利用路由修改thinkphp框架开发的后台地址

    一般我们写前台是home 后台是 admin那么我们的后台是 域名/admin 那我们要随时修改这个地址是非常麻烦的事情开始可能会想到,就是把模块名称改掉,或者分组(3.1版)这样的话不但要改配置,连 ...

随机推荐

  1. shell条件测试test

    shell条件测试可以通过以下两种方式: test   参数    测试内容 [ 参数  测试内容 ] 一.测试文件类型: test  -e   文件名          (测试文件是否存在) [ - ...

  2. LightOJ1119 Pimp My Ride(状压DP)

    dp[S]表示已经完成的工作集合 枚举从哪儿转移过来的,再通过枚举计算花费..水水的.. #include<cstdio> #include<cstring> #include ...

  3. 【Linux高频命令专题(16)】less

    概述 less 工具也是对文件或其它输出进行分页显示的工具,应该说是linux正统查看文件内容的工具,功能极其强大.less 的用法比起 more 更加的有弹性.在 more 的时候,我们并没有办法向 ...

  4. 暑假集训(2)第五弹 ----- Who&#39;s in the Middle(poj2388)

    G - Who's in the Middle Crawling in process... Crawling failed Time Limit:1000MS     Memory Limit:32 ...

  5. java 常用基本数据类型的默认值

    在使用基本数据类型作为类成员的时候,有时只初始化了而没有给变量赋值,那么此时,java会给你的变量赋一个默认初始值. boolean        false char              '/ ...

  6. MySQL存储过程带in和out参数

    MySQL存储过程带in和out参数 最简单的例子: [html] mysql> DELIMITER $$ mysql> USE test $$ Database changed mysq ...

  7. PL/SQL批处理语句(BULK COLLECT子句和FORALL语句)

    Oracle为PL/SQL中的SQL相关功能提供了FORALL语句和BULK COLLECT子句,显著的增强了SQL相关功能.这两个语句一起被称作PL/SQL的批处理语句.Oracle为什么要提供这两 ...

  8. 在CentOs7上部署Gunicorn

    Gunicorn 的作用与优点这里就不再赘述,如不知道你也不会找这些对吧? 正文 安装简单,直接使用pip即可 pip3 install gunicorn 昨日在 Centos 中想部署Gunicor ...

  9. linux系统网络相关问题

    暂时将你的 eth0 这张网络卡的 IP 设定为 192.168.1.100 ,如何进行? ifconfig eth0 192.168.1.100 我要增加一个路由规则,以 eth0 连接 192.1 ...

  10. NSIS脚本 打包安装程序

    相关工具 nsis http://nsis.sourceforge.net/Special_Builds HM NIS Edit http://hmne.sourceforge.net/ 例子: ; ...