获取网页内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"fmt"
"io"
"net/http"
)

func main() {
// 目标网址
url := "https://httpbin.org/get"

// 发送 GET 请求
resp, err := http.Get(url)
if err != nil {
fmt.Println("请求失败:", err)
return
}
// 必须关闭响应体,防止内存泄漏
defer resp.Body.Close()

// 读取响应内容
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("读取内容失败:", err)
return
}

// 输出网页内容
fmt.Println("网页内容:")
fmt.Println(string(body))
}

解析html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"fmt"
"net/http"
"golang.org/x/net/html"
)

func main() {
// 获取网页
resp, _ := http.Get("https://golang.org")
defer resp.Body.Close()

// 解析 HTML
doc, err := html.Parse(resp.Body)
if err != nil {
fmt.Println("解析失败:", err)
return
}

// 遍历节点,提取链接
var links []string
var f func(*html.Node)
f = func(n *html.Node) {
// 如果是元素节点 且 标签是 a
if n.Type == html.ElementNode && n.Data == "a" {
// 遍历属性找 href
for _, attr := range n.Attr {
if attr.Key == "href" {
links = append(links, attr.Val)
}
}
}
// 递归遍历子节点
for c := n.FirstChild; c != nil; c = c.NextSibling {
f(c)
}
}
f(doc)

// 输出所有链接
fmt.Println("页面所有链接:")
for _, link := range links {
fmt.Println(link)
}
}

doc, err := html.Parse(resp.Body)的作用?

解析响应体中的HTML内容,返回一个HTML节点树的根节点doc。
把网页的原始 HTML 代码,变成 Go 能看懂、能遍历、能查找的 DOM 树结构

原生解析Json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"encoding/json"
"fmt"
)

// 定义结构体,对应 JSON 字段
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
}

func main() {
// JSON 字符串
jsonStr := `{"name":"张三","age":20,"email":"zhangsan@example.com"}`

// 声明结构体变量
var user User

// 解析 JSON
err := json.Unmarshal([]byte(jsonStr), &user)
if err != nil {
fmt.Println("解析失败:", err)
return
}

// 使用解析后的数据
fmt.Println("姓名:", user.Name)
fmt.Println("年龄:", user.Age)
fmt.Println("邮箱:", user.Email)
}

不确定json里有啥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"encoding/json"
"fmt"
)

func main() {
jsonStr := `{"name":"李四","age":25,"email":"lisi@example.com"}`

// 定义 map
var data map[string]interface{}

json.Unmarshal([]byte(jsonStr), &data)

// 取值需要类型断言
fmt.Println("name:", data["name"].(string))
fmt.Println("age:", data["age"].(float64)) // JSON 数字默认是 float64
}

带上jwt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"io"
"net/http"
)

func main() {
url := "https://httpbin.org/get"

// 1. 创建请求
req, _ := http.NewRequest("GET", url, nil)

// 2. 【关键】加 Token
token := "你的token字符串"
req.Header.Add("Authorization", "Bearer "+token)

// 3. 发送请求
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()

// 读取结果
body, _ := io.ReadAll(resp.Body)
println(string(body))
}

1

req.Header.Add(“Cookie”, “session=abc123; uid=10001”)

2

req.AddCookie(&http.Cookie{
Name: “session”,
Value: “abc123”,
})

1
2
3
4
5
6
7
8
9
10
11
req, _ := http.NewRequest("GET", url, nil)

// 1. 带Token
req.Header.Add("Authorization", "Bearer your-token-here")

// 2. 带Cookie
req.AddCookie(&http.Cookie{Name: "session", Value: "xxx"})

// 3. 还能加其他头
req.Header.Add("User-Agent", "Mozilla/5.0")
req.Header.Add("Referer", "https://google.com")

令牌桶 限流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package main

import (
"fmt"
"io"
"math/rand"
"net/http"
"time"
)

// 最大并发数:根据网站容忍度调整,一般 3~10
const maxConcurrency = 5

func main() {
// 构造待爬URL列表
urls := []string{
"https://www.baidu.com",
"https://www.qq.com",
"https://www.163.com",
"https://www.sina.com",
"https://www.zhihu.com",
"https://www.jd.com",
"https://www.taobao.com",
}

// 🔥 核心:带缓冲 channel = 并发控制器
tokenCh := make(chan struct{}, maxConcurrency)

// 遍历任务
for _, u := range urls {
// 1. 获取令牌(满了会阻塞,控制并发)
tokenCh <- struct{}{}

// 2. 启动协程爬取
go func(url string) {
// 函数结束:归还令牌
defer func() { <-tokenCh }()

// 🔥 防封关键:随机延时
delay := time.Millisecond * time.Duration(rand.Intn(1500)+500)
time.Sleep(delay)
fmt.Printf("开始爬取: %s,延时: %v\n", url, delay)

// 爬取逻辑
err := crawl(url)
if err != nil {
fmt.Printf("爬取失败: %s, err: %v\n", url, err)
} else {
fmt.Printf("爬取成功: %s\n", url)
}
}(u)
}

// 等待所有协程归还令牌
for i := 0; i < maxConcurrency; i++ {
tokenCh <- struct{}{}
}

fmt.Println("所有任务完成!")
}

