网络爬虫:用 Colly 抓网页,用 chromedp 抓动态页
网络爬虫:用 Colly 抓网页,用 chromedp 抓动态页
这一章把前 17 章的 Go 知识拧成一条工程流水线:自动抓网页数据。 爬虫干的事一句话:替人访问网页,把要的字段抠出来存好。 主线例子会带你在一个真实网站上走完全程——从访问首页、找到「Python 指南」链接, 到抓出该分类下全部文章、写进 CSV 文件;最后换成「无头浏览器」, 去抓普通工具抓不动的 JavaScript 动态页。
1. 爬虫是什么,流水线有几步
书里给的定义:爬虫是从网上自动批量(不需要人一个个点)取数据的过程, 数据可以是结构化的(整整齐齐的表格)或非结构化的(没人排版的自由文本)1。
一条完整的流水线六步,书里列得清楚2:
| 步骤 | 干什么 | 本章用什么 |
|---|---|---|
| Web 请求 | 访问 URL,把网页内容要过来 | Colly 的 Visit |
| HTML 解析 | 把原始 HTML 读成结构化的「DOM 树」 | Colly 内置解析器 |
| 数据提取 | 从树里抠出要的字段 | CSS 选择器 + OnHTML |
| 数据转换 | 清洗、整形 | 装进 struct |
| 存储 | 存 CSV/数据库 | encoding/csv |
| 自动化 | 批量、翻页、循环 | 回调里再 Visit |
(HTML 是网页的标记语言;DOM 树是把 HTML 解析成的结构——每个标签是树上的一个节点。)
Go 为什么适合干这个,书里给的理由很直接:并发是爬虫的命门—— 成千上万个页面要同时抓,而并发恰好是 Go 的看家本领3。
2. 工具箱:Go 的爬虫库
书里列了五个,按用途记住名字和分工4:
| 库 | 分工 | 什么时候用 |
|---|---|---|
| Colly | 通用爬虫框架,API 干净,支持并发 | 静态页面的首选,本章主角 |
| GoQuery | 类 jQuery 的 HTML 解析 | 只要解析、不要框架时 |
| Goose | 专抓新闻文章正文 | 内容提取专用 |
| Pholcus | 分布式大规模爬虫 | 集群级任务 |
| chromedp | 驱动真 Chrome(无头) | JavaScript 渲染的动态页 |
Colly 的卖点书里给了硬数字:单机单核每秒 1000+ 请求5。
3. Colly 的心智模型:Collector + 事件回调
Colly 的世界观两个词:Collector(采集器) 和 事件回调。
Collector 是爬虫本体:它规定怎么逛网站、每个页面干什么;
用 colly.NewCollector() 造一个默认配置的6。
事件回调是 Colly 的核心机制:你给各种事件挂函数,事情发生时 Colly 回调你。 常用事件7:
| 事件 | 什么时候回调 | 典型用途 |
|---|---|---|
OnRequest | 发出请求之前 | 改请求头、打印正在访问的 URL |
OnResponse | 收到响应之后 | 看状态码、响应体 |
OnError | 出错时(网络错、超时、解析错) | 记日志、容错 |
OnHTML | 页面里有元素匹配给定选择器时 | 抠数据的主战场 |
OnScraped | 页面抓完之后 | 收尾清理 |
还有一个工程要素:请求排队和限速。Colly 默认按添加顺序处理请求,
并支持按域名设置并发数和请求间隔(防止把人家服务器打爆);
限速还可以配第三方库 golang.org/x/time/rate8。
4. 主线例子:抓下「Python 指南」的全部文章
照原书,目标站是 erraticcoder.com,四步走通9。
第 ① 步:最小爬虫——访问一个页面。
go mod init webScraper // 建模块
go get github.com/gocolly/colly // 装 Colly
c := colly.NewCollector()
c.OnRequest(func(r *colly.Request) {
fmt.Println("Visiting: ", r.URL)
})
c.Visit("https://erraticcoder.com") Start Web Scraping!
Visiting: erraticcoder.com图说:Visit 一调,流水线启动——先回调 OnRequest(所以先看到 Visiting),
再真的发 HTTP 请求。挂上 OnResponse 能看到「请求完成」,
URL 写错(如 https://1.com)则 OnError 收到
dial tcp: lookup .com: no such host10。
第 ② 步:用选择器抠数据。 OnHTML 的第一个参数是 CSS 选择器——
描述「我要哪种标签」的查询语言:
c.OnHTML("a", func(e *colly.HTMLElement) {
fmt.Println(e.Attr("href")) // 打印页面上所有链接
})输出是一串 URL,从首页链到 /blog/、/golang/、/python/、
各篇文章……11 想看网页上有哪些标签?浏览器的开发者工具
(右键 → Inspect,或 Ctrl+Shift+I)能直接看任何页面的 HTML 结构12。
第 ③ 步:顺藤摸瓜——进分类页抓列表。
要的是「Python guide」分类下的文章列表,两步走:
先在首页找到文字包含 "Python guide" 的那个链接,再访问它、
抓它里面 li.listing-item a(列表项里的链接)的文字和地址:
c.OnHTML("a:contains('Python guide')", func(e *colly.HTMLElement) {
pythonURL := e.Attr("href") // 第一步:拿到分类页地址
fmt.Println("Python Guide URL: ", pythonURL)
c.OnHTML("li.listing-item a", func(e *colly.HTMLElement) {
fmt.Println(e.Text, ": ", e.Attr("href")) // 第二步:每篇文章的标题+链接
})
c.Visit(pythonURL) // 访问分类页,触发上面的回调
})输出形如:Where do we use decision tree algorithm : https://erraticcoder.com/2022/01/20/where-do-we-use-decision-tree-algorithm/,
共 10 篇13。
图说:回调里再注册回调、再 Visit——爬虫就是这样「从一个页面爬到另一个页面」的。 每次 Visit 都重新走一遍整条流水线(请求 → 解析 → 匹配选择器 → 回调)。
第 ④ 步:装进结构体,写进 CSV。
存数据先用学过的 struct(呼应第 10 章):
type PythonBlogs struct {
subject, url string
}
var blogs []PythonBlogs
// 在 OnHTML 回调里:
pythonBlog := PythonBlogs{subject: e.Text, url: e.Attr("href")}
blogs = append(blogs, pythonBlog)写 CSV 四行骨架,defer 收尾(呼应第 16 章的两条 defer 纪律):
file, _ := os.Create("blogs.csv")
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush() // 写完必须 Flush,缓冲才真正落盘
writer.Write([]string{"subject", "url"}) // 表头
for _, blog := range blogs {
writer.Write([]string{blog.subject, blog.url})
}至此,一个能跑、能存盘的完整爬虫收工14。
5. 动态页与无头浏览器:chromedp
静态 HTML 用 Colly 就够;但有些页面的内容是 JavaScript 在浏览器里跑完之后 才生成的——HTTP 请求拿到的只是一具空壳。解法:驱动一个真浏览器, 让它跑完 JS 再抠数据。 无头(headless)模式 = 不弹出窗口、在后台跑, 省掉图形渲染的开销,还能在服务器上跑15。
书里用 chromedp 抓 books.toscrape.com 的书名和价格,翻 5 页。三个新零件:
零件一:context(上下文)。 chromedp.NewContext(context.Background())
开一个浏览器会话;defer cancel() 在 main 结束时释放浏览器资源
——书里特意解释了 context 的作用:管超时、取消、跨调用传信息,
以及「结束就 cancel 防泄漏」这条 Go 惯例16。
零件二:chromedp.Run(ctx, 一串动作)。 Run 把「导航、等待、点击、
取文本」这些动作排成队执行:
err := chromedp.Run(ctx,
chromedp.Navigate(url), // 打开页面
chromedp.Sleep(5*time.Second), // 等 JS 渲染完
chromedp.ActionFunc(func(ctx context.Context) error {
// 自定义动作:在这里抠数据、翻页
}),
)零件三:选择器再取文本 + 翻页循环。 核心动作在 ActionFunc 里:
取所有书节点、逐本取出标题(h3)和价格(p.price_color)写入 CSV,
然后 chromedp.Click(".next > a") 点「下一页」,循环 5 次17:
for count := 0; count < pageCount; count++ {
var articles []*cdp.Node
chromedp.Run(ctx, chromedp.Nodes("article.product_pod", &articles))
for _, article := range articles {
var title, price string
chromedp.Run(ctx, chromedp.Text("h3", &title, chromedp.FromNode(article)))
chromedp.Run(ctx, chromedp.Text("p.price_color", &price, chromedp.FromNode(article)))
writer.Write([]string{title, price})
}
chromedp.Run(ctx, chromedp.Click(".next > a")) // 翻页
time.Sleep(10 * time.Second)
}书里提醒了一个工程现实:网页结构会变——今天写死的选择器, 网站改版后就抓空了,爬虫脚本要跟着维护18。
6. 别给人家添堵:四条规矩
书末的合规清单,原样接住19:
- 表明身份:按目标站要求带上 User-Agent 等请求头;
- 尊重版权和隐私:不用爬虫侵犯知识产权、不碰用户隐私;
- 别抓敏感信息;
- 别把服务器打垮:控制请求量和频率、拉长间隔、避开高峰; 遵守 robots.txt(网站放在根路径下、给爬虫看的「哪些能抓哪些不能抓」告示)。
判断(我们的,不是书里的): 这一章的工具教学中规中矩, 但「别添堵」四条放在章末而不是开头,顺序值得商榷—— 现实中爬虫项目的第一课往往是「这个站让不让抓」,而不是「怎么抓」。 技术上还有一个书里没提的默认答案:有 API 就先用 API (官方接口稳定、合法、通常更省),爬虫应该是 API 不存在时的第二选择。 如果错,会错在: 有些场景(价格监控、竞品分析)根本没有 API, 爬虫是唯一通道——这时四条规矩从「注意事项」升级成「生死线」, 违反的后果从「被封 IP」到「收律师函」都有。
7. 边界与局限
- 没有讲并发抓取的配置(Colly 的 AllowedDomains、Parallelism、 Async 模式),而这恰是 Go 爬虫相对 Python 的主场;
- chromedp 的例子靠
Sleep硬等渲染,实际更稳的写法是chromedp.WaitVisible(...)等具体元素出现(补充,不在书里); - 反爬对抗(验证码、IP 池、登录态)没有涉及——入门之外的深水区;
- 数据存储只到 CSV,数据库落库(配合 GORM)未展开。
8. 可带走的
- 爬虫流水线六步:请求 → 解析 HTML → 提取 → 转换 → 存储 → 自动化;
- Go 爬虫库五选一:Colly(通用)、GoQuery(纯解析)、Goose(文章)、 Pholcus(分布式)、chromedp(动态页);
- Colly 心智模型:Collector + 事件回调;OnRequest/OnResponse/OnError/OnHTML/OnScraped 各司其职;
- CSS 选择器是抠数据的语言;浏览器开发者工具(Inspect)看页面结构;
- 回调里可以再注册回调、再 Visit——爬虫靠这个跨页面爬;
- 抓到的数据装进 struct 切片,
encoding/csv写盘; csv.Writer 用完必须 Flush; - defer 收尾两件事:关文件、cancel 浏览器 context;
- 动态页(JavaScript 渲染)要用 chromedp 驱动真浏览器,无头模式后台跑;
- 网页结构会变,选择器要跟着维护;
- 规矩四条:表明身份、尊重版权隐私、不抓敏感信息、限速 + 遵守 robots.txt。
9. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 爬虫定义与六步流水线 | Web Scraping in Go | text/27-fm-learning-go-programming.txt:61(搜「automated process」) · text/27-fm-learning-go-programming.txt:64(搜「different stages」) |
| Go 适合爬虫的理由 | Web Scraping in Go | text/27-fm-learning-go-programming.txt:55(搜「large-scale」) |
| 五个爬虫库 | Web Scraping in Go | text/27-fm-learning-go-programming.txt:94(搜「clean APIs」) · text/27-fm-learning-go-programming.txt:118(搜「Chrome DevTools Protocol」) |
| Colly 组件与事件 | Web Scraping in Go | text/27-fm-learning-go-programming.txt:133(搜「Collector is a core component」) · text/27-fm-learning-go-programming.txt:148(搜「OnHTML」) · text/27-fm-learning-go-programming.txt:172(搜「rate limiting」) |
| Colly 每秒千次请求 | Web Scraping in Go | text/27-fm-learning-go-programming.txt:184(搜「1K requests/second」) |
| go mod init 与安装 Colly | Web Scraping in Go | text/27-fm-learning-go-programming.txt:226(搜「go mod init webScraper」) · text/27-fm-learning-go-programming.txt:235(搜「go get github.com/gocolly/colly」) |
| 最小爬虫与三个事件回调 | Web Scraping in Go | text/27-fm-learning-go-programming.txt:269(搜「colly.NewCollector()」) · text/27-fm-learning-go-programming.txt:293(搜「erraticcoder.com」) · text/27-fm-learning-go-programming.txt:350(搜「no such host」) |
| OnHTML 抠全部链接 | Web Scraping in Go | text/27-fm-learning-go-programming.txt:377(搜「erraticcoder.com/blog/」) |
| 顺藤摸瓜进分类页 | Web Scraping in Go | text/27-fm-learning-go-programming.txt:407(搜「a:contains('Python Guide')」) · text/27-fm-learning-go-programming.txt:449(搜「Python Guide URL」) · text/27-fm-learning-go-programming.txt:452(搜「decision-tree-algorithm」) |
| struct 存数据与 CSV 输出 | Web Scraping in Go | text/27-fm-learning-go-programming.txt:518(搜「type PythonBlogs struct」) · text/27-fm-learning-go-programming.txt:644(搜「[{Where do we use」) |
| CSV 写入骨架 | Web Scraping in Go | text/27-fm-learning-go-programming.txt:653(搜「os.Create("blogs.csv")」) · text/27-fm-learning-go-programming.txt:662(搜「writer.Flush()」) |
| 无头浏览器与 chromedp | Web Scraping in Go | text/27-fm-learning-go-programming.txt:854(搜「hides the browser window」) · text/27-fm-learning-go-programming.txt:881(搜「go get -u github.com/chromedp/chromedp」) |
| context 与 cancel | Web Scraping in Go | text/27-fm-learning-go-programming.txt:953(搜「chromedp.NewContext」) · text/27-fm-learning-go-programming.txt:965(搜「prevent resource leaks」) |
| chromedp 翻页抓书 | Web Scraping in Go | text/27-fm-learning-go-programming.txt:1085(搜「article.product_pod」) · text/27-fm-learning-go-programming.txt:1103(搜「p.price_color」) · text/27-fm-learning-go-programming.txt:1124(搜「Click(".next > a")」) |
| 网页结构会变的提醒 | Web Scraping in Go | text/27-fm-learning-go-programming.txt:1157(搜「different over time」) |
| 四条合规规矩 | Web Scraping in Go | text/27-fm-learning-go-programming.txt:1166(搜「compliance with the law」) · text/27-fm-learning-go-programming.txt:1181(搜「robots.txt」) |
Footnotes
-
出处:「Web Scraping in Go」第 58–61 段(
text/27-fm-learning-go-programming.txt:61,搜「automated process」)。 ↩ -
出处:「Web Scraping in Go」第 64–82 段(
text/27-fm-learning-go-programming.txt:67,搜「Web request」;text/27-fm-learning-go-programming.txt:70,搜「DOM tree」)。 ↩ -
出处:「Web Scraping in Go」第 55 段(
text/27-fm-learning-go-programming.txt:55,搜「large-scale」)。 ↩ -
出处:「Web Scraping in Go」第 85–124 段(五个库的介绍与导入路径)。 ↩
-
出处:「Web Scraping in Go」第 184 段(
text/27-fm-learning-go-programming.txt:184,搜「1K requests/second」)。 ↩ -
出处:「Web Scraping in Go」第 127–133 段(
text/27-fm-learning-go-programming.txt:133,搜「Collector is a core component」)。 ↩ -
出处:「Web Scraping in Go」第 142–167 段(各事件回调的定义)。 ↩
-
出处:「Web Scraping in Go」第 169–172 段(
text/27-fm-learning-go-programming.txt:172,搜「rate limiting」)。 ↩ -
出处:「Web Scraping in Go」第 214–644 段,主线例子的四步均照原书。 ↩
-
出处:「Web Scraping in Go」第 287–356 段(
text/27-fm-learning-go-programming.txt:293,搜「erraticcoder.com」;text/27-fm-learning-go-programming.txt:350,搜「no such host」)。 ↩ -
出处:「Web Scraping in Go」第 359–401 段(
text/27-fm-learning-go-programming.txt:377,搜「erraticcoder.com/blog/」)。 ↩ -
出处:「Web Scraping in Go」第 482 段(
text/27-fm-learning-go-programming.txt:482,搜「Ctrl + Shift + I」)。 ↩ -
出处:「Web Scraping in Go」第 404–479 段(
text/27-fm-learning-go-programming.txt:449,搜「Python Guide URL」;text/27-fm-learning-go-programming.txt:452,搜「decision-tree-algorithm」)。 ↩ -
出处:「Web Scraping in Go」第 485–848 段(
text/27-fm-learning-go-programming.txt:518,搜「type PythonBlogs struct」;text/27-fm-learning-go-programming.txt:644,搜「[{Where do we use」;CSV 骨架见第 653–662 段)。 ↩ -
出处:「Web Scraping in Go」第 851–875 段(
text/27-fm-learning-go-programming.txt:854,搜「hides the browser window」)。 ↩ -
出处:「Web Scraping in Go」第 950–968 段(
text/27-fm-learning-go-programming.txt:956,搜「context library」;text/27-fm-learning-go-programming.txt:965,搜「prevent resource leaks」)。 ↩ -
出处:「Web Scraping in Go」第 974–1154 段(
text/27-fm-learning-go-programming.txt:1085,搜「article.product_pod」;text/27-fm-learning-go-programming.txt:1103,搜「p.price_color」;text/27-fm-learning-go-programming.txt:1124,搜「Click(".next > a")」)。 ↩ -
出处:「Web Scraping in Go」第 1157 段(
text/27-fm-learning-go-programming.txt:1157,搜「different over time」)。 ↩ -
出处:「Web Scraping in Go」第 1163–1181 段(
text/27-fm-learning-go-programming.txt:1166,搜「compliance with the law」;text/27-fm-learning-go-programming.txt:1178,搜「overburdening」;text/27-fm-learning-go-programming.txt:1181,搜「robots.txt」)。 ↩