停止静默失败:使用 LLMs 验证 Web Scraper 输出

发布: (2026年2月9日 GMT+8 13:43)
10 分钟阅读
原文: Dev.to

I’m happy to translate the article for you, but I need the full text of the post (the content you’d like translated). Could you please paste the article’s body here? Once I have that, I’ll provide a Simplified‑Chinese translation while keeping the source link, formatting, markdown, and any code blocks unchanged.

问题:结构验证 vs 语义验证

在传统的数据管道中,我们使用 结构验证。Python 中的 pydantic 或 JSON Schema 等工具非常擅长确保名为 price 的字段是浮点数,名为 sku 的字段是字符串。

from pydantic import BaseModel

class Product(BaseModel):
    title: str
    price: float
    sku: str

如果你的爬虫把字符串 "Free Shipping" 提取到 price 字段,Pydantic 会抛出错误,因为 "Free Shipping" 不能被转换为浮点数。这很有帮助,但它并没有解决 语义 问题。

如果爬虫从 “推荐产品” 侧边栏而不是主产品页面提取了 "$19.99"?从结构上看,它是一个有效的浮点数;从语义上看,却是错误的。传统代码难以 “读取” 页面来判断一段文本是否是 正确 的文本。这正是 AI Judge 能发挥作用的地方。

解决方案:“AI Judge” 架构

AI Judge 模式在你的爬取循环中引入二次验证步骤。不要对解析器盲目信任,而是取一小段原始 HTML 和提取的 JSON,交给 LLM。

工作流

  1. 提取 – 你的爬虫(Playwright、BeautifulSoup 等)使用选择器提取数据。
  2. 上下文抽样 – 隔离出发现数据的 HTML 块。
  3. 验证 – LLM 将原始 HTML 与 JSON 进行比较。
  4. 决策 – 如果 LLM 标记出不匹配,系统会提醒开发者或触发重试。

通过使用 LLM,你可以利用其理解非结构化文本和视觉层次的能力,而无需编写成千上万行脆弱的正则或手动检查。

第一步:设置(脆弱的爬虫)

让我们从一个标准的提取脚本开始。我们将针对一个典型的电子商务产品页面,类似于那些在 BestBuy.com‑Scrapers 仓库中的页面。

import requests
from bs4 import BeautifulSoup

def extract_product_data(html_content):
    soup = BeautifulSoup(html_content, "html.parser")

    # These selectors break easily if the site updates
    return {
        "title": soup.select_one(".product-title").get_text(strip=True),
        "price": soup.select_one(".price-value").get_text(strip=True),
        "sku":   soup.select_one(".model-number").get_text(strip=True),
    }

# Imagine this HTML is fetched via requests
sample_html = (
    'Sony Alpha 7 IV'
    '$2,499.99'
)
data = extract_product_data(sample_html)
print(data)

这在今天可以正常工作,但如果站点将 .price-value 改为 .price-display-v2,你的爬虫将返回 None 或从不相关的元素中抓取数据。

Source:

第2步:构建 AI 验证器

要构建验证器,编写一个提示,让 LLM 扮演 QA 工程师的角色。LLM 应返回 结构化响应——布尔值和错误原因。

我们将使用 openai 库和 JSON 模式,确保输出可机器读取。

import openai
import json

client = openai.OpenAI(api_key="YOUR_API_KEY")

def validate_extraction(html_snippet: str, extracted_data: dict) -> dict:
    prompt = f"""
    You are a Data Quality Auditor. Compare extracted JSON data 
    against a raw HTML snippet to ensure accuracy.

    RAW HTML:
    {html_snippet}

    EXTRACTED JSON:
    {json.dumps(extracted_data, ensure_ascii=False, indent=2)}

    Rules:
    1. Check if the 'title' in JSON matches the main product title in HTML.
    2. Check if the 'price' in JSON matches the actual product price.
    3. Ignore minor whitespace or formatting differences.
    4. If the data is missing or incorrect, set 'is_valid' to false.

    Return ONLY a JSON object with this structure:
    {{"is_valid": boolean, "reason": "string explaining the error if invalid"}}
    """

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        response_format={"type": "json_object"},
    )

    return json.loads(response.choices[0].message.content)

为什么这样有效

  • 上下文隔离 – 发送整个 100 KB 的 HTML 文件成本高且噪声大。我们只发送相关容器。
  • 语义比较 – LLM 能理解 HTML 中的 "$2,499.99" 与 JSON 中的 "2499.99" 是相同的,即使格式不同。
  • 推理能力 – 若验证失败,"reason" 字段会立即提供调试提示。

