Selenium: 如何在加载/执行页面的任何其他脚本之前注入/执行页面中的Javascript?

47

我正在使用Selenium Python WebDriver浏览一些页面。在任何其他JavaScript代码被加载和执行之前,我希望将JavaScript代码注入到页面中。另一方面,我需要我的JS代码作为该页面的第一个JS代码被执行。有没有办法通过Selenium实现这一点?

我已经谷歌了几个小时,但是我找不到任何正确的答案!


但我的问题是如何在页面加载之前使用Selenium Webdriver注入JS代码。我无法访问这些页面的内容,因此除非我使用代理重写页面内容,否则无法在其中注入JS代码。 - Alex
3
我想我找到了答案。根据http://grokbase.com/t/gg/selenium-users/12a99543jq/is-there-a-way-to-inject-javascripts-before-page-onload,除非我们使用代理在页面开始时注入脚本,否则我们无法这样做。 - Alex
1
您能否安装类似于GreaseMonkey或Tampermonkey的应用程序来注入您的脚本?https://addons.mozilla.org/en-us/firefox/addon/greasemonkey/ - Brian Cain
是的,您可以通过自己的扩展程序或GreaseMonkey来完成。 - Alex
如果您没有使用物理显示器,而是使用类似PhantomJS的工具,您可以获取目标页面的DOM。接下来,您可以遍历DOM,注入您的脚本,并添加一个onLoad触发器以在页面加载时执行脚本。在我看来,这是最直接的方法之一。 - Abhinav
6个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
20

Selenium现在已经支持Chrome开发者工具协议(CDP)API,因此,在每次页面加载时执行脚本非常容易。以下是一个示例代码:

driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {'source': 'alert("Hooray! I did it!")'})

它将在每个页面加载时执行该脚本。有关更多信息,请访问:


9
自从1.0.9版本以来,selenium-wire 已经具备了修改请求响应的功能。以下是在网页到达浏览器之前将脚本注入网页的示例。
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 Monet
1
这只是注入资源的记录。为避免混淆,我已将其删除。 - Mattwmaster58
谢谢!您知道custom_response_handler注入函数是否允许修改响应头吗?我看到我们可以返回响应的body,但在我的示例中,我还想在响应中添加或修改一个头。 - Jean Monet
我不确定,你可以尝试在res.headers中(覆)写一些键。 - Mattwmaster58
似乎这个功能在2021年1月已被弃用:https://pypi.org/project/selenium-wire/,版本为V3 - 你知道有什么替代方案吗? - n.r.

6
如果您想在页面被浏览器解析和执行之前将某些内容注入到html中,建议使用代理工具,如Mitmproxy。请参考Mitmproxy

一个网站是否使用https,这个能不能实现? - blablaalb

4

如果您无法修改页面内容,可以使用代理或在浏览器中安装的扩展程序中使用内容脚本。在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>
    ...

感谢您提供这个非常简洁和详细的答案。我知道自从您发布这篇文章以来,6年多的时间里事情已经发生了很大的变化,但是基本的Java示例似乎仍然有效...除了Firefox 99。当我尝试在Firefox中使用这种技术时,executeScript调用成功完成,但是我尝试注入的函数似乎没有持久化(typeof myFunction == 'undefined')。然而,如果我直接在开发者工具控制台中运行相同的代码,则会得到预期的结果(typeof myFunction == 'function')。您有任何诊断此问题的建议吗? - Scott Babcock

4

我知道已经过了几年,但我找到了一种不需要修改网页内容和不使用代理的方法来做到这一点!我正在使用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”选项。

文档:https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/capabilities_exports_PageLoadStrategy.html

编辑:请注意,“eager”选项尚未在Chrome中实现...


谢谢!一直在寻找如何在页面呈现之前执行脚本,这个方法很有效。如果其他人也遇到这个问题,我还让它在Chrome中运行了。Python示例 - 010011100101
2
对我没用。这并不能确保脚本在页面加载之前运行,它只是允许脚本在页面变为可交互时立即运行。 - villasv
@010011100101,您能否在此处发布代码作为解决方案?谢谢。 - n.r.

0

这是@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()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,