案例7:错误处理与鲁棒性设计:企业级网页提取系统的稳定性保障

案例7:错误处理与鲁棒性设计:企业级网页提取系统的稳定性保障

【免费下载链接】llm-scraper Turn any webpage into structured data using LLMs 【免费下载链接】llm-scraper 项目地址: https://gitcode.com/GitHub_Trending/ll/llm-scraper

场景挑战

企业级应用对稳定性要求极高,某电商比价平台的llm-scraper实现初期经常因各种异常(网站改版、网络波动、反爬机制)导致服务中断。通过系统化的错误处理和重试机制设计,系统可用性从85%提升至99.9%。

企业级错误处理架构

mermaid

企业级鲁棒性实现代码

import { chromium } from 'playwright'
import { z } from 'zod'
import { openai } from '@ai-sdk/openai'
import LLMScraper from './../src'
import { exponentialBackoff, retry } from 'ts-retry-promise'
import winston from 'winston'

// 1. 完善的日志系统
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
})

// 2. 错误类型定义
enum ScrapeErrorType {
  URL_INVALID = 'URL_INVALID',
  PAGE_LOAD_FAILED = 'PAGE_LOAD_FAILED',
  LLM_CALL_FAILED = 'LLM_CALL_FAILED',
  DATA_VALIDATION_FAILED = 'DATA_VALIDATION_FAILED',
  RATE_LIMITED = 'RATE_LIMITED',
  UNKNOWN_ERROR = 'UNKNOWN_ERROR'
}

class ScrapeError extends Error {
  type: ScrapeErrorType
  url: string
  retryable: boolean
  
  constructor(message: string, type: ScrapeErrorType, url: string, retryable = false) {
    super(message)
    this.type = type
    this.url = url
    this.retryable = retryable
    this.name = 'ScrapeError'
  }
}

// 3. 重试策略配置
const retryStrategies = {
  // 网络错误使用指数退避重试
  network: exponentialBackoff({
    delay: 1000,    // 初始延迟1秒
    maxDelay: 10000, // 最大延迟10秒
    maxRetries: 3   // 最多重试3次
  }),
  
  // LLM错误使用固定间隔重试
  llm: {
    delay: 2000,    // 固定延迟2秒
    maxRetries: 2   // 最多重试2次
  },
  
  // 数据修复重试
  dataFix: {
    delay: 1000,
    maxRetries: 1
  }
}

// 4. 浏览器操作封装(带重试和错误处理)
async function safeBrowserOperation(url: string, operation: (page) => Promise<any>) {
  let browser
  let context
  let page
  
  try {
    // 启动浏览器(带错误处理)
    browser = await chromium.launch({
      headless: 'new',
      args: [
        '--no-sandbox',
        '--disable-setuid-sandbox',
        '--disable-blink-features=AutomationControlled' // 反反爬
      ],
      timeout: 30000
    })
    
    // 创建隐身上下文(隔离环境)
    context = await browser.newContext({
      userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
      permissions: ['geolocation'],
      geolocation: { latitude: 39.9042, longitude: 116.4074 }, // 随机地理位置
      viewport: { width: 1280 + Math.floor(Math.random() * 200), height: 720 + Math.floor(Math.random() * 200) }
    })
    
    // 添加反检测脚本
    await context.addInitScript(() => {
      delete (window as any).navigator.webdriver
      // 其他反反爬措施...
    })
    
    page = await context.newPage()
    
    // 设置页面加载超时和错误处理
    page.setDefaultTimeout(30000)
    page.on('pageerror', error => {
      logger.error(`页面错误: ${error.message}, URL: ${url}`)
    })
    
    // 执行操作
    return await operation(page)
  } finally {
    // 确保资源释放,即使出错也执行
    if (page) await page.close().catch(e => logger.warn(`关闭页面失败: ${e}`))
    if (context) await context.close().catch(e => logger.warn(`关闭上下文失败: ${e}`))
    if (browser) await browser.close().catch(e => logger.warn(`关闭浏览器失败: ${e}`))
  }
}

