Go爬虫实例:爬取豆瓣图书

写在前面

通过实例学习Go语言爬虫功能

需求分析

  1. 获取内容

    • 书名

    • 作者/出版社

    • 评分

    • 评分人数

    • 简介

  2. 保存数据

    • 将爬取到的数据存入数据库MySQL

    • 使用GORM连接数据库

  3. 页面展示

    • 将爬取到的数据通过接口的形式展示

    • 使用Gin框架

    • 分页

网站分析

目标网站:豆瓣读书Top250 URL: https://book.douban.com/top250

第一步:访问目标网站

点击第2页,url变化为:

https://book.douban.com/top250?start=25

由url可以看出,

  1. 每页可获取25条数据

  2. 请求地址中加上start=i可以进行分页操作

  3. 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("插入数据成功")
}

需要注意的就是下面几个点

  1. 分页条件找对

  2. 数据库连接和GORM的基本使用

  3. 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开始的)

好的,整个案例的需求我们都做完了

总结

在这个案例中,我们学到了怎么使用GORM对数据库进行操作;也学到了如何使用Gin起一个web服务。接下来大家好好熟悉下这两个库的使用吧。这两个库相对来说是比较简单的,至少上手使用的成本是很低的。

发表回复