Selenium 默认在顶层文档(Top Window)中查找元素。如果目标元素位于 <iframe> 或 <frame> 内部,直接定位必然抛出 NoSuchElementException——你在 F12 里明明能看到它,但 Selenium 的上下文根本没有进入那个内嵌页面。
处理逻辑分三步:定位 iframe → 切换进去 → 操作完成后切回。
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 里。
上一篇提到了 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),再重新定位新页面的元素:
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 的当前上下文不会自动跳转过去。需要手动切换窗口句柄:
# 记录点击前的句柄
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 的线程池是比手动管理多进程更可控的方案:
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 执行时,才动用它。
把上述模式整合成一个可复用的结构:
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 删除。