地球人都知道,如果使用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>
然後嘗試呼叫ChromeDriver
的executeCdpCommand
方法
可以看到,第一個引數是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沒有這個命令的資訊,自然也就無法執行了。經過進一步檢視原始碼,我發現ChromiumDriverCommandExecutor
是HttpCommandExecutor
的子類,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已經被成功替換了。
總結一下解決問題的流程
- 繼承RemoteWebDriver類
- 參考ChromeDriver實現
executeCdpCommand
方法 - 參考ChromeDriver建立自定義的
commandExecutor
增加命令
事實上,因為cdp命令是chrome瀏覽器提供的支援,與selenium無關,在selenium4中只是內建了這個命令的引數和地址,呼叫的原理與原來支援的方法是一樣的。在自己實現的CdpRemoteWebDriver
中已經自己新增了引數,並不需要將依賴升級到4.0.0
就可以呼叫cdp命令了。