相信很多写过一段时间爬虫的同学大概率撞过这堵“隐形墙”:在浏览器里明明白白显示着"¥9.9"的价格,或者一段验证码文本,但当你切换到 DevTools 时,发现那个数字安安静静地挂在某个标签里。然而,当你用 requests 抓下来,再用 BeautifulSoup 或 XPath 去提取得时候,拿到的却是一个空列表。即便你认为是 JS 动态加载,一通 Selenium 渲染操作后,DOM 里依旧是一无所有。
这其实并非 JS 的锅,而是 CSS **伪元素(Pseudo-elements)**在作祟。
今天这篇文章,我们就从技术根源出发,手把手教你如何破除这层“伪装”,并结合强大的代理工具实现稳定抓取。
根因剖析:文本去哪了?
问题的根源在于,你想要抓取的文本根本就不在 DOM 树里。
这些文本通常是 CSS 引擎根据 ::before 或 ::after 伪元素的 content 属性在运行时凭空生成的。因为伪元素并不属于 DOM 节点集合,传统的 getElementById、querySelector 或者 XPath 自然全都失效了。
在动手写代码前,我们必须先分清 content 的四种常见取值来源:
- 写死的字符串:例如 content: “¥9.9”;,值直接硬编码在 CSS 规则中。
- attr()attr() 函数引用:例如 content: attr(data-price);,真实数据其实藏在 HTML 元素的>破局方案与实战演示
应对这种反爬,我们的决策优先级永远是:能拿接口绝不渲染,能抓静态绝不上自动化。
- 首选方案:直采接口。绝大数伪元素只是装饰,背后都有明文 JSON 接口支撑。找到接口直接发纯 HTTP 请求,速度快且反爬压力小。
- 次选方案:正则解析 CSS。如果 content 是写死的字符串或来自 attr(),可以直接把 CSS 抓下来用正则提取,不需要启动浏览器。
- 终极方案:渲染引擎 + 代理 IP。当 CSS 是动态注入的,或者强制使用 counter() 渲染时,我们就必须祭出 Selenium / Playwright 并调用 window.getComputedStyle 来读取最终样式。
渲染抓取实战(配合爬虫代理)
走到终极方案时,一个无法回避的问题是:单个 IP 使用 Selenium 频繁访问,其极其明显的自动化特征会迅速触发验证码甚至封禁。浏览器需要加载完整资源、停留时间长,IP 暴露面极大。
在这类渲染型抓取中,因为会话连续性(如保持登录态、Cookie 验证、表单提交)的要求,一旦中途跳变 IP,会导致之前的验证全部失效。此时,我们需要使用代理厂商提供的固定转发模式。
以爬虫代理为例,其固定转发模式能够在一个特定时间窗口(通常 1 到 3 分钟)内保持出口 IP 一致,完美契合了渲染型会话的需要。
以下是基于 Selenium 接入固定转发代理,并抓取伪元素数据的完整示例代码:
# 推荐使用 selenium-wire 以支持原生不支持的代理账密认证fromseleniumwireimportwebdriverfromselenium.webdriver.common.byimportByfromselenium.webdriver.support.uiimportWebDriverWaitfromselenium.webdriver.supportimportexpected_conditionsasECimporttime# 1. 配置亿牛云代理(固定转发模式,保持IP在时间窗口内连续)proxyHost="t.16yun.cn"proxyPort="31111"proxyUser="username"# 替换成你的账号proxyPass="password"# 替换成你的密码options={"proxy":{"http":f"http://{proxyUser}:{proxyPass}@{proxyHost}:{proxyPort}","https":f"http://{proxyUser}:{proxyPass}@{proxyHost}:{proxyPort}","no_proxy":"localhost,127.0.0.1"# 绕过本地请求}}# 2. 启动浏览器driver=webdriver.Chrome(seleniumwire_options=options)try:url="https://example.com/product/123"driver.get(url)# 3. 避免 headless 时序陷阱,不能仅等待元素存在,要轮询判断 content 是否渲染# 定义一个自定义的等待函数,轮询获取 ::before 的内容deadline=time.time()+10# 10秒超时extracted_price=Nonewhiletime.time()<deadline:# 执行 JS 获取 getComputedStyleval=driver.execute_script("return getComputedStyle(document.querySelector('.price'), '::before').getPropertyValue('content');")# 伪元素存在时,有效内容不会是空或 'none'ifvalandval!="none":# getPropertyValue('content') 返回的结果通常会包裹着额外的双引号或单引号extracted_price=val.strip('"').strip("'")breaktime.sleep(0.3)ifextracted_price:print(f"成功抓取伪元素价格:{extracted_price}")else:print("伪元素内容未能在规定时间内渲染。")finally:driver.quit()进阶避坑指南
在实操过程中,再点出几个很容易踩坑的地方:
- 返回值自带引号:就像代码中展示的一样,使用 getPropertyValue(‘content’) 取出来的如果是字符串,它会原样返回 ““¥9.9"”。一定要用 strip('”‘).strip("’") 进行清洗,不然你的数据库里全都是脏数据。
- 警惕图片“文字”:如果 content 返回的是 url(…),请立刻停止尝试提取文本,那实质上是一张图片。你要么去找其他数据源接口,要么只能引入复杂的 OCR 链路。
- 不要轻信 attr()attr():如果通过静态解析 CSS 发现 content 是 attr(data-price),千万别以为大功告成了,你拿到的只是一个表达式。你必须回到那个真实的 DOM 节点上,去提取它自身的>