传统爬虫的痛点不是写代码——是维护。

你要爬一个技术博客,先 F12 看 DOM,找到标题的 class 是 .article-title,作者是 .author-name span,正文是 #content p。写好了跑两周,网站改版了——class 全变了,你的爬虫挂了。

AI 爬虫的思路刚好反过来:不写选择器,让 AI 自己看 DOM 决定什么是标题、什么是正文。


核心思路

用户给一个 URL
  → chromedp 渲染页面(执行 JS,加载动态内容)
    → 提取 DOM + 可见文本
      → 丢给 LLM:「请从这个页面提取:标题、作者、发布日期、正文」
        → LLM 返回结构化 JSON
          → 存入数据库

关键区别:不是用 LLM 替换 CSS 选择器,是让 LLM 理解页面内容。


第一步:chromedp 渲染并提取内容

Go 的 chromedp 库提供了无头 Chrome 控制能力:

package main

import (
    "context"
    "fmt"
    "strings"
    "time"

    "github.com/chromedp/chromedp"
)

type WebPage struct {
    URL     string
    Title   string
    HTML    string
    Text    string
}

// 渲染页面,提取标题+可见文本
func fetchPage(url string, timeoutSec int) (*WebPage, error) {
    ctx, cancel := chromedp.NewContext(context.Background())
    defer cancel()

    ctx, cancel = context.WithTimeout(ctx, time.Duration(timeoutSec)*time.Second)
    defer cancel()

    var title, textContent string

    // 获取页面标题
    err := chromedp.Run(ctx,
        chromedp.Navigate(url),
        chromedp.WaitReady("body"),
        chromedp.Sleep(2*time.Second), // 等 JS 渲染
        chromedp.Title(&title),
        chromedp.Evaluate(`document.body.innerText`, &textContent),
    )
    if err != nil {
        return nil, fmt.Errorf("获取页面失败: %w", err)
    }

    // 去掉过长的空白
    textContent = compressWhitespace(textContent)

    return &WebPage{
        URL:   url,
        Title: title,
        Text:  textContent,
    }, nil
}

func compressWhitespace(s string) string {
    lines := strings.Split(s, "\n")
    var result []string
    for _, line := range lines {
        trimmed := strings.TrimSpace(line)
        if trimmed != "" {
            result = append(result, trimmed)
        }
    }
    return strings.Join(result, "\n")
}

document.body.innerText 拿到的是用户肉眼能看到的所有文字——不包含 <script><style> 里的噪音。


第二步:LLM 结构化提取

import "encoding/json"

type Article struct {
    Title       string   `json:"title"`
    Author      string   `json:"author"`
    PublishDate string   `json:"publish_date"`
    Summary     string   `json:"summary"`
    Tags        []string `json:"tags"`
    BodyLength  int      `json:"body_length"`
    CodeBlocks  int      `json:"code_blocks"`
}

func extractArticle(page *WebPage) (*Article, error) {
    // 只取前 8000 字符发给 LLM(太长浪费 Token)
    snippet := page.Text
    if len([]rune(snippet)) > 8000 {
        snippet = string([]rune(snippet)[:8000])
    }

    prompt := fmt.Sprintf(`从以下网页文本中提取文章信息。

网页标题: %s
URL: %s

正文前 8000 字符:
%s

请返回 JSON 格式(严格遵守,不要 Markdown 包裹):

{
  "title": "文章标题",
  "author": "作者名(找不到填 null)",
  "publish_date": "发布日期 YYYY-MM-DD(找不到填 null)",
  "summary": "100字以内摘要",
  "tags": ["标签1", "标签2"],
  "body_length": 正文总字符数(估算),
  "code_blocks": 代码块数量
}

规则:
- 作者名只取具体人名/昵称,不要取网站名
- 日期优先从文章元数据提取(通常在开头),其次从 URL
- 标签从文章分类/关键词提取,不超过 5 个
- 如果某项找不到,填 null(字符串)或 0(数字)`, page.Title, page.URL, snippet)

    result, err := callLLM("你是数据提取专家。只返回 JSON,不返回其他内容。", prompt)
    if err != nil {
        return nil, err
    }

    // 清理 LLM 输出(有时会带 ```json 包裹)
    result = cleanJSON(result)

    var article Article
    if err := json.Unmarshal([]byte(result), &article); err != nil {
        return nil, fmt.Errorf("JSON 解析失败: %w\n原始输出: %s", err, result)
    }

    return &article, nil
}

