PAYATHON 2026

Selenium 自动结账时出现 no such frame 错误

支付阿杰

结论

switch_to.frame(2) 和 switch_to.frame(0) 中的数字,是 iframe 在当前页面上下文中的临时序号。结账页的 iframe 往往是动态加载的,加载顺序还会受到网络、Cookie、弹窗和支付组件的影响。因此,Selenium IDE 录制时使用的序号,不一定适用于 PyCharm 启动的新浏览器会话。

建议按以下顺序处理:

  1. 等待外层 iframe 出现,然后切换进去;
  2. 在外层 iframe 中等待支付 iframe,再切换一次;
  3. 等待 cardNum 变为可交互状态;
  4. 操作完成后,根据 btnPay 所在的实际层级返回父 frame 或主文档。

定位 iframe 时,应优先使用稳定的 id、name、title 或 src 特征,不要依赖数字索引。

为什么会出现 no such frame

执行下面的代码后:

driver.switch_to.frame(2)

WebDriver 的当前上下文已经切换到第 3 个 iframe。接着执行:

driver.switch_to.frame(0)

此时查找的不是主页面中的第 1 个 iframe,而是第 3 个 iframe 内部的第 1 个 iframe。

出现 no such frame,通常有以下几种原因:

  • 外层 iframe 已经出现,但内部支付 iframe 还没有加载;
  • 当前选中的第 3 个 iframe 不是录制时的那个 iframe;
  • 页面新增了 Cookie、风控或营销 iframe,导致序号发生变化;
  • Selenium IDE 在录制和回放时隐式等待了页面状态,而导出的代码没有等待相同的状态;
  • 直接打开 checkout URL 后,没有完整恢复购物车、配送地址或结账会话,因此支付 iframe 没有生成。

删除第一层切换后无法找到 cardNum 也很正常,因为这个元素不在主文档中,而是在一层或多层 iframe 内。

推荐实现

先检查结账页的实际 DOM,找出能够稳定识别两层 iframe 的属性。假设外层结账 iframe 的 src 包含 Global-e,内部支付 iframe 的 src 或 title 包含 payment,可以这样写:

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait, Select
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 30)

# 必须先通过正常流程进入购物车和结账页,
# 确保当前会话中已经生成有效的 checkout。
driver.get("https://www.adidas.co.il/en/checkout#Global-e_International_Checkout")

# 每次开始定位 frame 前先回到主文档,避免上下文不确定
driver.switch_to.default_content()

# 以下选择器只是结构示例,需要替换为页面当前实际属性
wait.until(
    EC.frame_to_be_available_and_switch_to_it(
        (By.CSS_SELECTOR, "iframe[src*='Global-e']")
    )
)

wait.until(
    EC.frame_to_be_available_and_switch_to_it(
        (By.CSS_SELECTOR, "iframe[src*='payment']")
    )
)

card_number = wait.until(
    EC.element_to_be_clickable((By.ID, "cardNum"))
)
card_number.send_keys("4444 4444 4444 4444")

Select(
    wait.until(EC.element_to_be_clickable((By.ID, "cardExpiryMonth")))
).select_by_visible_text("08")

Select(
    wait.until(EC.element_to_be_clickable((By.ID, "cardExpiryYear")))
).select_by_visible_text("2031")

wait.until(
    EC.element_to_be_clickable((By.ID, "cvdNumber"))
).send_keys("111")

driver.switch_to.default_content()

wait.until(
    EC.element_to_be_clickable((By.ID, "btnPay"))
).click()

frame_to_be_available_and_switch_to_it 会等待目标 iframe 可用,并将 WebDriver 切换进去。对于动态页面,如果只等待普通元素出现,随后立即调用 switch_to.frame(),仍可能遇到加载时序问题。

如果 iframe 没有稳定的属性

可以逐层列出 iframe 的属性,确认支付组件到底位于哪一层。下面的代码适合诊断,不建议把输出中的序号直接用作最终选择器:

from selenium.webdriver.common.by import By

driver.switch_to.default_content()

frames = driver.find_elements(By.TAG_NAME, "iframe")

for index, frame in enumerate(frames):
    print(
        index,
        "id=", frame.get_attribute("id"),
        "name=", frame.get_attribute("name"),
        "title=", frame.get_attribute("title"),
        "src=", frame.get_attribute("src")
    )

找到疑似外层 iframe 后,进入其中检查下一层:

driver.switch_to.default_content()

outer_frame = wait.until(
    EC.presence_of_element_located(
        (By.CSS_SELECTOR, "iframe[src*='可识别的外层地址片段']")
    )
)
driver.switch_to.frame(outer_frame)

