从 Prompt 到 Parser:一次知乎采集的曲折经历

简介: 本文探讨了使用大模型和Playwright技术在知乎进行数据采集时遇到的挑战及其优化策略。初始方案因页面异步加载、DOM结构变化和限制策略而失败。为了提高数据采集的稳定性和可靠性,提出了增强渲染层、适配器层和回退监控机制的改进方案。通过这些改进,可以有效应对页面异步加载和DOM变化带来的问题,同时规避限制策略的影响,从而实现更高效、稳定的数据采集。

在写爬虫的工作中,总会遇到那些“看起来简单,做起来崩溃”的任务。知乎采集就是这样一个典型的案例。本来想借助大模型,把网页结构交给它自动理解,然后直接吐出 JSON 格式的结果。但从最初的顺风顺水,到后面的一连串问题,整个过程几乎像一场“事故调查”。

事件回放:时间轴上的波动

上午 9:00
一开始的设想很简单:用 Playwright 打开知乎问题页面,拿到渲染后的 HTML,再把它丢给大模型。让它帮忙识别出问题标题、第一条回答,以及前五条评论。为了避免请求被拦截,还加上了爬虫代理。理论上,这样就能轻松完成数据抓取。

上午 10:10
最初的效果非常好。在几个测试页面里,模型准确地把标题和回答内容提取出来,评论也能顺利解析。我甚至开始庆幸,终于能摆脱写那些又臭又长的 XPath 规则了。

上午 11:45
麻烦很快出现。部分页面的回答和评论是延迟加载的,刚打开时 DOM 里根本没有完整内容。模型拿到的 HTML 其实不全,结果返回的数据缺胳膊少腿。日志里不断冒出 missing field: comments

下午 1:30
还没等我缓过来,知乎页面的小改版又来了一刀。回答卡片的 class 名字改了,嵌套层次也调整过,模型生成的规则一下子就失灵了。结果不是空数据,就是掺杂了一堆广告推荐内容。

下午 3:00
接下来更糟。随着采集频率提高,知乎的风控机制直接把我拉进了“人机校验”流程。验证码、滑块轮番上阵,代理再好也救不了。这时整个采集队列等于是停摆。

下午 4:30
最后的会议上,大家一致认定:仅靠 Prompt-to-Parser 的方案太脆弱。它确实能帮忙,但还得配合一整套兜底机制,才能在真实场景里跑得下去。

问题复盘

回头看,那些故障其实都能归结为几个根源:

  1. 渲染时机不对
    评论和回答是异步注入的,如果没等 DOM 稳定就丢给模型,必然缺失数据。
  2. 解析方式过于依赖 DOM 表象
    模型“看”到的页面结构变化太快,今天能用,明天就失效。
  3. 反爬策略干扰
    知乎的风控相对严格,频繁访问触发验证码几乎是必然。
  4. 缺乏兜底与报警
    出了问题只能人工排查,没有任何回退机制,效率极低。

改进思路:三层防护

为了让方案能撑住长期运行,我们重新梳理了架构,补上了几道保险。

  1. 渲染层
    • 用 Playwright 把页面完整加载出来。
    • 设定等待规则,不只是 networkidle,还要等到回答和评论数量稳定才开始解析。
    • 尽量复用 Cookie 会话,降低频繁触发验证码的概率。
  2. 适配器层
    • 给模型设计固定的 Prompt 模板,明确要哪些字段。
    • 加上备用的 CSS/XPath 选择器,当模型失灵时用来兜底。
    • 定期检查解析结果,把表现稳定的 Prompt 固化下来,逐步形成“适配器库”。
  3. 回退与监控
    • 如果模型和备用规则都失效,就保存 HTML 快照,丢进人工工单系统。
    • 出现大量验证码时,自动降低采集速率,而不是死磕。
    • 对异常情况设置告警,让问题暴露得更及时。

改进后的小片段代码

下面这段示例,是我后来用来测试的核心流程(删掉了一些公司内部的逻辑,保留了关键部分)。主要思路就是:先渲染,再交给模型,如果解析失败就用备用选择器。

import asyncio
import json
import time
from typing import Optional
from playwright.async_api import async_playwright, Page
import openai  # 假设使用 OpenAI 风格 API;替换为你自己的 LLM 接口

# === 爬虫代理(亿牛云示例仅作配置展示) =====
# 请替换成真实的代理信息或使用环境变量管理
PROXY_HOST = "proxy.16yun.cn"
PROXY_PORT = 3100
PROXY_USER = "16YUN"
PROXY_PASS = "16IP"

# Playwright 接受的 proxy 字段需要字典形式
PLAYWRIGHT_PROXY = {
   
    "server": f"http://{PROXY_HOST}:{PROXY_PORT}",
    "username": PROXY_USER,
    "password": PROXY_PASS
}

# ======= 基本配置 =======
# LLM API key 通过环境变量/安全配置提供(此处仅示意)
# openai.api_key = "YOUR_API_KEY"

