第2课:HTTP请求与响应处理详解
1. Go标准库net/http详解
在Go语言中,net/http
包是处理HTTP请求的标准库,它提供了强大而简洁的API。下面我们来了解如何创建和配置一个HTTP客户端:
1.1 HTTP客户端
go
package main
import (
"fmt"
"net/http"
"time"
"io"
)
func main() {
// 创建自定义HTTP客户端
client := &http.Client{
// 设置整体请求超时时间为10秒
Timeout: 10 * time.Second,
// Transport用于配置HTTP传输的细节
Transport: &http.Transport{
// 最大空闲连接数
MaxIdleConns: 10,
// 每个主机最大连接数
MaxConnsPerHost: 10,
// 空闲连接在关闭前的最大存活时间
IdleConnTimeout: 30 * time.Second,
},
}
// 发送GET请求
fmt.Println("发送请求中...")
resp, err := client.Get("https://httpbin.org/get")
if err != nil {
fmt.Printf("请求发生错误: %v\n", err)
return
}
// 重要:记得关闭响应体
defer resp.Body.Close()
// 读取响应体内容
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("读取响应体失败: %v\n", err)
return
}
// 打印响应详情
fmt.Printf("状态码: %s\n", resp.Status)
fmt.Printf("响应头: %v\n", resp.Header)
fmt.Printf("响应体长度: %d 字节\n", len(body))
fmt.Printf("响应内容: %s\n", body)
}
代码解释:
http.Client
是Go中发送HTTP请求的主要结构体Timeout
参数控制整个请求的超时时间(包括连接、发送和接收数据)Transport
允许我们自定义HTTP传输层的行为MaxIdleConns
和MaxConnsPerHost
帮助控制连接池的大小defer resp.Body.Close()
是必不可少的,防止资源泄漏
在爬虫开发中,设置请求超时非常重要,可以避免因单个请求卡住导致整个爬虫停止
1.2 创建自定义POST请求
有时候,我们需要更精细地控制HTTP请求的内容,例如指定请求方法、添加请求体或自定义请求头:
go
package main
import (
"fmt"
"net/http"
"strings"
"io"
)
func main() {
// 创建带有JSON请求体的POST请求
jsonData := `{"username": "clown5", "password": "123456"}`
body := strings.NewReader(jsonData)
// 使用http.NewRequest创建自定义请求
req, err := http.NewRequest("POST", "https://httpbin.org/post", body)
if err != nil {
fmt.Printf("创建请求失败: %v\n", err)
return
}
// 设置Content-Type为JSON
req.Header.Set("Content-Type", "application/json")
// 设置额外的请求头
req.Header.Set("X-API-Key", "your_api_key")
// 创建HTTP客户端并发送请求
client := &http.Client{}
fmt.Println("发送POST请求...")
resp, err := client.Do(req)
if err != nil {
fmt.Printf("发送请求失败: %v\n", err)
return
}
defer resp.Body.Close()
// 读取并显示响应
respBody, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("读取响应失败: %v\n", err)
return
}
fmt.Printf("状态码: %s\n", resp.Status)
fmt.Printf("响应体: %s\n", respBody)
}
1.3 创建自定义GET请求
当然我们也可以通过这个方法,创建GET请求 :
go
// 创建请求
req, err := http.NewRequest("GET", "https://httpbin.org/get", nil)//请求载荷设置为nil即可
if err != nil {
log.Fatalf("创建请求失败: %v", err)
}
1.4 创建HTTP客户端(带代理)
为了针对访问频繁,我们可以通过不断的切换代理IP来访问请求, 或者像Google、Ins这里网站需要VPN才能访问,我们也可以通过添加代理地址来访问。
go
//只需要增加http.Transport
proxyURL, err := url.Parse(ProxyURL)
if err != nil {
return nil, fmt.Errorf("解析代理URL失败: %v", err)
}
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{
Timeout: 30 * time.Second,
Transport: transport,
}
2. 设置请求头和Cookie
在实际爬虫开发中,我们常常需要设置各种HTTP头部和请求参数来模拟浏览器行为或满足网站要求。
2.1 管理请求头
请求头对于与Web服务器通信至关重要,合适的请求头可以帮助我们:
- 表明客户端身份(User-Agent)
- 指定接受的内容类型(Accept)
- 控制缓存行为(Cache-Control)
- 传递认证信息(Authorization)
go
package main
import (
"fmt"
"net/http"
"io"
)
func main() {
// 创建新的GET请求
req, err := http.NewRequest("GET", "https://httpbin.org/headers", nil)
if err != nil {
fmt.Printf("创建请求失败: %v\n", err)
return
}
// 设置模拟真实浏览器的请求头
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Cache-Control", "max-age=0")
req.Header.Set("Sec-Ch-Ua", "\"Not A(Brand\";v=\"99\", \"Google Chrome\";v=\"120\", \"Chromium\";v=\"120\"")
req.Header.Set("Sec-Ch-Ua-Mobile", "?0")
req.Header.Set("Sec-Ch-Ua-Platform", "\"Windows\"")
// 添加自定义头部
req.Header.Add("X-Requested-With", "XMLHttpRequest")
// 发送请求
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("请求失败: %v\n", err)
return
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("读取响应失败: %v\n", err)
return
}
// 打印状态和响应内容
fmt.Printf("状态码: %s\n", resp.Status)
fmt.Printf("响应体: %s\n", body)
// 打印所有响应头
fmt.Println("\n所有响应头:")
for key, values := range resp.Header {
for _, value := range values {
fmt.Printf("%s: %s\n", key, value)
}
}
}
User-Agent
告诉服务器关于客户端浏览器和操作系统的信息,现代网站通常使用请求头来防止爬虫,所以合理设置这些头部很重要Set()
方法会覆盖已有的头部值,而Add()
方法会添加新值(不替换已有值),请求头的名称是大小写不敏感的(Header会自动规范化名称)
2.2 使用Cookie获取登录状态
有很多数据需要我们登录才能访问,那么怎么获取登录状态呢?大部分网站登录后,都会生成一个cookie,这个cookie就保存了我们账号的登录状态(有的网站可能用的 Authorization)。
当账号登录后,我们只需要使用浏览器的开发者工具
, 从任意一个请求获取到cookie,然后添加到请求头即可。
实际上很多网站想要获取数据,还有一些其他的自定义请求,只有cookie是不够的
下面我们通过一个代码来简单的演示下,我们使用这个地址作为测试https://www.ghxi.com/userset
,访问账号设置, 如果账号登录应该是下面的界面

