提取src/href需用url.url.resolvereference解析相对路径,设http超时和user-agent,流式下载二进制,自定义transport调优连接池,用semaphore限并发,并对文件路径做clean及遍历校验。

用 goquery 提取 src 和 href 时容易漏掉相对路径
HTML 里静态资源路径不全是绝对 URL,img[src]、link[href]、script[src] 常见相对路径(如 ./js/app.js、../css/style.css、/static/logo.png),直接拼接下载会 404。
必须结合页面原始 URL 做解析,不能只靠字符串拼接。
推荐用标准库 url.URL 的 ResolveReference 方法,它能正确处理各种相对路径逻辑:
- 传入页面的原始
*url.URL(比如https://example.com/blog/post.html) - 对每个提取到的
src或href字符串构造一个*url.URL - 调用
baseURL.ResolveReference(refURL)得到绝对地址
别手写 strings.HasPrefix 判断开头是 / 还是 ./——这种逻辑在嵌套路径或带查询参数的 URL 下极易出错。
http.Client 下载二进制资源前要设好 Timeout 和 User-Agent
没设超时的 http.Client 遇到慢响应或挂起服务会卡死 goroutine;没设 User-Agent 的请求可能被 Nginx 或 CDN 直接拒收(返回 403),尤其对 .js、.css 等静态资源。
- 用
&http.Client{Timeout: 10 * time.Second}控制单次请求上限 - 在
req.Header.Set("User-Agent", "golang-fetcher/1.0")加标识 - 下载图片等二进制内容时,用
io.Copy流式写入文件,别全读进内存(resp.Body可能上百 MB)
示例关键片段:
resp, err := client.Do(req)<br>if err != nil { return }<br>defer resp.Body.Close()<br>out, _ := os.Create(filepath.Join(dir, filename))<br>io.Copy(out, resp.Body)
立即学习“go语言免费学习笔记(深入)”;
并发下载时要注意 net/http 默认连接池限制
Go 默认 http.DefaultClient 的 Transport 对同一 host 最多复用 2 个空闲连接,且最多 100 个总连接。如果一次性并发抓 50 个资源,大量请求会排队等待连接,实际变成串行。
- 显式配置
http.Transport:tr := &http.Transport{<br> MaxIdleConns: 100,<br> MaxIdleConnsPerHost: 100,<br> IdleConnTimeout: 30 * time.Second,<br>} - 避免用全局
http.DefaultClient,每个任务新建 client 或复用带定制 transport 的 client - 用
semaphore控制并发数(比如 ≤20),比无节制 goroutine 更稳
否则你会看到大量请求耗时突然跳到 30s+,其实是连接等待超时,不是网络慢。
保存文件前必须清理路径,防止 ../../../etc/passwd 类路径遍历
从 HTML 解析出的 src 可能含恶意路径(如 ../../config.json),直接 os.Create(path) 会写到任意目录。Go 没有内置“安全路径拼接”函数,得手动处理。
- 对解析后的文件名做
filepath.Clean(),再检查是否以".."开头或含".."路径段 - 更稳妥做法:提取最后一级文件名(
filepath.Base(u.Path)),忽略原始路径结构 - 如果必须保留目录结构(如
css/main.css),先Clean再确认结果仍在目标根目录下:absPath := filepath.Join(rootDir, cleanPath)<br>if !strings.HasPrefix(absPath, filepath.Clean(rootDir)+string(os.PathSeparator)) {<br> // 拒绝写入<br>}
这个点最容易被跳过——本地测试时一切正常,一上线遇到恶意 HTML 就出事。