// 5. 主提取函数(带完整错误处理)
async function enterpriseScrape(url, schema, options = {}) {
  const startTime = Date.now()
  const traceId = generateTraceId() // 生成唯一追踪ID
  const result = {
    success: false,
    data: null,
    error: null,
    metadata: {
      traceId,
      duration: 0,
      retries: {
        network: 0,
        llm: 0,
        dataFix: 0
      }
    }
  }
  
  try {
    // 1. 输入验证
    if (!isValidUrl(url)) {
      throw new ScrapeError('无效的URL格式', ScrapeErrorType.URL_INVALID, url)
    }
    
    logger.info(`开始提取: ${url}`, { traceId, url })
    
    // 2. 页面加载(带重试)
    const pageContent = await retry(
      async () => {
        return await safeBrowserOperation(url, async (page) => {
          // 导航到页面
          const response = await page.goto(url, {
            waitUntil: 'domcontentloaded',
            timeout: 30000
          })
          
          // 检查HTTP错误状态码
          if (response && !response.ok()) {
            if (response.status() === 429) {
              throw new ScrapeError(`请求过于频繁,被限流`, ScrapeErrorType.RATE_LIMITED, url, true)
            }
            throw new ScrapeError(`HTTP错误: ${response.status()}`, ScrapeErrorType.PAGE_LOAD_FAILED, url, response.status() >= 500)
          }
          
          // 检查是否被反爬拦截
          const title = await page.title()
          if (title.includes('验证码') || title.includes('安全验证')) {
            throw new ScrapeError('遇到反爬验证,需要人工处理', ScrapeErrorType.PAGE_LOAD_FAILED, url, false)
          }
          
          // 返回页面内容
          return await page.content()
        })
      },
      retryStrategies.network,
      {
        onRetry: (error, attempt) => {
          result.metadata.retries.network = attempt
          logger.warn(`页面加载重试 ${attempt},错误: ${error.message}`, { traceId, attempt, error: error.message })
        }
      }
    )
    
    // 3. LLM提取(带重试)
    const llm = openai.chat('gpt-4o', { timeout: 20000 })
    const scraper = new LLMScraper(llm)
    
    const llmResult = await retry(
      async () => {
        return await scraper.run(pageContent, schema, {
          ...options,
          prompt: '严格按照提供的schema提取和验证数据,不要添加额外信息'
        })
      },
      retryStrategies.llm,
      {
        onRetry: (error, attempt) => {
          result.metadata.retries.llm = attempt
          logger.warn(`LLM提取重试 ${attempt},错误: ${error.message}`, { traceId, attempt, error: error.message })
        },
        shouldRetry: (error) => {
          // 只重试特定类型的错误
          const retryableErrors = ['timeout', 'rate_limit', 'internal_error']
          return retryableErrors.some(code => error.message.toLowerCase().includes(code))
        }
      }
    )
    
    // 4. 数据验证和修复
    let validatedData
    try {
      validatedData = schema.parse(llmResult.data)
    } catch (validationError) {
      // 尝试数据修复
      logger.warn(`数据验证失败,尝试修复`, { traceId, error: validationError.message })
      
      validatedData = await retry(
        async () => {
          // 使用LLM尝试修复数据
          const fixResult = await fixInvalidData(llmResult.data, schema, validationError)
          return schema.parse(fixResult) // 重新验证修复后的数据
        },
        retryStrategies.dataFix,
        {
          onRetry: (error, attempt) => {
            result.metadata.retries.dataFix = attempt
            logger.warn(`数据修复重试 ${attempt}`, { traceId, attempt })
          }
        }
      )
    }
    
    // 5. 成功结果处理
    result.success = true
    result.data = validatedData
    logger.info(`提取成功`, { traceId, url, duration: Date.now() - startTime })
    
    return result
    
  } catch (error) {
    // 错误处理和分类
    if (error instanceof ScrapeError) {
      result.error = {
        type: error.type,
        message: error.message
      }
      logger.error(`提取失败: ${error.message}`, { traceId, url, errorType: error.type, retryable: error.retryable })
    } else {
      result.error = {
        type: ScrapeErrorType.UNKNOWN_ERROR,
        message: error.message || '未知错误'
      }
      logger.error(`提取失败: ${error.message}`, { traceId, url, stack: error.stack })
    }
    
    // 对于可重试的错误,发送告警但不中断流程
    if (error.retryable) {
      // 发送告警通知
      await sendAlert(`可重试错误: ${error.message}`, { traceId, url, error })
    }
    
    return result
    
  } finally {
    // 计算总耗时
    result.metadata.duration = Date.now() - startTime
    
    // 记录性能指标
    recordMetrics({
      url,
      duration: result.metadata.duration,
      success: result.success,
      errorType: result.error?.type,
      retries: result.metadata.retries
    })
  }
}

