碎碎念

跟着小生凡一老师做的,目的是熟悉网络的请求与响应&数据库
从17号晚上开始敲了两三个点马马虎虎敲完了,不到200行,中间连接数据库,import等等耽误时间了,先复习一遍,然后找个静态网站抓一下

不管代码有多少行,一个爬虫的核心流程通常是:
发送请求获取响应解析数据保存结果
所以接下来也按照这个步骤来过一遍两个项目

代码开源在了go-proj-self


(一)抓取豆瓣top250

发送请求

  • 构造客户端
1
client:=http.Client
  • 构造GET请求

网页处按F12->点network->刷新->点TOP250->会有url,请求方法等

1
2
3
4
5
req,err:=http.NewRequest("get","https://movie.douban.com/top250",nil)
//请求体没有所以是nil
if err != nil { //如果出错
fmt.Println("req err", err)
}
  • 添加请求头,伪造成浏览器请求,防止被反爬虫识别而失败
1
req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36")
  • 发送请求
1
2
3
4
5
resp, err := client.Do(req)
if err != nil {
fmt.Println("请求失败", err)
}
defer resp.Body.Close()//防止资源泄露

获取响应

当请求成功时,可以正常获取响应

解析数据

css选择器 在爬取静态页面的时候常用css选择器

  • 将 HTTP 响应的 Body 转换为一个可操作的 DOM 文档
1
2
3
4
docDetail, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
fmt.Println("解析失败", err)
}
  • 获取节点信息
    • Find(selector)通过css选择器定位到html目标
    • Each(func(i int,s *goquery.Selection){})遍历找到的所有元素,执行自定义逻辑
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
//find根据css选择器找到目标html元素,each遍历元素执行自定义操作func
docDetail.Find("#content > div > div.article > ol > li").
Each(func(i int, s *goquery.Selection) { //s是当前节点的索引

//存储元素,为插入数据库做准备
var data MovieData

title := s.Find("div > div.info > div.hd > a > span:nth-child(1)").Text() //提取文本内容:标题

img := s.Find("div > div.pic > a > img") //找到img元素
imgtem, ok := img.Attr("src") //提取src

info := s.Find("div > div.info > div.bd > p:nth-child(1)").Text() //简洁(导演,主演,年份)

score := s.Find("div > div.info > div.bd > div > span.rating_num").Text() //评分
quote := s.Find("div > div.info > div.bd > p.quote > span").Text() //引言

if ok {
director, actor, year := InfoSpite(info) //构造InfoSpite函数解析字段提取主演,导演,年份

data.Title = title
data.Picture = imgtem
data.Score = score
data.Quote = quote

data.Actor = actor
data.Director = director
data.Year = year

if InsertData(data) { //插入数据库

} else {
fmt.Println("插入失败")
return
}

}
})
fmt.Println("插入成功")
return

保存数据

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
func InitDB() { //初始化连接数据库
//path := strings.Join([]string{USERNAME, ":", PASSWORD, "@tcp(", HOST, ":", PORT, ")/", DBNAME, "?charset=utf8"}, "")
path := USERNAME + ":" + PASSWORD + "@tcp(" + HOST + ":" + PORT + ")/" + DBNAME + "?charset=utf8"

DB, _ = sql.Open("mysql", path) //打开数据库连接
//设置了连接的最大存活时间为 10(单位应该是与具体实现相关,通常是秒),意味着一个数据库连接在经过 10 个单位时间后,如果还没有被使用,将会被关闭回收,这样可以避免长时间闲置的连接占用过多资源
DB.SetConnMaxLifetime(10)
//设定了最大空闲连接数为 5,也就是数据库连接池中最多允许存在 5 个处于空闲状态的连接,当空闲连接数超过这个值时,多余的空闲连接会被关闭,有助于合理控制资源使用和提升数据库连接性能
DB.SetMaxIdleConns(5)

//通过 DB.Ping() 语句来测试与数据库的连接是否可用
//它会向数据库发送一个简单的测试请求(比如 MySQL 中类似 SELECT 1 的操作)
//如果连接正常且数据库可以响应,则返回 nil ,否则返回一个包含错误信息的 error 类型值。
if err := DB.Ping(); err != nil {
fmt.Println("opon database fail")
return
}

fmt.Println("connect success")
}

func InsertData(movieData MovieData) bool {

//使用全局变量 DB 来创建一个数据库事务 tx
tx, err := DB.Begin()
if err != nil {
fmt.Println("Begin err ", err)
return false
}

//准备sol语句
stmt, err := tx.Prepare("INSERT INTO movie_data(`Title`,`Director`,`Picture`,`Actor`,`Year`,`Score`,`Quote`)VALUES(?,?,?,?,?,?,?)")
if err != nil {
fmt.Println("Prepare err ", err)
return false
}

//stmt.Exec:通过预处理语句执行插入操作,将 MovieData 结构体中的字段逐个绑定到 SQL 语句中的占位符 ?
_, err = stmt.Exec(movieData.Title, movieData.Director, movieData.Picture, movieData.Actor, movieData.Year, movieData.Score, movieData.Quote)
if err != nil {
fmt.Println("Exec err ", err)
return false
}

//提交事务,将sql语句写入数据库
_ = tx.Commit()

return true
}

(二)抓取静态在线图书馆

在线图书馆连接
跟上一个差不多,主要自己敲一遍,熟悉一下过程,有几个小点在这里列一下

  • 小星星图标转换成string
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
scoreClass, exists := s.Find("article > p").Attr("class")
score := "Unknown"
if exists {
// scoreClass 可能是 "star-rating Three" 或类似字符串
scoreParts := strings.Split(scoreClass, " ")
if len(scoreParts) > 1 {
score = scoreParts[1] // 提取 "Three"
}
}

// 将评分转换为数字存储
rating := 0
switch score {
case "One":
rating = 1
case "Two":
rating = 2
case "Three":
rating = 3
case "Four":
rating = 4
case "Five":
rating = 5
default:
rating = 0 // 如果没有匹配到
}
  • 文字追踪到去掉空格和换行符
1
2
rawStatus := s.Find("article > div.product_price > p.instock.availability").Text()
status := strings.TrimSpace(rawStatus) // 去掉前后的空格和换行符

(三)动态网页

2024/12/19更新一下

爬的是b站评论,不同点在要先找到评论的接口api,然后把json格式转为struct存起来,再序列化出来用,代码开源在了github上
其余解析,存取没有什么不同,就没写完