# ======= 工具函数:等待 DOM 稳定 =======
async def wait_for_content_stable(page: Page, selector: str, timeout: int = 10000, stable_rounds: int = 3):
    """
    等待指定 selector 出现并且节点数量在连续 stable_rounds 次检查中保持不变,
    以应对异步分页/延迟注入的问题。
    """
    end_time = time.time() + timeout / 1000.0
    last_count = -1
    stable = 0

    await page.wait_for_selector(selector, timeout=timeout)  # 首次出现
    while time.time() < end_time:
        nodes = await page.query_selector_all(selector)
        count = len(nodes)
        if count == last_count:
            stable += 1
            if stable >= stable_rounds:
                return True
        else:
            stable = 0
            last_count = count
        await asyncio.sleep(0.5)
    return False

# ======= 备份静态选择器(回退规则) =======
# 这些选择器为示例,需在真实环境中调试与维护
FALLBACK_SELECTORS = {
   
    "title": "h1.QuestionHeader-title",
    "first_answer_author": ".AnswerCard .AuthorInfo .UserLink",
    "first_answer_time": ".AnswerCard time",
    "first_answer_content": ".AnswerCard .RichContent-inner",
    "comments": ".CommentItem"
}

# ======= 主流程:渲染并调用 LLM 解析 =======
async def fetch_zhihu_question(url: str) -> dict:
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True, proxy=PLAYWRIGHT_PROXY)
        context = await browser.new_context(
            # 模拟常见 UA,Cookie 可通过 add_cookies 恢复登录态(如有合法权限)
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121 Safari/537.36",
            locale="zh-CN"
        )

        # 在每个新页面注入脚本,尝试减少 navigator.webdriver 指示(注意合规边界)
        await context.add_init_script("""() => { Object.defineProperty(navigator, 'webdriver', {get: () => undefined}); }""")

        page = await context.new_page()
        await page.goto(url, timeout=60000)

        # 等待关键内容稳定(回答区)——知乎回答列表的容器可能使用类似 .AnswerList
        stable = await wait_for_content_stable(page, ".AnswerCard", timeout=15000, stable_rounds=4)
        # 若未稳定,可进一步尝试长等待或抓取 API(如合法)
        # 截取渲染后的 HTML 快照
        html = await page.content()
        await browser.close()

    # ======= Prompt-to-Parser:构建 Prompt 并调用 LLM =======
    prompt = f"""
    你是一个网页解析助手。请从下面的知乎问题页渲染后 HTML 中提取:
    - question_title: 问题标题(字符串)
    - first_answer: 包含 author, time, content 字段(文本)
    - comments: 前 5 条评论(每条包含 author, time, text)

    HTML:
    {html[:4000]}
    请返回纯 JSON,不要包含额外解释。如果某个字段缺失,请用 null。
    """

    try:
        # 使用 LLM(此处为伪代码示例,请替换为实际调用)
        resp = openai.ChatCompletion.create(
            model="gpt-4o-mini",
            messages=[
                {
   "role": "system", "content": "你是结构化数据提取专家。"},
                {
   "role": "user", "content": prompt}
            ],
            max_tokens=1200,
            temperature=0
        )
        llm_text = resp["choices"][0]["message"]["content"]
        parsed = json.loads(llm_text)
    except Exception as e:
        parsed = {
   "error": f"LLM 提取失败: {e}"}

    # ======= 验证字段完整性,若关键字段缺失则触发回退解析(静态选择器) =======
    def is_valid(p: dict):
        return isinstance(p.get("question_title"), str) and p.get("first_answer") and p["first_answer"].get("content")

    if not is_valid(parsed):
        # 回退:用预定义 CSS 选择器直接从 DOM 中抽取(需要重新渲染或保存的 html->dom 工具)
        # 为简化示例:使用 playwright 在 headless 浏览器里再次提取(同步复用上次渲染结果更好)
        # 这里演示回退思路:在真实实现中请把 html 保存在可重载环境,然后用 lxml/bs4/CSS 解析
        fallback = {
   "question_title": None, "first_answer": None, "comments": []}
        # (伪代码)——实际应重建 Playwright 上下文或使用保存的 DOM 进行解析
        # 以下仅作为结构示例
        fallback["question_title"] = "回退:无法从 LLM 获得标题,需人工/选择器检查"
        parsed = {
   "fallback_used": True, **fallback}

    # 添加一些元数据
    parsed_meta = {
   
        "timestamp": int(time.time()),
        "url": url,
        "llm_used": True,
        "fallback": parsed.get("fallback_used", False),
        "result": parsed
    }
    return parsed_meta

# ======= 运行示例 =======
if __name__ == "__main__":
    sample_url = "https://www.zhihu.com/question/xxxxxx"  # 替换为目标问题
    result = asyncio.run(fetch_zhihu_question(sample_url))
    print(json.dumps(result, ensure_ascii=False, indent=2))

