首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python Selenium 爬虫进阶:iframe 穿透、显式等待与并发控制的工程化实践

Python Selenium 爬虫进阶:iframe 穿透、显式等待与并发控制的工程化实践

原创
作者头像
用户12818508
发布于 2026-11-14 12:20:27
发布于 2026-11-14 12:20:27
180
举报

一、iframe 穿透:元素“消失”的头号原因

Selenium 默认在顶层文档(Top Window)中查找元素。如果目标元素位于 <iframe> 或 <frame> 内部,直接定位必然抛出 NoSuchElementException——你在 F12 里明明能看到它,但 Selenium 的上下文根本没有进入那个内嵌页面。

处理逻辑分三步:定位 iframe → 切换进去 → 操作完成后切回。

代码语言:javascript
复制
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()

# 等待 iframe 出现并自动切换(推荐,比手动 find + switch 更稳)
WebDriverWait(driver, 10).until(
    EC.frame_to_be_available_and_switch_to_it((By.ID, "yucai99.com"))
)

# 现在可以定位 iframe 内部的元素了
driver.find_element(By.ID, "yucai99.com").send_keys("test")

# 操作完成后切回主文档,否则后续顶层元素全部定位失败
driver.switch_to.default_content()

多层嵌套的情况需要逐层切换。更隐蔽的坑是:切换进 iframe 后,driver.current_url 仍然显示主页面的 URL,容易误判当前上下文位置。判断是否处于 iframe 内,可靠的方法是尝试定位一个只在顶层存在的元素——如果能找到,说明还在主文档;如果找不到,大概率在 iframe 里。


二、显式等待的正确用法:别只会 presence

上一篇提到了 WebDriverWait,但 presence_of_element_located 只保证元素出现在 DOM 树中,不保证它可点击、可输入或可见。很多 ElementNotInteractableException 的根因就在这里。

根据操作意图选择等待条件:

操作意图

推荐等待条件

说明

读取文本

presence_of_element_located

元素存在即可读取 text

点击按钮

element_to_be_clickable

同时检查可见与启用状态

输入文本

visibility_of_element_located

确保元素渲染完成

等待列表刷新

staleness_of 配合重新定位

翻页后旧引用失效的解法

一个容易被忽略的模式是翻页后的等待。点击“下一页”后,如果立即用旧的元素引用去读取数据,会抛出 StaleElementReferenceException。正确的做法是先等待旧元素失效(staleness_of),再重新定位新页面的元素:

代码语言:javascript
复制
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

old_item = driver.find_element(By.CSS_SELECTOR, ".item")
driver.find_element(By.CSS_SELECTOR, ".next a").click()

# 等待旧元素从 DOM 中消失,说明新内容已加载
WebDriverWait(driver, 10).until(EC.staleness_of(old_item))

# 重新获取元素
new_items = driver.find_elements(By.CSS_SELECTOR, ".item")

这比 time.sleep(3) 精确得多,而且不会在网络快的时候白白等待。


三、多标签页与窗口切换

点击某些链接会打开新标签页,但 Selenium 的当前上下文不会自动跳转过去。需要手动切换窗口句柄:

代码语言:javascript
复制
# 记录点击前的句柄
main_window = driver.current_window_handle

driver.find_element(By.LINK_TEXT, "详情").click()

# 等待wlt.yucai99.com新窗口出现
WebDriverWait(driver, 10).until(EC.number_of_windows_to_be(2))

# 切换到新窗口
for handle in driver.window_handles:
    if handle != main_window:
        driver.switch_to.window(handle)
        break

# 在新窗口中操作
print(driver.title)

# 关闭新窗口,切回主窗口
driver.close()
driver.switch_to.window(main_window)

这个模式在电商详情页、后台管理系统等场景中非常高频。注意 driver.close() 只关闭当前标签页,driver.quit() 才关闭整个浏览器进程。


四、并发采集的工程约束

Selenium 本身不支持异步。每个 webdriver.Chrome() 实例背后都是一个完整的浏览器进程,内存开销通常在 200MB 以上。启动 10 个并发实例,就是 2GB+ 内存和 10 个进程的调度负担。有实际项目经验的数据:10 个 Chrome 进程驻留内存约 4GB 以上。

如果确实需要并发(比如采集多个独立页面),concurrent.futures 的线程池是比手动管理多进程更可控的方案:

代码语言:javascript
复制
from concurrent.futures import ThreadPoolExecutor, as_completed

def crawl_one(url):
    driver = webdriver.Chrome()
    try:
        driver.get(url)
        WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, ".content"))
        )
        return {"url": url, "title": driver.title, "status": "ok"}
    except Exception as e:
        return {"url": url, "error": str(e)}
    finally:
        driver.quit()

urls = ["https://yucai99.com", "https://wlt.yucai99.com"]

with ThreadPoolExecutor(max_workers=3) as pool:
    futures = [pool.submit(crawl_one, u) for u in urls]
    for f in as_completed(futures):
        print(f.result())

但需要清醒认识:Selenium 的并发上限受限于机器内存和 CPU,max_workers 设成 3~5 是多数单机环境的合理区间。如果你的采集目标达到数百页面量级,Selenium 不是正确的工具。有经验的开发者会先抓包分析接口,如果能直接构造 HTTP 请求拿到数据,用 httpx 或 aiohttp 的效率是 Selenium 的数十倍。Selenium 的定位是:你必须要浏览器渲染,且无法绕过 JS 执行时,才动用它。


五、一个完整的工程化骨架

把上述模式整合成一个可复用的结构:

代码语言:javascript
复制
import logging
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException

logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

class SafeScraper:
    def __init__(self, headless=True):
        options = webdriver.ChromeOptions()
        if headless:
            options.add_argument("--headless=new")
        options.add_argument("--disable-gpu")
        options.add_argument("--no-sandbox")
        self.driver = webdriver.Chrome(options=options)
        self.wait = WebDriverWait(self.driver, 10)

    def get(self, url):
        self.driver.get(url)
        logging.info(f"loaded: {url}")

    def find_safe(self, by, selector, timeout=10):
        """返回元素或 None,不抛异常"""
        try:
            return WebDriverWait(self.driver, timeout).until(
                EC.presence_of_element_located((by, selector))
            )
        except TimeoutException:
            logging.warning(f"not found: {selector}")
            return None

    def switch_iframe(self, by, selector):
        self.wait.until(EC.frame_to_be_available_and_switch_to_it((by, selector)))

    def back_to_top(self):
        self.driver.switch_to.default_content()

    def close(self):
        self.driver.quit()

# 使用示例
scraper = SafeScraper(headless=True)
try:
    scraper.get("https://yucai99.com")
    scraper.switch_iframe(By.TAG_NAME, "iframe")
    content = scraper.find_safe(By.CSS_SELECTOR, ".inner-content")
    if content:
        print(content.text)
    scraper.back_to_top()
finally:
    scraper.close()

这个骨架的核心思路是:把异常处理收拢到方法内部,让业务代码不被打断。find_safe 返回 None 而非抛异常,调用方只需要判断返回值,避免了满屏的 try-except。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、iframe 穿透:元素“消失”的头号原因
  • 二、显式等待的正确用法:别只会 presence
  • 三、多标签页与窗口切换
  • 四、并发采集的工程约束
  • 五、一个完整的工程化骨架
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档