1. net/http 包相关方法

1.1 http.NewRequestWithContext

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

1.2 Request 结构体类型

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

1.3 http.DefaultClient.Do

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

1.4 Response 结构体类型

1
2
3
4
5
6
7
8
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

1
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. 具体实现代码

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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)
}
}