应优先使用goquery或golang.org/x/net/html解析DOM提取链接,正则仅适用于极简可控场景;因HTML结构多变、编码复杂、易受注释和JS干扰,正则易漏匹配、性能差且难维护。

别用正则直接扫整个 HTML 字符串提取 <a> 链接——它不可靠、难维护、容易漏或错匹配;真要快又稳,优先走 goquery 或 golang.org/x/net/html 解析 DOM,正则只在极简场景兜底。
为什么正则提取 <a href="...">... 很容易翻车
HTML 不是正则友好的格式:标签属性顺序不确定(href 可能在 target 前或后)、有换行/缩进/多余空格、存在注释、JS 内联字符串干扰、甚至嵌套引号(href='javascript:alert("foo")')都会让 regexp.MustCompile(<a[^>]*href=["']([^"']*)["'][^>]*>([^<]*) 这类表达式崩掉。
- 常见错误现象:
FindAllStringSubmatch返回空 slice,但网页明明有链接——其实是正则没覆盖href=xxx无引号的情况,或被<!-- <a href=... -->注释吞了 - 性能陷阱:对几 MB 的 HTML 调用
FindAllStringSubmatch时,正则引擎反复回溯,CPU 占用飙升,比 DOM 解析慢 3–5 倍 - 编码风险:若 HTML 声明为
charset=gbk,而你用string(body)直接喂给正则,中文 href 会变成乱码,匹配失败
goquery 提取所有 href 的正确写法
关键不是“怎么写选择器”,而是怎么把 http.Response.Body 安全、准确地传给解析器。很多问题出在 Body 处理环节,不是语法写错。
- 必须检查
resp.StatusCode == http.StatusOK,否则NewDocumentFromReader会静默返回空文档,后续Find("a")看似成功,实则没节点 - 推荐用
bufio.NewReader(resp.Body)包一层,避免底层io.Readerbuffer 不足导致解析截断 - 若网页含
charset=gb2312,得先用golang.org/x/net/html/charset转码,再传给NewDocumentFromReader,否则Text()和Attr("href")都可能乱码 -
Find("a").Each(...)才真正遍历每个节点;Find("a").Attr("href")只返回第一个匹配项的值,不是全部
示例片段:
文章转信息图。将文章/笔记转化为手机可读的 HTML 信息图,自动匹配视觉风格。触发场景:文章转图、笔记转图、信息图、转小红书图、做张图、可视化这篇文章、文生图。
立即学习“go语言免费学习笔记(深入)”;
doc, err := goquery.NewDocumentFromReader(bufio.NewReader(resp.Body))
if err != nil {
return nil, err
}
var links []string
doc.Find("a").Each(func(i int, s *goquery.Selection) {
if href, ok := s.Attr("href"); ok && href != "" {
links = append(links, href)
}
})
什么时候才该用正则?以及怎么用才不坑
正则只适合两种情况:一是你**确定 HTML 片段结构极简且可控**(比如日志里埋的固定格式 <a href="https://x.com">link</a>);二是做预处理,比如从 DOM 提取的纯文本中再捞 URL(此时已无标签干扰)。
- 用
regexp.FindAllStringSubmatch比FindAllString更安全:它能分离捕获组,避免手动切字符串出错 - 正则模式别写太宽,例如
`href=["']([^"']+)["']`比`<a[^>]*href=["']([^"']*)["'][^>]*>`更稳定——前者只关心属性值,后者还得匹配整个标签结构 - 如果必须匹配带文本的链接,用命名捕获组 +
FindAllStringSubmatchIndex配合string()切片更可控,避免[][]byte转换出错 - 永远设超时:正则匹配失控时会卡死,尤其面对畸形 HTML,建议用
regexp.CompilePOSIX(更保守)或限制最大匹配长度
golang.org/x/net/html 解析的轻量替代方案
如果你不想引入 goquery(它依赖 net/html + jQuery 风格封装),又觉得正则太脆,net/html 原生 API 是最小依赖解法,但要注意递归边界和内存管理。
- 别用
html.Parse(io.Reader)直接读大文件——它会一次性加载全部节点到内存,10MB HTML 可能吃掉 50MB+ RAM - 遍历用
forEachNode递归函数,但务必在n.Type == html.ElementNode && n.Data == "a"时立即提取href属性,不要攒全量节点再处理 - 提取属性得手动遍历
n.Attrslice,href不一定在第一个位置,需循环比对key == "href" - 注意相对 URL:
href值可能是/path或./img.png,要用url.URL.ResolveReference转成绝对地址,否则后续请求会 404
真正耗时的从来不是“怎么写正则”或“怎么选库”,而是字符编码判断、Body 是否被提前读取、HTTP 状态码忽略、相对路径未补全这四点——它们不出错时一切顺利,一出就是静默失败。


















