欢迎光临
我们一直在努力

AI 爬虫:Agent 自动采集 + 结构化网页信息

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

你要爬一个技术博客,先 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 爬虫抽取详情

进阶玩法

  • 增量爬取 —— 每天只爬新增的 URL,用已有文章做去重。
  • 竞品监控 —— 爬竞品的技术博客,每周生成一份「对手更新了什么」摘要。
  • 自动知识库 —— 把所有爬到的文章存向量数据库,建一个可搜索的技术知识图谱。
  • 这些都是 daily-report-agent 的变体——数据源从 Git API 换成网页,其他逻辑不变。

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

    赞(0)
    未经允许不得转载:171主机测评 » AI 爬虫:Agent 自动采集 + 结构化网页信息
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址