使用 net/http 实现并发爬取多个 url 标题

1. net/http 包相关方法

1.1 http.NewRequestWithContext

req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
  • 这个方法用于创建一个新的 HTTP 请求。
  • 它接受一个 context.Context 对象,可以用来设置请求的超时、取消等操作。
  • 第一个参数是 HTTP 方法,这里是 “GET”。
  • 第二个参数是要请求的 URL。
  • 第三个参数是请求体,这里传入 nil 表示没有请求体。
  • 返回一个 *http.Request 对象和错误对象。

1.2 Request 结构体类型

type Request struct {
   Method string // 指定HTTP方法(GET,POST,PUT等)。
   URL *url.URL
   ......
}

1.3 http.DefaultClient.Do

resp, err := http.DefaultClient.Do(req)
  • http.DefaultClient 是一个全局的 *http.Client 对象,它提供了默认的 HTTP 客户端实现。
  • Do 方法用于发送 HTTP 请求并返回响应。
  • 它接受一个 *http.Request 对象作为参数,表示要发送的请求。
  • 返回一个 *http.Response 对象和一个错误对象。

1.4 Response 结构体类型

type Response struct {
  Status   string // e.g. "200 OK"
  StatusCode int   // e.g. 200
  Proto    string // e.g. "HTTP/1.0"
  ProtoMajor int   // e.g. 1
  ProtoMinor int   // e.g. 0
  ......
}

1.5 http.Response

  • http.Response 结构表示 HTTP 响应。
  • 它包含响应状态码、响应头和响应体等信息。
  • 在代码中,我们使用 resp.StatusCode 来检查响应的状态码是否为200,以确定请求是否成功。

1.6 http.Response.Body

defer resp.Body.Close()
  • Body 字段是一个 io.ReadCloser 接口,代表响应体。
  • 在读取完响应体后,我们应该关闭响应体以释放资源。通常使用 defer 关键字来确保在函数退出时关闭响应体。

2. golang.org/x/net/html 包相关方法

2.1 html.Parse

  • func Parse(r io.Reader) (*Node, error)
  • 此函数接受一个实现了 io.Reader 接口的对象作为参数,通常是一个 http.Response.Body 或文件等。
  • 返回一个 *html.Node 对象和一个 error,表示解析的根节点以及可能发生的错误。

2.2 html.Render

  • func Render(w io.Writer, n *Node) error
  • 此函数接受一个实现了 io.Writer 接口的对象以及一个 *html.Node 对象作为参数,将HTML节点 n 以HTML格式写入 w。
  • 返回一个 error,表示可能发生的写入错误。

2.3 html.ParseFragment

  • func ParseFragment(r io.Reader, context *Node) ([]*Node, error)
  • 此函数接受一个实现了 io.Reader 接口的对象以及一个上下文节点 *html.Node 对象作为参数。
  • 返回解析的HTML片段中的节点切片和一个 error

2.4 html.EscapeString

  • func EscapeString(s string) string
  • 此函数接受一个HTML字符串作为参数,返回其在HTML中的转义形式。

2.5 html.UnescapeString

  • func UnescapeString(s string) string
  • 此函数接受一个转义过的HTML字符串作为参数,返回其原始形式。

2.6 html.Node

  • HTML文档中的节点表示。
  • 每个节点都有一个类型、一系列属性和子节点。
  • 可以通过 Type 字段来判断节点的类型,如 ElementNodeTextNode 等。
  • 可以通过 Data 字段获取节点的数据,如元素节点的标签名或文本节点的内容。
  • 可以通过 Attr 字段获取节点的属性。

3. 具体实现代码

package main

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

	"golang.org/x/net/html"
)

// fetchTitle 使用给定的URL获取网站的标题。
func fetchTitle(ctx context.Context, url string) (string, error) {
	startTime := time.Now()
	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		return "", err
	}

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

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("bad status: %s", resp.Status)
	}

	doc, err := html.Parse(resp.Body)
	if err != nil {
		return "", err
	}

	var title string
	var f func(*html.Node)
	f = func(n *html.Node) {
		if n.Type == html.ElementNode && n.Data == "title" && n.FirstChild != nil {
			title = n.FirstChild.Data
		}
		for c := n.FirstChild; c != nil; c = c.NextSibling {
			f(c)
		}
	}
	f(doc)

	elapsedTime := time.Since(startTime).Round(time.Millisecond)
	return title + " (" + elapsedTime.String() + ")", nil
}

// crawlURLs 并发地爬取一系列URL的标题。
func crawlURLs(ctx context.Context, urls []string) ([]string, error) {
	// 使用WaitGroup等待所有的goroutine完成
	var wg sync.WaitGroup
	wg.Add(len(urls))

	// 使用通道来收集结果(防止结果竟态)
	titles := make(chan string, len(urls))
    
	for _, url := range urls {
        // 每一个url创建一个goroutine
		go func(url string) {
			defer wg.Done()
			title, err := fetchTitle(ctx, url)
			if err != nil {
				fmt.Printf("Error fetching %s: %v\n", url, err)
				titles <- "Error: " + err.Error()
				return
			}
			// 将标题发送到通道
			titles <- title
		}(url)
	}

	// 等待所有的goroutine完成
	wg.Wait()
	close(titles)

	// 将通道的结果收集到数组中
	resultTitles := make([]string, 0, len(urls))
	for title := range titles {
		resultTitles = append(resultTitles, title)
	}

	return resultTitles, nil
}

func main() {
	urls := []string{
		"https://www.baidu.com",
		"https://www.36kr.com",
		"https://www.sina.com.cn",
		"https://www.jd.com",
		"https://www.taobao.com",
		"https://www.pinduoduo.com",
		"https://www.tmall.com",
		"https://www.zhihu.com",
		"http://www.juejin.cn",
		"https://www.aliyun.com",
	}

	// 创建一个上下文,例如,用于设置超时
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	titles, err := crawlURLs(ctx, urls)
	if err != nil {
		fmt.Println("Error:", err)
		os.Exit(1)
	}

	for i, title := range titles {
		fmt.Printf("%d: %s\n", i+1, title)
	}
}

使用 net/http 实现并发爬取多个 url 标题
http://coderedeng.github.io/2024/04/30/Go爬虫 - 手动实现并发爬取多个url标题/
作者
Evan Deng
发布于
2024年4月30日
许可协议