最后的感受

知乎这样的站点,页面结构复杂、更新频繁,还有不小的风控压力。指望一次性的 Prompt 就能解决所有问题是不现实的。它确实能大大减少我们写解析规则的工作量,但要想在生产环境里跑稳,必须加上渲染等待、备用选择器、回退机制和监控告警。

换句话说,Prompt-to-Parser 就像一个聪明的助手,但它不是万能的。真正的稳定性,还是要靠架构设计和防线层层叠加来保证。

相关文章
|
存储 算法 关系型数据库
第10章 索引优化与查询优化【2.索引及调优篇】【MySQL高级】3
第10章 索引优化与查询优化【2.索引及调优篇】【MySQL高级】3
444 0
小技巧 - 微信零钱转出免手续费方法(利用零钱通转出)
小技巧 - 微信零钱转出免手续费方法(利用零钱通转出)
4560 0
小技巧 - 微信零钱转出免手续费方法(利用零钱通转出)
|
6月前
|
安全 搜索推荐 机器人
风险规则引擎-RPA 作为自动化依赖业务决策流程的强大工具
机器人流程自动化(RPA)是一种通过软件“机器人”自动执行重复性任务的技术,能大幅提升工作效率。它适用于财务、电商等领域的标准化流程,如账单处理和退货管理。然而,RPA在复杂决策场景中存在局限,需结合决策模型(DMN)和业务规则管理系统(BRMS)实现流程与决策的协同自动化,从而增强灵活性与业务价值。
|
11月前
|
数据采集 人工智能 JSON
Crawl4AI:为大语言模型打造的开源网页数据采集工具
随着大语言模型(LLMs)的快速发展,高质量数据成为智能系统的关键基础。**Crawl4AI**是一款专为LLMs设计的开源网页爬取工具,可高效提取并结构化处理网页数据,突破传统API限制,支持JSON、HTML或Markdown等格式输出。
922 3
Crawl4AI:为大语言模型打造的开源网页数据采集工具
|
4月前
|
数据采集 人工智能 自然语言处理
让跨境电商“懂文化”:AI内容生成在全球民族特色品类中的实践
本文提出并落地了一套基于大模型与民族文化知识库的民族品类智能识别与匹配方案,旨在解决跨境电商平台在服务穆斯林、印度裔等特定民族群体时面临的“供需错配”难题。
744 27
|
8月前
|
自然语言处理 前端开发 JavaScript
Playwright系列课(2) | 元素定位四大法宝:CSS/文本/XPath/语义化定位实战指南
本文是Playwright系列第二课,详解元素定位四大核心技术:CSS选择器、文本定位、XPath和语义化定位,结合实战演示各方法应用场景。重点解析Playwright智能定位器(Locator)的独特优势——自动等待与重试机制,通过预检元素可操作性(可见/可点击)有效规避网络延迟导致的脚本失效,显著提升自动化测试稳定性。
|
存储 监控 安全
电脑格式化了还能恢复数据吗?
在日常使用电脑的过程中,我们可能会因为各种原因需要格式化硬盘。然而,格式化操作会清除硬盘上的所有数据,很多人担心格式化后数据无法找回。本文将详细介绍电脑格式化后的数据恢复方法,帮助大家在不小心格式化硬盘后,仍有机会找回重要文件。
电脑格式化了还能恢复数据吗?
|
存储 人工智能 人机交互
PC Agent:开源 AI 电脑智能体,自动收集人机交互数据,模拟认知过程实现办公自动化
PC Agent 是上海交通大学与 GAIR 实验室联合推出的智能 AI 系统,能够模拟人类认知过程,自动化执行复杂的数字任务,如组织研究材料、起草报告等,展现了卓越的数据效率和实际应用潜力。
2131 1
PC Agent:开源 AI 电脑智能体,自动收集人机交互数据,模拟认知过程实现办公自动化
|
Web App开发 JavaScript Java
自动化测试的利剑:Selenium WebDriver入门与实践
【9月更文挑战第21天】在软件开发的海洋中,自动化测试犹如一艘船,帮助开发者们快速航行至质量保证的彼岸。本文将作为你的罗盘,指引你了解和掌握Selenium WebDriver这一强大的自动化测试工具。通过深入浅出的方式,我们将探索Selenium WebDriver的基本概念、安装过程以及编写简单测试脚本的方法。无论你是刚接触自动化测试的新手,还是希望提升测试技能的开发者,这篇文章都将为你提供有价值的指导。
|
JavaScript
vue3完整教程从入门到精通(新人必学2,搭建项目)
本文介绍了如何在Vue 3项目中安装并验证Element Plus UI框架,包括使用npm安装Element Plus、在main.js中引入并使用该框架,以及在App.vue中添加一个按钮组件来测试Element Plus是否成功安装。
663 0
vue3完整教程从入门到精通(新人必学2,搭建项目)