第 3 步:实现反馈循环

现在,让我们将验证器集成到爬取逻辑中。在生产环境中,你不应该因为单个错误而停止整个爬取,但应记录错误并在错误率超过特定阈值时终止爬虫。

def run_scraper(url: str, error_threshold: float = 0.05):
    html = requests.get(url).text
    extracted_data = extract_product_data(html)

    # Grab only the relevant HTML snippet (e.g., the product container)
    # For demonstration we just reuse the whole page; replace with a proper selector.
    html_snippet = html  # TODO: narrow this down

    validation = validate_extraction(html_snippet, extracted_data)

    if not validation["is_valid"]:
        # Log the failure and optionally retry or flag for manual review
        print(f"Validation failed for {url}: {validation['reason']}")
        # Increment error counter, etc.
    else:
        # Persist the clean data
        print("✅ Data validated:", extracted_data)

    # Example of error‑rate handling (pseudo‑code)
    # if error_rate > error_threshold:
    #     raise RuntimeError("Error rate exceeded – stopping crawl")

生产技巧

提示描述
批量验证将一批项目一起验证以减少 API 调用(例如,一次请求发送 10 条 HTML 片段)。
缓存对相同的 HTML 片段缓存 LLM 响应,以节省成本。
限流尊重 OpenAI 的速率限制;在收到 429 响应时使用指数退避。
可观测性is_validreason 字段存入监控仪表盘,以便及早发现漂移。
回退如果 LLM 不可用,回退到结构化验证并标记为稍后审查。

回顾

  1. 结构验证 捕获类型不匹配,但不捕获上下文错误。
  2. AI 驱动的语义验证 让大型语言模型验证提取的值确实属于预期的元素。
  3. 将验证器集成到你的流水线中作为轻量、可选的步骤,记录失败,并仅在超过阈值时采取行动。

通过在爬虫中添加 AI 判官,你可以将静默失败转化为可操作的警报,显著减少在生产环境中调试失效选择器所花费的时间。祝爬取愉快!

令牌

soup = BeautifulSoup(html, 'html.parser')
container = str(soup.select_one(".product-main-area"))

validation_result = validate_extraction(container, extracted_data)

if not validation_result['is_valid']:
    print(f"CRITICAL: Validation failed for {url}")
    print(f"Reason: {validation_result['reason']}")
    # Log to your monitoring system (e.g., Sentry or ScrapeOps)
    return None

return extracted_data

优化:成本与性能

将每个请求都发送给 LLM 会让你的爬虫变慢且成本高昂。如果你抓取 100,000 个页面,每页 $0.01 的 API 调用费用就会累计到 $1,000。使用 Statistical Sampling(统计抽样)来进行优化。

1. 抽样

你并不需要验证每一行数据。检查 1 % 的数据通常就足以捕捉到全站范围的布局变化。

import random

def should_validate(rate=0.01):
    return random.random() < rate

# In your loop
if should_validate(rate=0.05):   # Validate 5 % of requests
    validation_result = validate_extraction(html, data)

2. 模型选择

避免在简单比较时使用 GPT‑4o。像 gpt-4o-miniclaude-3-haiku 这样的模型成本显著更低,且完全能够胜任 JSON 与 HTML 的比较工作。它们的延迟也要低得多。

3. 基于置信度的触发

仅在本地代码**“不确定”**时才触发 AI Judge。例如,当选择器返回空字符串或正则表达式匹配失败时,将 HTML 交给 LLM,要求它找出缺失的数据。

总结

使用 AI 自动化模式验证将网页抓取从“碰碰运气”的方式提升为严谨的工程学科。通过将大语言模型(LLM)作为语义 QA 层,你可以在数据集被破坏之前捕获静默失败。

关键要点

  • 结构验证(Pydantic)捕获数据类型错误,而 语义验证(AI)捕获上下文错误。
  • 上下文隔离 至关重要——仅向 LLM 发送相关的 HTML 片段,以节省成本并提升准确性。
  • 使用抽样 可保持管道的性能和成本效益。
  • 结构化输出 让你能够直接将 AI 反馈集成到代码逻辑中。

下一步

考虑使用 ScrapeOps Proxy Provider 来确保在开始验证过程之前,从目标获取高质量的 HTML。成功的数据提取始于合适的工具,终于可靠的验证。

0 浏览
Back to Blog

相关文章

阅读更多 »

解锁笔记本电脑 GPU 的隐藏力量

概述:大多数现代笔记本电脑都配备了强大的 GPU,但往往未被充分利用。无论你是运行本地 LLM 的软件工程师,还是数据科学家……