Go语言实现爬取豆瓣电影Top250

330 阅读5分钟

写在前面

书接上文,之前我们用Go语言实现了爬取必应壁纸,并把壁纸信息爬取到本地,接下来咱们继续,用Go语言爬取豆瓣电影Top250。好像只要说到入门爬虫,豆瓣电影Top250就是必爬的,不知道大家是怎么个情况,反正当年我学python做爬虫的时候,就是爬豆瓣电影,而且当时爬得过分了。自家IP都被豆瓣给禁了一段时间。扯远了。啊哈哈哈!!!

需求

上一个案例比较简单,这个案例咱们稍微做复杂点

  1. 爬取豆瓣电影Top250

    • 电影名
    • 电影上映时间
    • 电影评分
    • 电影评分人数
    • 电影的quote
  2. 将爬取到的数据存入数据库MySQL

    • 这里使用GORM连接数据库
    • 其实也可以不使用ORM,或者使用其他的ORM
  3. 起一个Web服务,提供一个接口,将爬取到的数据通过接口的形式展示

    • 使用Gin框架
    • 分页

网站分析

豆瓣电影Top250

第一步

第一页数据的请求地址和分页栏 image.png 第二页数据的请求地址和分页栏 image.png 第三页数据的请求地址和分页栏 image.png 从这里我们就可以推出

  1. 每次请求能拿到25条数据
  2. 请求地址中加上start=xxx可以进行分页操作
  3. xxx是25的倍数(其实也可以不是)

第二步 第二步需要观察数据是异步加载的还是直接加载。人话就是页面上现在显示的数据是通过地址栏地址得到的还是通过别的请求拿到的。如果对前端有了解的话这里应该不难理解

image.png 我们调出控制台可以发现,电影信息确实是从地址栏的地址中获取到的,接下来我就是想这个地址发起请求获取数据、解析数据、入库...

爬取数据代码

package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "regexp"
    "strconv"
    "time"

    "gorm.io/driver/mysql"
    "github.com/PuerkitoBio/goquery"
    "gorm.io/gorm"
)

type Movie struct {
    gorm.Model
    Title        string  `json:"title" gorm:"column:title"`
    PublicYear   string  `json:"public_year" gorm:"column:public_year"`
    Score        float64 `json:"score" gorm:"column:score"`
    CommentCount int64   `json:"comment_count" gorm:"column:comment_count"`
    Quote        string  `json:"quote" gorm:"column:quote"`
}

func NewMovie(title, publicYear, quote string, score float64, commentCount int64) *Movie {
    return &Movie{
        Title:        title,
        PublicYear:   publicYear,
        Score:        score,
        CommentCount: commentCount,
        Quote:        quote,
    }
}

func (m *Movie) TableName() string {
    return "movie"
}

var (
    db     *gorm.DB
    movies []*Movie
)

func init() {
    dsn := "root:123456@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(&Movie{}) // 自动同步表
}

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", "movie.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())
        year := 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(year)
        fmt.Println(commentCount)
        fmt.Println(score)
        fmt.Println(quote)
        movies = append(movies, NewMovie(title, year, quote, score, commentCount))
        //time.Sleep(time.Second)
        fmt.Println("-------------------------")
    })
}

func main() {
    // 需求:
    // 1. 爬取片名、爬取年份、爬取评价人数、爬取评分、爬取描述。其他的信息大家自己解析
    // 2. 爬取到的数据入mysql数据库
    // 3. 起一个web服务,暴露接口直接获取到电影数据【分页处理】
    client := &http.Client{}
    url := "https://movie.douban.com/top250?start=%d&filter="
    method := "GET"
    // 数据爬取操作
    for i := 1; i <= 10; i++ {
        Run(method, fmt.Sprintf(url, i*25), nil, client)
        time.Sleep(time.Second * 2) // 主动等待下吧,别被ban了
    }

    // 数据入库操作
    if err := db.Create(movies).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/movie", 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([]*Movie, 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":  "查询成功",
            "movies":      ms,
        })
    })
    log.Fatalln(r.Run(":8080")) // 启动服务
}

这里的ToMySQL方法就是我们在上一个爬取数据并入库的代码的main函数改的,这里的main函数我们用于启动web服务

image.png 这里我们啥查询参数都没加,默认是出来10条数据,没毛病

image.png 现在我们加上了查询参数

  • limit=7:只要7条数据
  • offset=32:从第33条数据开始(因为下标是从0开始的)

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

总结

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