func cleanJSON(s string) string {
    // 去掉可能的 Markdown 代码块包裹
    s = strings.TrimPrefix(s, "```json")
    s = strings.TrimPrefix(s, "```")
    s = strings.TrimSuffix(s, "```")
    return strings.TrimSpace(s)
}

实测:爬一篇技术博客

我找了一篇中等复杂度的技术文章来测:

输入 URL: 一篇关于「Go 泛型实战」的博客文章

LLM 提取结果:

{
  "title": "Go 泛型实战:从 interface{} 到类型安全",
  "author": "张三",
  "publish_date": "2026-03-15",
  "summary": "本文通过一个缓存库的重构案例,展示了如何用 Go 1.21 泛型替代 interface{} 提升类型安全性,包含泛型约束、类型推断和性能对比。",
  "tags": ["Go", "泛型", "重构", "类型安全"],
  "body_length": 5200,
  "code_blocks": 8
}

标题、作者、日期、标签——全对。摘要 6 秒生成,比我手动写的好。


第三步:爬完存数据库

import "database/sql"
import _ "github.com/mattn/go-sqlite3"

type Store struct {
    db *sql.DB
}

func NewStore(dbPath string) (*Store, error) {
    db, err := sql.Open("sqlite3", dbPath)
    if err != nil {
        return nil, err
    }

    db.Exec(`CREATE TABLE IF NOT EXISTS articles (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        url TEXT UNIQUE,
        title TEXT,
        author TEXT,
        publish_date TEXT,
        summary TEXT,
        tags TEXT,
        body_length INTEGER,
        code_blocks INTEGER,
        crawled_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )`)

    return &Store{db: db}, nil
}

func (s *Store) SaveArticle(a *Article) error {
    tags, _ := json.Marshal(a.Tags)
    _, err := s.db.Exec(
        `INSERT OR REPLACE INTO articles
         (url, title, author, publish_date, summary, tags, body_length, code_blocks)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
        a.URL, a.Title, a.Author, a.PublishDate,
        a.Summary, string(tags), a.BodyLength, a.CodeBlocks,
    )
    return err
}

完整流程

func main() {
    store, _ := NewStore("articles.db")

    urls := []string{
        "https://example.com/go-generics-in-action",
        "https://example.com/rust-vs-go-2026",
    }

    for _, url := range urls {
        fmt.Printf("爬取: %s\n", url)

        // 1. 渲染页面
        page, err := fetchPage(url, 30)
        if err != nil {
            fmt.Printf("  跳过 (获取失败): %v\n", err)
            continue
        }

        // 2. AI 提取
        article, err := extractArticle(page)
        if err != nil {
            fmt.Printf("  跳过 (提取失败): %v\n", err)
            continue
        }

        // 3. 存数据库
        if err := store.SaveArticle(article); err != nil {
            fmt.Printf("  跳过 (保存失败): %v\n", err)
            continue
        }

        fmt.Printf("  ✅ %s (%s, %d 字)\n",
            article.Title, article.Author, article.BodyLength)
    }
}

AI 爬虫 vs 传统爬虫

传统爬虫 AI 爬虫
开发 写 XPath / CSS 选择器 写 Prompt
适应性 网站改版就挂 自动适应不同结构
准确率 99%(规则明确时) 90-95%(内容相关时)
成本/页 ~0 ¥0.002-0.01(LLM 调用)
速度 <1 秒 3-8 秒
维护 高(结构变了要改代码) 低(靠 AI 自己理解)

使用建议:

  • 固定结构的页面(列表页、API 响应)→ 传统爬虫,便宜又快
  • 不同网站的详情页、文章内容 → AI 爬虫,一劳永逸
  • 混合策略:传统爬虫爬列表拿到 URL → AI 爬虫抽取详情

进阶玩法

  1. 增量爬取 —— 每天只爬新增的 URL,用已有文章做去重。
  2. 竞品监控 —— 爬竞品的技术博客,每周生成一份「对手更新了什么」摘要。
  3. 自动知识库 —— 把所有爬到的文章存向量数据库,建一个可搜索的技术知识图谱。

这些都是 daily-report-agent 的变体——数据源从 Git API 换成网页,其他逻辑不变。

下一篇换方向:AI 写测试。让它自己读源码、写测试、跑测试、看报错、修代码。闭环自动化。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