inner_frames = driver.find_elements(By.TAG_NAME, "iframe")

for index, frame in enumerate(inner_frames):
    print(
        index,
        "id=", frame.get_attribute("id"),
        "name=", frame.get_attribute("name"),
        "title=", frame.get_attribute("title"),
        "src=", frame.get_attribute("src")
    )

如果支付 iframe 的属性完全是动态的,可以遍历候选 iframe,查找其中是否存在 cardNum:

from selenium.common.exceptions import (
    NoSuchElementException,
    StaleElementReferenceException,
)

def switch_to_frame_containing(driver, locator):
    driver.switch_to.default_content()

    for outer in driver.find_elements(By.TAG_NAME, "iframe"):
        driver.switch_to.default_content()

        try:
            driver.switch_to.frame(outer)

            if driver.find_elements(*locator):
                return True

            for inner in driver.find_elements(By.TAG_NAME, "iframe"):
                driver.switch_to.parent_frame()
                driver.switch_to.frame(outer)
                driver.switch_to.frame(inner)

                if driver.find_elements(*locator):
                    return True

        except (NoSuchElementException, StaleElementReferenceException):
            continue

    driver.switch_to.default_content()
    return False

下面是一个更完整的递归方案,可以处理动态 DOM:

from selenium.common.exceptions import (
    NoSuchFrameException,
    StaleElementReferenceException,
)
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait


def switch_to_frame_with_element(driver, locator, max_depth=5):
    driver.switch_to.default_content()

    def search(depth):
        if driver.find_elements(*locator):
            return True

        if depth >= max_depth:
            return False

        frames = driver.find_elements(By.TAG_NAME, "iframe")

        for index in range(len(frames)):
            try:
                # 重新获取列表,避免动态页面导致旧 WebElement 失效
                frames = driver.find_elements(By.TAG_NAME, "iframe")
                driver.switch_to.frame(frames[index])

                if search(depth + 1):
                    return True

                driver.switch_to.parent_frame()

            except (NoSuchFrameException, StaleElementReferenceException):
                driver.switch_to.default_content()
                return False

        return False

    return search(0)


found = WebDriverWait(driver, 30).until(
    lambda d: switch_to_frame_with_element(d, (By.ID, "cardNum"))
)

if not found:
    raise RuntimeError("未在可访问的 iframe 中找到 cardNum")

WebDriverWait(driver, 20).until(
    lambda d: d.find_element(By.ID, "cardNum").is_enabled()
).send_keys("4444 4444 4444 4444")

从长期维护的角度看,最好还是使用明确的 iframe 定位器。递归搜索更适合排查问题,或者用于 iframe 属性确实无法稳定识别的页面。

正确返回 frame 层级

下面两个切换方法的作用不同:

driver.switch_to.parent_frame()

它会返回上一层。例如,从支付 iframe 返回外层 checkout iframe。

driver.switch_to.default_content()

它会直接返回页面最外层的主文档。

因此,不能默认认为 btnPay 一定位于主文档。需要通过开发者工具确认它所在的层级:

  • 如果 btnPay 位于支付 iframe 的外层 iframe,使用 parent_frame();
  • 如果它位于最外层页面,使用 default_content();
  • 如果它也在支付 iframe 内,点击前就不应退出该 iframe。

例如,按钮位于外层 checkout iframe 时,可以这样处理:

driver.switch_to.parent_frame()

wait.until(
    EC.element_to_be_clickable((By.ID, "btnPay"))
).click()

注意事项

  • 不要直接依赖 frame(0) 或 frame(2)。数字表示当前上下文中的 iframe 顺序,不是固定编号。
  • 加购、进入结账页和填写信息必须在同一个 WebDriver 会话中完成。直接访问带 hash 的 checkout URL,可能缺少服务器端的购物车或支付会话。
  • iframe 跨域不会阻止 Selenium 使用 switch_to.frame()。但切换进去之前,无法直接从父文档定位其中的元素。
  • cardNum 可能由支付服务延迟创建,因此应等待元素变为可点击状态,而不只是等待页面加载完成。
  • iframe 重新加载后,之前保存的 WebElement 会失效。遇到 StaleElementReferenceException 时,需要重新定位 iframe。
  • 选择 <select> 选项时,使用 Select.select_by_visible_text() 通常比拼接全局 XPath 更稳妥。
  • 自动化支付可能触发 3-D Secure、验证码或风控校验。仅靠切换 iframe 无法保证完成这些交互。调试时应优先使用网站或支付服务提供的测试环境和测试卡。

备注:内容仅供参考。