// 辅助函数:数据修复
async function fixInvalidData(invalidData, schema, validationError) {
  const llm = openai.chat('gpt-4o-mini')
  const fixPrompt = `以下数据未能通过schema验证:
  
  数据: ${JSON.stringify(invalidData, null, 2)}
  
  错误: ${validationError.message}
  
  schema: ${JSON.stringify(schema._def, null, 2)}
  
  请根据错误信息和schema修复数据,只返回修复后的JSON,不要添加额外解释。`
  
  const { text } = await llm.generate(fixPrompt)
  
  try {
    return JSON.parse(text)
  } catch (e) {
    throw new Error(`数据修复失败: ${e.message}`)
  }
}

// 工具函数
function isValidUrl(url) {
  try {
    new URL(url)
    return true
  } catch {
    return false
  }
}

function generateTraceId() {
  return Date.now().toString(36) + Math.random().toString(36).substr(2, 5)
}

企业级特性详解

  1. 全链路追踪

    • 生成唯一traceId贯穿整个提取流程
    • 详细记录每个步骤的耗时和重试次数
    • 支持分布式追踪系统集成
  2. 智能重试机制

    • 区分可重试和不可重试错误
    • 指数退避策略应对网络波动
    • 按错误类型定制重试次数和间隔
  3. 反爬对抗策略

    • 模拟真实用户浏览器指纹
    • 动态调整请求间隔和并发数
    • 验证码检测和告警机制
  4. 数据质量保障

    • 严格的schema验证
    • 自动数据修复机制
    • 异常值检测和过滤
  5. 可观测性

    • 结构化日志记录
    • 性能指标收集和监控
    • 错误告警和自动恢复

企业级部署建议

  1. 多区域部署

    • 跨区域部署以避免地域网络问题
    • 基于地理位置的请求路由
  2. 流量控制

    • 基于域名的请求频率限制
    • 自动IP轮换机制
    • 请求优先级队列
  3. 灾备方案

    • 降级策略:当LLM服务不可用时切换到规则提取
    • 备用数据源:关键数据多源备份
    • 手动操作入口:复杂情况允许人工介入

总结与最佳实践

llm-scraper作为新一代网页数据提取工具,通过结合浏览器自动化、大语言模型和结构化数据验证,解决了传统爬虫面临的诸多挑战。本文通过7个社区案例,展示了从基础数据提取到企业级系统构建的完整实践路径。

核心优势回顾

  • 开发效率:大幅降低网页提取功能的开发时间(平均减少80%代码量)
  • 适应性强:无需维护CSS选择器,网站改版后仍能正常工作
  • 灵活性高:支持任意网页结构,从简单静态页面到复杂SPA应用
  • 易于集成:简单API设计,轻松集成到现有工作流

不同场景的最佳实践指南

应用场景推荐模型性能优化重点关键配置参数
简单数据提取gpt-4o-mini/llama3-8b缓存策略temperature=0, maxTokens=500
复杂结构化数据gpt-4o/gemini-pro提示工程详细schema描述,示例引导
实时监控系统gpt-4o-mini流式处理stream=true, 短轮询间隔
本地部署需求gemma3-7b/llama3-8b硬件加速Ollama GPU配置,模型量化
企业级应用gpt-4o + 本地模型备份错误处理与重试完善的监控和告警机制

未来发展方向

  1. 多模态提取:结合图像识别能力,处理验证码和复杂视觉布局
  2. 自主学习能力:通过少量标注样本自动优化提取策略
  3. 更深度的工具整合:与数据分析、可视化工具的无缝集成
  4. 更低的使用门槛:无需编码的可视化配置界面

通过本文案例的实践经验,你可以快速掌握llm-scraper的核心用法,并根据自身需求灵活调整和扩展。无论是构建简单的数据聚合工具,还是企业级的监控系统,llm-scraper都能提供强大而灵活的技术支持。

收藏本文,关注项目更新,获取更多高级用法和最佳实践指南!下期我们将探讨"llm-scraper与RPA的融合应用",敬请期待。

【免费下载链接】llm-scraper Turn any webpage into structured data using LLMs 【免费下载链接】llm-scraper 项目地址: https://gitcode.com/GitHub_Trending/ll/llm-scraper

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值