
zoopla 网站受 cloudflare 严格反爬保护,原生 selenium 分页常因触发人机验证而中断;本文提供绕过 cookie 弹窗、识别 shadow dom、基于 url 变化判断翻页终止的鲁棒方案,并附可直接运行的优化代码。
zoopla 网站受 cloudflare 严格反爬保护,原生 selenium 分页常因触发人机验证而中断;本文提供绕过 cookie 弹窗、识别 shadow dom、基于 url 变化判断翻页终止的鲁棒方案,并附可直接运行的优化代码。
在自动化抓取 Zoopla(如 https://www.zoopla.co.uk/house-prices/england/)这类高防护网站时,分页失效的根本原因并非代码逻辑错误,而是反爬机制提前拦截。该站点使用 Cloudflare Bot Management,当检测到连续、高频、无真实用户行为特征的页面跳转(如快速点击“Next”),会在第 20–30 页左右返回验证页(如 “Checking you are human…”),导致后续 find_element 失败、current_url 不变、staleness_of 等待超时——这正是原始脚本“只爬第一页就停止”的真实原因。
要构建稳定分页流程,需同时解决三大关键问题:
-
Cookie 同意弹窗拦截:Zoopla 使用 Shadow DOM 封装 Cookie 拒绝按钮(
#usercentrics-root),普通 CSS 选择器无法穿透; -
Cloudflare 的静默拦截:翻页后若
driver.current_url未更新,即表明已进入验证流程,应立即终止; -
元素定位鲁棒性:避免依赖易变 class 名(如
_1hzil3o9),优先使用语义化属性(如data-testid,aria-live)。
以下为生产级优化方案(已通过实测):
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from undetected_chromedriver import Chrome
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.common.exceptions import TimeoutException, NoSuchElementException
from selenium.webdriver.common.action_chains import ActionChains
from typing import cast, Iterator
URL = "https://www.zoopla.co.uk/house-prices/england/?new_homes=include&q=england+&orig_q=united+kingdom&view_type=list&pn=1"
TIMEOUT = 5
def etext(e: WebElement) -> str:
if e:
if t := e.text.strip():
return t
if (p := e.get_property("textContent")) and isinstance(p, str):
return p.strip()
return ""
def click(driver: WebDriver, e: WebElement) -> None:
ActionChains(driver).move_to_element(e).click().perform()
def get_all(driver: WebDriver, css: str) -> Iterator[WebElement]:
wait = WebDriverWait(driver, TIMEOUT)
ec = EC.presence_of_all_elements_located
sel = By.CSS_SELECTOR, css
try:
yield from wait.until(ec(sel))
except TimeoutException:
pass
def click_next(driver: WebDriver) -> bool:
"""返回是否成功点击 Next;失败则返回 False"""
for a in get_all(driver, "a[aria-live='polite'] > div > div:nth-child(2)"):
if etext(a) == "Next":
try:
click(driver, a)
return True
except Exception:
return False
return False
def get_shadow_root(driver: WebDriver) -> WebDriver | None:
wait = WebDriverWait(driver, TIMEOUT)
sel = By.ID, "usercentrics-root"
try:
sre = wait.until(EC.presence_of_element_located(sel))
return cast(WebDriver, sre.shadow_root)
except TimeoutException:
return None
def accept_or_reject_cookies(driver: WebDriver) -> None:
shadow = get_shadow_root(driver)
if not shadow:
return
try:
wait = WebDriverWait(shadow, TIMEOUT)
button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "button[data-testid='uc-deny-all-button']")))
click(driver, button) # 注意:此处传入的是外层 driver,非 shadow
except Exception:
pass # Cookie 弹窗可能未出现,忽略
if __name__ == "__main__":
result = []
with Chrome(headless=False, use_subprocess=True) as driver: # headless=True 可提升速度,但调试建议设为 False
driver.get(URL)
accept_or_reject_cookies(driver)
prev_url = ""
npages = 0
print("Starting pagination...")
while prev_url != driver.current_url and npages < 45: # 安全上限,防无限循环
prev_url = driver.current_url
npages += 1
print(f"Processing page {npages} — URL: {prev_url}")
# 提取当前页所有房产地址(更稳定:用 data-testid + h2 组合)
for house in get_all(driver, "div[data-testid='result-item']"):
try:
addr_elem = house.find_element(By.CSS_SELECTOR, "h2")
date_elem = house.find_element(By.CSS_SELECTOR, "span[data-testid='price-history-date']")
result.append({
"address": etext(addr_elem),
"DateLast_sold": etext(date_elem)
})
except NoSuchElementException:
continue # 跳过结构异常的条目
# 尝试翻页
if not click_next(driver):
print("Next button not found or unclickable — stopping.")
break
# 强制等待新页面加载(比 staleness_of 更可靠)
WebDriverWait(driver, 10).until(lambda d: d.current_url != prev_url)
print(f"✅ Successfully scraped {len(result)} properties across {npages} pages.")关键注意事项:
- ✅ 必须使用
undetected_chromedriver v3+:uc.Chrome()可有效规避基础指纹检测,Chrome()原生驱动极易被 Cloudflare 拦截; - ✅ 禁用
headless=True初期调试:可视化模式便于观察 Cookie 弹窗、验证页是否出现; - ⚠️ 勿依赖
staleness_of判断翻页完成:Cloudflare 页面可能 DOM 结构不变但内容为空,应以current_url变化为核心判据; - ⚠️ 添加
npages 安全上限:防止因网络波动导致 URL 未及时更新而死循环; - ? 数据提取优先使用
data-testid属性:Zoopla 明确维护该属性,远比动态 class(如_1hzil3o9)稳定; - ? 真实用户行为模拟:代码中
move_to_element().click()比直接.click()更贴近人工操作,降低风控概率。
综上,Zoopla 分页不是“写法错误”,而是反爬对抗场景下的工程实践问题。稳定性的核心在于:主动识别防护信号(URL 冻结)、穿透前端封装(Shadow DOM)、放弃脆弱假设(DOM 完全重载)、拥抱语义化选择器。按此思路重构,即可在合规前提下高效获取多页结构化数据。

