// 爬取函数
func crawl(url string) error {
// 🔥 防封:设置超时
client := http.Client{Timeout: 10 * time.Second}

req, _ := http.NewRequest("GET", url, nil)

// 🔥 防封:加 UA、Cookie、Token(你之前学的)
req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")

resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

_, err = io.ReadAll(resp.Body)
return err
}

官方实现

golang.org/x/time/rate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

import (
"context"
"fmt"
"io"
"net/http"
"sync"
"time"

"golang.org/x/time/rate"
)

func main() {
urls := []string{
"https://www.baidu.com",
"https://www.qq.com",
"https://www.163.com",
}

// 🔥 真正的令牌桶:每秒 2 个请求,最多突发 3 个
limiter := rate.NewLimiter(2, 3)

var wg sync.WaitGroup

for _, u := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done()

// 🔥 核心:等待令牌(限流在这里)
// 没有令牌会阻塞,直到每秒生成新令牌
err := limiter.Wait(context.Background())
if err != nil {
fmt.Println("限流错误:", err)
return
}

// 开始爬取
crawl(url)
fmt.Println("完成:", url)
}(u)
}

wg.Wait()
fmt.Println("所有任务完成")
}

func crawl(url string) error {
client := http.Client{Timeout: 5 * time.Second}
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("User-Agent", "Mozilla/5.0")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
_, err = io.ReadAll(resp.Body)
return err
}

等令牌不会造成负担吗?
Go 的 goroutine(协程)被阻塞等待时,不占 OS 线程、不占 CPU、几乎不占内存!

UA池

每个请求随机换一个浏览器标识,不让网站发现你是固定 UA 的爬虫。

1
2
3
4
5
6
7
8
9
10
11
12
13
var uaList = []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ... Chrome/120.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ... Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15",
}

// 随机取一个
func randomUA() string {
return uaList[rand.Intn(len(uaList))]
}

// 使用
req.Header.Set("User-Agent", randomUA())

代理IP池

每个请求随机换一个代理 IP,不让网站发现你是固定 IP 的爬虫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 代理池
var proxyList = []string{
"http://192.168.1.100:8888",
"http://192.168.1.101:8888",
"http://192.168.1.102:8888",
}

// 随机取代理
func randomProxy() string {
return proxyList[rand.Intn(len(proxyList))]
}

// 构造带代理的 HTTP 客户端
func newClientWithProxy() *http.Client {
proxyURL, _ := url.Parse(randomProxy())

transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL), // 核心:走代理IP
}

return &http.Client{
Transport: transport,
Timeout: 10 * time.Second,
}
}

蘑菇代理、阿布云、快代理

Referer 链路模拟

爬首页 → Referer 为空 或 填搜索引擎
爬列表页 → Referer 填首页
爬详情页 → Referer 填列表页

1
2
3
4
5
6
7
8
9
访问首页: https://www.xxx.com
→ Referer: ""

访问列表页: https://www.xxx.com/list
→ Referer: "https://www.xxx.com"

访问详情页: https://www.xxx.com/detail/1
→ Referer: "https://www.xxx.com/list"

1
2
3
4
5
6
// 爬列表页时
req.Header.Set("Referer", "https://www.xxx.com")

// 爬详情页时
req.Header.Set("Referer", "https://www.xxx.com/list")

不能每次请求 new 一个 http.Client

每次都创建新的 Transport → 每次都创建新连接池 → 每次都重建 TCP 连接
重 慢

正确例子

// 全局单例
var globalClient = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
// 连接池配置…
},
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"net/http"
"sync"
"time"
)

// 全局单例 Client(爬虫唯一的 HTTP 客户端)
var (
spiderClient *http.Client
once sync.Once
)

