Go语言爬虫系列教程(二) HTTP请求与响应处理详解

发布于:2025-05-23 ⋅ 阅读:(16) ⋅ 点赞:(0)

第2课:HTTP请求与响应处理详解

1. Go标准库net/http详解

在Go语言中,net/http包是处理HTTP请求的标准库,它提供了强大而简洁的API。下面我们来了解如何创建和配置一个HTTP客户端:

1.1 HTTP客户端

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传输层的行为
  • MaxIdleConnsMaxConnsPerHost帮助控制连接池的大小
  • defer resp.Body.Close()是必不可少的,防止资源泄漏

在爬虫开发中,设置请求超时非常重要,可以避免因单个请求卡住导致整个爬虫停止

1.2 创建自定义POST请求

有时候,我们需要更精细地控制HTTP请求的内容,例如指定请求方法、添加请求体或自定义请求头:

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请求 :


    
    // 创建请求
    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才能访问,我们也可以通过添加代理地址来访问。

	//只需要增加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)
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 ,访问账号设置, 如果账号登录应该是下面的界面

在这里插入图片描述

但是如果没登录直接访问,会跳转到登录界面

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. 请求重试机制

我们在爬虫的时候,可能会因为网络波动等原因 ,导致访问失败,这时候就需要引入重试机制,避免数据遗漏。

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 定义爬虫结构

// 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 定义错误结构


// 定义错误类型
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 实现基本的获取页面功能


// 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 爬取静态网页

下面是,我们希望获取到的内容,为了获取到内容,我使用正则表达式来匹配

<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

<small class="author" itemprop="author">Albert Einstein</small>

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], "&quot;", "\"")
			cleanQuote = strings.ReplaceAll(cleanQuote, "&#39;", "'")

			// 写入文件
			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解析库,这将在下一章介绍