爬虫实战:如何优雅地抓取网页中隐藏在伪元素(__before)里的文本?

相信很多写过一段时间爬虫的同学大概率撞过这堵“隐形墙”:在浏览器里明明白白显示着"¥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 元素的 data-price 属性里。
  • url()url() 函数引入:插入的是一张图片而不是文字,遇到这种情况无法当做文本抓取,只能走结构化接口或 OCR。
  • counter()counter() 计数器:通常用于自动编号,值是渲染引擎在运行时算出来的,必须依赖浏览器渲染才能读取。

破局方案与实战演示

应对这种反爬,我们的决策优先级永远是:能拿接口绝不渲染,能抓静态绝不上自动化

  • 首选方案:直采接口。绝大数伪元素只是装饰,背后都有明文 JSON 接口支撑。找到接口直接发纯 HTTP 请求,速度快且反爬压力小。
  • 次选方案:正则解析 CSS。如果 content 是写死的字符串或来自 attr(),可以直接把 CSS 抓下来用正则提取,不需要启动浏览器。
  • 终极方案:渲染引擎 + 代理 IP。当 CSS 是动态注入的,或者强制使用 counter() 渲染时,我们就必须祭出 Selenium / Playwright 并调用 window.getComputedStyle 来读取最终样式。

渲染抓取实战(配合爬虫代理)

走到终极方案时,一个无法回避的问题是:单个 IP 使用 Selenium 频繁访问,其极其明显的自动化特征会迅速触发验证码甚至封禁。浏览器需要加载完整资源、停留时间长,IP 暴露面极大。

在这类渲染型抓取中,因为会话连续性(如保持登录态、Cookie 验证、表单提交)的要求,一旦中途跳变 IP,会导致之前的验证全部失效。此时,我们需要使用代理厂商提供的固定转发模式

爬虫代理为例,其固定转发模式能够在一个特定时间窗口(通常 1 到 3 分钟)内保持出口 IP 一致,完美契合了渲染型会话的需要。

以下是基于 Selenium 接入固定转发代理,并抓取伪元素数据的完整示例代码:

# 推荐使用 selenium-wire 以支持原生不支持的代理账密认证
from seleniumwire 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
import time

# 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 = None

    while time.time() < deadline:
        # 执行 JS 获取 getComputedStyle
        val = driver.execute_script(
            "return getComputedStyle(document.querySelector('.price'), '::before').getPropertyValue('content');"
        )
        
        # 伪元素存在时,有效内容不会是空或 'none'
        if val and val != "none":
            # getPropertyValue('content') 返回的结果通常会包裹着额外的双引号或单引号
            extracted_price = val.strip('"').strip("'")
            break
        time.sleep(0.3)

    if extracted_price:
        print(f"成功抓取伪元素价格: {extracted_price}")
    else:
        print("伪元素内容未能在规定时间内渲染。")

finally:
    driver.quit()

进阶避坑指南

在实操过程中,再点出几个很容易踩坑的地方:

  1. 返回值自带引号:就像代码中展示的一样,使用 getPropertyValue(‘content’) 取出来的如果是字符串,它会原样返回 ““¥9.9"”。一定要用 strip('”‘).strip("’") 进行清洗,不然你的数据库里全都是脏数据。
  2. 警惕图片“文字”:如果 content 返回的是 url(https://blog.csdn.net/%E2%80%A6),请立刻停止尝试提取文本,那实质上是一张图片。你要么去找其他数据源接口,要么只能引入复杂的 OCR 链路。
  3. 不要轻信 attr()attr():如果通过静态解析 CSS 发现 content 是 attr(data-price),千万别以为大功告成了,你拿到的只是一个表达式。你必须回到那个真实的 DOM 节点上,去提取它自身的 data-price 属性值。

一句话总结这套打法:先判断来源,能拿接口绝不渲染;真要渲染,记得上稳定代理(如爬虫代理)保底。希望这篇文章能帮你打破“看见抓不着”的窘境,让你的爬虫更上一层楼!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值