但是如果没登录直接访问,会跳转到登录界面
go
package main
import (
"fmt"
"bytes"
"mime/multipart"
"net/http"
"io"
)
func main() {
cookie :=`设置为你自己的cookie`
url := "https://www.ghxi.com/wp-admin/admin-ajax.php"
method := "POST"
payload := &bytes.Buffer{}
writer := multipart.NewWriter(payload)
_ = writer.WriteField("action", "wpcom_is_login")
err := writer.Close()
if err != nil {
fmt.Println(err)
return
}
client := &http.Client {
}
req, err := http.NewRequest(method, url, payload)
if err != nil {
fmt.Println(err)
return
}
req.Header.Add("cookie", cookie)
req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
req.Header.Set("Content-Type", writer.FormDataContentType())
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
运行测试,如果没有添加cookie,输出的内容会包含登录账号
的字段,如果正确的添加cookie,输出的内容会包含账号信息
。
3. 请求重试机制
我们在爬虫的时候,可能会因为网络波动等原因 ,导致访问失败,这时候就需要引入重试机制,避免数据遗漏。
go
package main
import (
"fmt"
"net/http"
"time"
)
func retryRequest(url string, maxRetries int) (*http.Response, error) {
var lastErr error
for attempt := 1; attempt <= maxRetries; attempt++ {
client := &http.Client{
Timeout: 5 * time.Second,
}
resp, err := client.Get(url)
if err == nil && resp.StatusCode < 500 {
return resp, nil
}
if resp != nil {
resp.Body.Close()
}
lastErr = err
if err == nil {
lastErr = fmt.Errorf("server error: %d", resp.StatusCode)
}
fmt.Printf("Attempt %d failed: %v\n", attempt, lastErr)
if attempt < maxRetries {
// 指数退避
waitTime := time.Duration(attempt) * time.Second
fmt.Printf("Waiting %v before retry...\n", waitTime)
time.Sleep(waitTime)
}
}
return nil, fmt.Errorf("all %d attempts failed: %v", maxRetries, lastErr)
}
func main() {
url := "https://httpbin.org/status/500"
resp, err := retryRequest(url, 3)
if err != nil {
fmt.Printf("Final error: %v\n", err)
return
}
defer resp.Body.Close()
fmt.Printf("Success! Status: %s\n", resp.Status)
}
4. 实战:爬取静态网页
让我们创建一个实际的爬虫示例,爬取一个引用网站(quotes.toscrape.com)并提取所有引言
4.1 定义爬虫结构
go
// Crawler 表示一个爬虫
type Crawler struct {
client *http.Client // HTTP客户端
userAgent string // 浏览器标识
delay time.Duration // 请求间隔时间
headers map[string]string
}
// NewCrawler 创建一个新的爬虫实例
func NewCrawler(timeout time.Duration, delay time.Duration) *Crawler {
return &Crawler{
client: &http.Client{
Timeout: timeout,
},
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
delay: delay,
}
}
4.2 定义错误结构
go
// 定义错误类型
type ErrorType string
const (
ErrorRequest ErrorType = "请求错误"
ErrorResponse ErrorType = "响应错误"
ErrorStatus ErrorType = "状态码错误"
)
// CrawlerError 代表爬虫错误
type CrawlerError struct {
Type ErrorType
Message string
Err error
}
func (e *CrawlerError) Error() string {
if e.Err != nil {
return e.Message + ": " + e.Err.Error()
}
return e.Message
}
4.3 实现基本的获取页面功能
go
// SetHeaders 设置自定义HTTP头部
func (c *Crawler) SetHeaders(headers map[string]string) {
// 如果设置了User-Agent,则更新crawler的userAgent属性
if ua, ok := headers["User-Agent"]; ok {
c.userAgent = ua
delete(headers, "User-Agent") // 从map中删除,避免重复设置
}
c.headers = headers
}
// Fetch 获取指定URL的网页内容
func (c *Crawler) Fetch(url string) ([]byte, error) {
// 创建请求
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, &CrawlerError{
Type: ErrorRequest,
Message: "创建请求失败",
Err: err,
}
}
// 设置头部
req.Header.Set("User-Agent", c.userAgent)
// 发送请求
resp, err := c.client.Do(req)
if err != nil {
return nil, &CrawlerError{
Type: ErrorRequest,
Message: "发送请求失败",
Err: err,
}
}
defer resp.Body.Close()
// 检查状态码
if resp.StatusCode != http.StatusOK {
return nil, &CrawlerError{
Type: ErrorStatus,
Message: "非正常状态码: " + resp.Status,
}
}
// 读取响应体
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, &CrawlerError{
Type: ErrorResponse,
Message: "读取响应失败",
Err: err,
}
}
// 返回页面内容
return body, nil
}
4.4 爬取静态网页
下面是,我们希望获取到的内容,为了获取到内容,我使用正则表达式来匹配
html
<span class="text" itemprop="text">"The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking."</span
html
<small class="author" itemprop="author">Albert Einstein</small>
go
func main() {
// 创建爬虫实例
crawler := NewCrawler(10*time.Second, 1*time.Second)
// 设置自定义头部
crawler.SetHeaders(map[string]string{
"Accept": "text/html,application/xhtml+xml,application/xml",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
})
// 定义目标URL
targetURL := "https://quotes.toscrape.com/"
fmt.Printf("开始爬取网站: %s\n", targetURL)
// 获取页面内容
content, err := crawler.Fetch(targetURL)
if err != nil {
log.Fatalf("爬取失败: %v", err)
}
// 提取引言(这里使用正则表达式作为示例,后面章节会介绍更好的HTML解析方法)
quoteRegex := regexp.MustCompile(`<span class="text" itemprop="text">(.*?)</span>`)
authorRegex := regexp.MustCompile(`<small class="author" itemprop="author">(.*?)</small>`)
quoteMatches := quoteRegex.FindAllStringSubmatch(string(content), -1)
authorMatches := authorRegex.FindAllStringSubmatch(string(content), -1)
if len(quoteMatches) == 0 {
log.Fatal("未找到任何引言")
}
// 创建结果文件
file, err := os.Create("quotes.txt")
if err != nil {
log.Fatalf("创建文件失败: %v", err)
}
defer file.Close()
// 写入引言
for i, quote := range quoteMatches {
if i < len(authorMatches) {
// 清理HTML实体
cleanQuote := strings.ReplaceAll(quote[1], """, "\"")
cleanQuote = strings.ReplaceAll(cleanQuote, "'", "'")
// 写入文件
line := fmt.Sprintf("%s - %s\n\n", cleanQuote, authorMatches[i][1])
file.WriteString(line)
// 打印到控制台
fmt.Println(line)
}
}
fmt.Printf("爬取完成,共获取 %d 条引言,已保存到 quotes.txt\n", len(quoteMatches))
}
在这个示例中,我们使用了正则表达式来提取网页中的引言和作者信息。这种方法适用于简单的提取任务,但对于复杂的HTML解析,我们需要使用专门的HTML解析库,这将在下一章介绍