// GetSpiderClient 单例模式获取客户端
func GetSpiderClient() *http.Client {
once.Do(func() {
spiderClient = &http.Client{
// 🔥 1. 全局超时(必设)
Timeout: 15 * time.Second,

// 🔥 2. 核心:自定义 Transport(连接池、代理、TCP 配置)
Transport: &http.Transport{
// 最大空闲连接(全局)
MaxIdleConns: 100,
// 每个 host 最大保持多少空闲连接(关键!)
MaxIdleConnsPerHost: 20,
// 连接空闲超时时间
IdleConnTimeout: 30 * time.Second,
// 建立 TCP 连接超时
DialTimeout: 5 * time.Second,
// 等待响应头超时
ResponseHeaderTimeout: 5 * time.Second,
// 禁用HTTP/2(很多网站对HTTP/2反爬更强)
ForceAttemptHTTP2: false,
// 代理配置(IP 池)
// Proxy: ... 后面讲
},
}
})
return spiderClient
}

cookieJar :go原生的自动cookie管理

(我去,之前我还真不知道这个)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
)

func main() {
// 1. 创建 Cookie 罐子
jar, _ := cookiejar.New(nil)

// 2. 创建带 Cookie 管理的客户端
client := &http.Client{
Jar: jar,
}

// 3. 登录(服务器会返回 Set-Cookie)
loginUrl := "https://example.com/login"
data := url.Values{"username": {"user"}, "password": {"123"}}
client.PostForm(loginUrl, data)

// 4. 访问需要登录的页面(自动带上 Cookie!)
resp, _ := client.Get("https://example.com/profile")
defer resp.Body.Close()

fmt.Println("请求成功,已自动携带登录 Cookie")
}

Transport应该全局单例

你问到了爬虫/接口请求最核心的痛点

我直接给你Go 里处理 JS 渲染、异步加载页面的所有实战方案,从简单到强大,你直接复制就能用



遇到 JS 渲染、异步加载的页面,直接用 http.Client 拿不到数据

方案 1:抓真实异步接口(最推荐、最快、最稳)

99% 的异步加载页面,数据都是通过 XHR / Fetch 接口加载的。

怎么做:

  1. 打开网页 F12 → Network → XHR / Fetch
  2. 找到真实数据接口
  3. 直接用 http.Client 请求这个接口

优点:

  • 速度极快
  • 性能最好
  • 不用渲染 JS
  • 最稳定

缺点:

  • 需要你手动找接口

方案 2:用 chromedp(控制 Chrome 无头浏览器)⭐⭐⭐⭐⭐

Go 最主流、最强大、企业级方案。
相当于用代码控制一个真正的 Chrome 浏览器。

安装

1
go get github.com/chromedp/chromedp

示例代码(直接运行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"context"
"log"
"time"

"github.com/chromedp/chromedp"
)

func main() {
// 创建 Chrome 上下文
ctx, cancel := chromedp.NewContext(
context.Background(),
chromedp.WithLogf(log.Printf),
)
defer cancel()

// 超时
ctx, cancel = context.WithTimeout(ctx, 15*time.Second)
defer cancel()

// 页面加载后,获取渲染完的 HTML
var html string
err := chromedp.Run(ctx,
chromedp.Navigate(`https://your-page.com`),
chromedp.WaitVisible(`#content`), // 等待某个元素出现
chromedp.OuterHTML(`html`, &html), // 获取渲染后的完整 HTML
)
if err != nil {
log.Fatal(err)
}

log.Println(html) // 这里就是 JS 渲染完的页面!
}

优点:

  • 完全模拟浏览器
  • 能处理登录、滑动、点击、JS 加密
  • 稳定、生态成熟

缺点:

  • 耗内存
  • 比直接请求接口慢

方案 3:用 go-rod(比 chromedp 更简单)⭐⭐⭐⭐

Go 界最简单好用的浏览器控制库,语法超直观。

安装

1
go get github.com/go-rod/rod

示例代码(极简)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"github.com/go-rod/rod"
)

func main() {
// 打开页面
page := rod.New().MustConnect().MustPage("https://your-page.com")

// 等待元素加载
page.MustWait(`#content`)

// 获取渲染后的 HTML
html := page.MustHTML()
fmt.Println(html)
}

优点:

  • 代码最少
  • 上手最快
  • 文档友好

方案 4:用 playwright-go(微软官方)⭐⭐⭐

playwright 是最强自动化工具,Go 只是其中一个语言 binding。

支持:Chrome、Firefox、Safari、Android、iOS…

安装

1
go get github.com/playwright-community/playwright-go

优点:

  • 功能最全面
  • 适合超级复杂网站
  • 能录屏、截图、抓网络请求

缺点:

  • 体积大
  • 稍微重

”让client sleep等加载完后再拉html也是一样的吧?”

http.Client 只是个下载 HTML 的工具,它:

  • 不能渲染 JS
  • 不能处理异步加载的资源
  • 不能模拟浏览器行为

指数退避

每次重试,等待时间翻倍。