写在前面
通过实例学习Go语言爬虫功能
需求分析
-
获取内容
-
书名
-
作者/出版社
-
评分
-
评分人数
-
简介
-
-
保存数据
-
将爬取到的数据存入数据库MySQL
-
使用连接数据库
-
-
页面展示
-
将爬取到的数据通过接口的形式展示
-
使用框架
-
分页
-
网站分析
目标网站:豆瓣读书Top250 URL: https://book.douban.com/top250
第一步:访问目标网站
点击第2页,url变化为:
https://book.douban.com/top250?start=25
由url可以看出,
-
每页可获取25条数据
-
请求地址中加上
start=i
可以进行分页操作 -
i
是25的倍数(其实也可以不是,但每页只有 25条数据),第n页为(i/25)+1
第二步:查看网页渲染类型
按F12进入浏览器开发者模式,观察数据是异步加载的还是直接输出,即服务器是提供数据接口还是直接输出HTML代码。
由开发者工具可以看出该页面是直接输出的HTML,因此需要对该页面发起请求获取网页内容并解析有效数据。
爬虫代码
package main
import (
"fmt"
"io"
"log"
"net/http"
"regexp"
"strconv"
"time"
"gorm.io/driver/mysql"
"github.com/PuerkitoBio/goquery"
"gorm.io/gorm"
)
type Book struct {
gorm.Model
Title string `json:"title" gorm:"column:title"`
Author string `json:"author" gorm:"column:author"`
Score float64 `json:"score" gorm:"column:score"`
CommentCount int64 `json:"comment_count" gorm:"column:comment_count"`
Quote string `json:"quote" gorm:"column:quote"`
}
func NewBook(title, author, quote string, score float64, commentCount int64) *Book {
return &Book{
Title: title,
Author: author,
Score: score,
CommentCount: commentCount,
Quote: quote,
}
}
func (m *Book) TableName() string {
return "book"
}
var (
db *gorm.DB
books []*Book
)
func init() {
dsn := "root:password@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local"
d, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
SkipDefaultTransaction: true, // 关闭gorm默认开启的全局事务
PrepareStmt: true, // 开启每次执行SQL会预处理SQL
})
if err != nil {
log.Println("连接数据库失败")
return
}
db = d
db.AutoMigrate(&Book{}) // 自动同步表
}
func ClearPlain(str string) string {
reg := regexp.MustCompile(`\s`)
return reg.ReplaceAllString(str, "")
}
func GetNumber(str string) string {
reg := regexp.MustCompile(`\d+`)
return reg.FindString(str)
}
func Run(method, url string, body io.Reader, client *http.Client) {
req, err := http.NewRequest(method, url, body)
if err != nil {
log.Println("获取请求对象失败")
return
}
req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36")
req.Header.Set("host", "book.douban.com")
resp, err := client.Do(req)
if err != nil {
log.Println("发起请求失败")
return
}
if resp.StatusCode != http.StatusOK {
log.Printf("请求失败,状态码:%d", resp.StatusCode)
return
}
defer resp.Body.Close() // 关闭响应对象中的body
query, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil {
log.Println("生成goQuery对象失败")
return
}
query.Find("ol.grid_view li").Each(func(i int, s *goquery.Selection) {
title := ClearPlain(s.Find("span.title").Text())
author := ClearPlain(GetNumber(s.Find("div.bd>p").Text()))
commentCountStr := ClearPlain(GetNumber(s.Find(".star>span").Eq(3).Text()))
scoreStr := ClearPlain(s.Find("span.rating_num").Text())
quote := ClearPlain(s.Find(".inq").Text())
commentCount, _ := strconv.ParseInt(commentCountStr, 10, 64)
score, _ := strconv.ParseFloat(scoreStr, 64) // 评分可能是小数,所以这里用的是ParseFloat方法
fmt.Println(title)
fmt.Println(author)
fmt.Println(commentCount)
fmt.Println(score)
fmt.Println(quote)
books = append(books, NewBook(title, author, quote, score, commentCount))
//time.Sleep(time.Second)
fmt.Println("-------------------------")
})
}
func main() {
// 需求:
// 1. 爬取书名、评价人数、评分、描述。
// 2. 爬取到的数据入mysql数据库
// 3. 起一个web服务,暴露接口直接获取到书籍数据【分页处理】
client := &http.Client{}
url := "https://book.douban.com/top250?start=%d"
method := "GET"
// 数据爬取操作
for i := 1; i <= 10; i++ {
Run(method, fmt.Sprintf(url, i*25), nil, client)
time.Sleep(time.Second * 2) // 主动等待下防止IP被拉黑
}
// 数据入库操作
if err := db.Create(books).Error; err != nil {
log.Println("插入数据失败", err.Error())
return
}
log.Println("插入数据成功")
}
需要注意的就是下面几个点
-
分页条件找对
-
数据库连接和GORM的基本使用
-
goquery的使用,其实就是css定位语法得掌握
Web服务代码
func main() {
// ToMySQL() // 爬取数据并入库。如果数据已经入库了,这行代码就不用执行了,会导致数据重复
// web服务相关
r := gin.Default()
r.GET("/v1/book", func(c *gin.Context) {
offsetStr := c.DefaultQuery("offset", "0") // 如果没传值,就设置默认值0
limitStr := c.DefaultQuery("limit", "10") // 如果没传值,就设置默认值10
// 类型转换
offset, _ := strconv.ParseInt(offsetStr, 10, 64)
limit, _ := strconv.ParseInt(limitStr, 10, 64)
ms := make([]*Book, 0)
if err := db.Limit(int(limit)).Offset(int(offset)).Find(&ms).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"status_code": http.StatusBadRequest,
"status_msg": "查询失败" + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status_code": http.StatusOK,
"status_msg": "查询成功",
"books": ms,
})
})
log.Fatalln(r.Run(":8080")) // 启动服务
}
这里的ToMySQL
方法就是我们在上一个爬取数据并入库的代码的main
函数改的,这里的main函数我们用于启动web服务
现在我们加上了查询参数
-
limit=7
:只要7条数据 -
offset=32
:从第33条数据开始(因为下标是从0开始的)
好的,整个案例的需求我们都做完了
总结
在这个案例中,我们学到了怎么使用对数据库进行操作;也学到了如何使用起一个web服务。接下来大家好好熟悉下这两个库的使用吧。这两个库相对来说是比较简单的,至少上手使用的成本是很低的。