我正在使用Selenium Python WebDriver浏览一些页面。在任何其他JavaScript代码被加载和执行之前,我希望将JavaScript代码注入到页面中。另一方面,我需要我的JS代码作为该页面的第一个JS代码被执行。有没有办法通过Selenium实现这一点?
我已经谷歌了几个小时,但是我找不到任何正确的答案!
我正在使用Selenium Python WebDriver浏览一些页面。在任何其他JavaScript代码被加载和执行之前,我希望将JavaScript代码注入到页面中。另一方面,我需要我的JS代码作为该页面的第一个JS代码被执行。有没有办法通过Selenium实现这一点?
我已经谷歌了几个小时,但是我找不到任何正确的答案!
Selenium现在已经支持Chrome开发者工具协议(CDP)API,因此,在每次页面加载时执行脚本非常容易。以下是一个示例代码:
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {'source': 'alert("Hooray! I did it!")'})
它将在每个页面加载时执行该脚本。有关更多信息,请访问:
import os
from seleniumwire import webdriver
from gzip import compress, decompress
from urllib.parse import urlparse
from lxml import html
from lxml.etree import ParserError
from lxml.html import builder
script_elem_to_inject = builder.SCRIPT('alert("injected")')
def inject(req, req_body, res, res_body):
# various checks to make sure we're only injecting the script on appropriate responses
# we check that the content type is HTML, that the status code is 200, and that the encoding is gzip
if res.headers.get_content_subtype() != 'html' or res.status != 200 or res.getheader('Content-Encoding') != 'gzip':
return None
try:
parsed_html = html.fromstring(decompress(res_body))
except ParserError:
return None
try:
parsed_html.head.insert(0, script_elem_to_inject)
except IndexError: # no head element
return None
return compress(html.tostring(parsed_html))
drv = webdriver.Firefox(seleniumwire_options={'custom_response_handler': inject})
drv.header_overrides = {'Accept-Encoding': 'gzip'} # ensure we only get gzip encoded responses
另一种远程控制浏览器并在页面内容加载前注入脚本的一般方法是使用基于完全不同协议的库,例如 Chrome DevTools Protocol。我所知道的最完整功能的是 playwright
injected.append((req, req_body, res, res_body, parsed_html))
?我没找到injected
指的是什么。 - Jean Monetcustom_response_handler
注入函数是否允许修改响应头吗?我看到我们可以返回响应的body
,但在我的示例中,我还想在响应中添加或修改一个头。 - Jean Monetres.headers
中(覆)写一些键。 - Mattwmaster58如果您无法修改页面内容,可以使用代理或在浏览器中安装的扩展程序中使用内容脚本。在Selenium中,您需要编写一些代码将脚本注入到现有元素的子级之一,但是在页面加载之前(当驱动程序的get()
调用返回时),您将无法运行它。
String name = (String) ((JavascriptExecutor) driver).executeScript(
"(function () { ... })();" ...
文档没有指定代码开始执行的时间。您希望它在DOM开始加载之前开始执行,因此只有使用代理或扩展内容脚本路线才能保证此需求得以满足。
如果您可以为页面添加一个最小的测试工具,则可以检测特殊的URL查询参数的存在并加载其他内容,但您需要使用内联脚本来实现。伪代码:
<html>
<head>
<script type="text/javascript">
(function () {
if (location && location.href && location.href.indexOf("SELENIUM_TEST") >= 0) {
var injectScript = document.createElement("script");
injectScript.setAttribute("type", "text/javascript");
//another option is to perform a synchronous XHR and inject via innerText.
injectScript.setAttribute("src", URL_OF_EXTRA_SCRIPT);
document.documentElement.appendChild(injectScript);
//optional. cleaner to remove. it has already been loaded at this point.
document.documentElement.removeChild(injectScript);
}
})();
</script>
...
executeScript
调用成功完成,但是我尝试注入的函数似乎没有持久化(typeof myFunction == 'undefined'
)。然而,如果我直接在开发者工具控制台中运行相同的代码,则会得到预期的结果(typeof myFunction == 'function'
)。您有任何诊断此问题的建议吗? - Scott Babcock我知道已经过了几年,但我找到了一种不需要修改网页内容和不使用代理的方法来做到这一点!我正在使用nodejs版本,但是其他语言的API应该也是一致的。你想要做的是:
const {Builder, By, Key, until, Capabilities} = require('selenium-webdriver');
const capabilities = new Capabilities();
capabilities.setPageLoadStrategy('eager'); // Options are 'eager', 'none', 'normal'
let driver = await new Builder().forBrowser('firefox').setFirefoxOptions(capabilities).build();
await driver.get('http://example.com');
driver.executeScript(\`
console.log('hello'
\`)
对我来说,“eager”选项可以使用。您可能需要使用“none”选项。
编辑:请注意,“eager”选项尚未在Chrome中实现...
这是@Mattwmaster58的答案的更新版本,适用于最新版本的selenium-wire
(本文撰写时为5.1.0)。还增加了对内联脚本标签上的nonce
属性的支持。
from lxml import html
from lxml.etree import ParserError
from lxml.html import builder
from seleniumwire import webdriver
from seleniumwire.request import Request, Response
from seleniumwire.thirdparty.mitmproxy.net.http import encoding as decoder
SCRIPT_BODY_TO_INJECT = 'alert("injected")'
def has_mime_type(header: str, expected_type: str) -> bool:
return header == expected_type or header.startswith(expected_type + ";")
def response_interceptor(request: Request, response: Response) -> None:
content_type = response.headers.get("Content-Type")
if (
response.status_code != 200
or not content_type
or not has_mime_type(content_type, "text/html")
):
return
encoding = response.headers.get("Content-Encoding", "identity")
try:
parsed_html = html.fromstring(decoder.decode(response.body, encoding))
except ParserError:
return
# Preserve nonce attribute to allow inline script.
# https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce
attrs = {}
if (nonce_script := parsed_html.find(".//script[@nonce]")) is not None:
attrs["nonce"] = nonce_script.get("nonce")
try:
injected_script = builder.SCRIPT(SCRIPT_BODY_TO_INJECT, **attrs)
parsed_html.head.insert(0, injected_script)
except IndexError: # No head element.
return
response.body = decoder.encode(
html.tostring(parsed_html.getroottree()), encoding
)
del response.headers["Content-Length"] # Avoid duplicate header.
response.headers["Content-Length"] = str(len(response.body))
def main():
with webdriver.Firefox() as session:
session.response_interceptor = response_interceptor
session.get("https://example.com")
if __name__ == "__main__":
main()
作为使用lxml
生成输出的替代方法(该方法可能会改变HTML的结构),您还可以使用正则表达式来插入标签并保留现有的格式。
from lxml import html
from lxml.etree import ParserError
from lxml.html import builder
from mimeparse import parse_mime_type
from seleniumwire import webdriver
from seleniumwire.request import Request, Response
from seleniumwire.thirdparty.mitmproxy.net.http import encoding as decoder
import re
SCRIPT_BODY_TO_INJECT = 'alert("injected")'
HEAD_TAG_RE = re.compile(r"<head\s*>()", re.IGNORECASE)
INLINE_SCRIPT_TAG_RE = re.compile(
r"()<script\b(?:(?!\bsrc\b\s*=\s*['\"]).)*?>", re.IGNORECASE
)
def response_interceptor(request: Request, response: Response) -> None:
content_type = response.headers.get("content-type")
if not content_type:
return
mime_type, mime_subtype, mime_params = parse_mime_type(content_type)
if (
response.status_code != 200
or mime_type != "text"
or mime_subtype != "html"
):
return
encoding = response.headers.get("content-encoding", "identity")
charset = mime_params.get("charset", "iso-8859-1")
try:
decoded_body = decoder.decode(response.body, encoding).decode(charset)
parsed_html = html.fromstring(decoded_body)
except ParserError:
return
# Preserve nonce attribute to allow inline script.
# https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce
attrs = {}
if (nonce_script := parsed_html.find(".//script[@nonce]")) is not None:
attrs["nonce"] = nonce_script.get("nonce")
# Some sites inject scripts before the DOCTYPE, which isn't valid markup
# but still runs.
if m := min((x for regex in (INLINE_SCRIPT_TAG_RE, HEAD_TAG_RE)
if (x := regex.search(decoded_body))),
key=lambda x: x.start()):
injected_script_text = html.tostring(
builder.SCRIPT(SCRIPT_BODY_TO_INJECT, **attrs), encoding="unicode"
)
replacement = (
m.string[m.start(): m.start(1)]
+ injected_script_text
+ m.string[m.start(1): m.end()]
)
modified_body = m.string[:m.start()] + replacement + m.string[m.end():]
response.body = decoder.encode(modified_body.encode(charset), encoding)
del response.headers["Content-Length"] # Avoid duplicate header.
response.headers["Content-Length"] = str(len(response.body))
def main():
with webdriver.Firefox() as session:
session.response_interceptor = response_interceptor
session.get("https://example.com")
if __name__ == "__main__":
main()
onLoad
触发器以在页面加载时执行脚本。在我看来,这是最直接的方法之一。 - Abhinav