登录
原创

Go爬取豆瓣电影Top250数据列表

发布于 2020-10-09 阅读 1319
  • Go
原创

简介

本文记录如何爬取豆瓣电影Top250的数据列表,并存储数据到MySQL数据库中的实践教程。

主要重难点是:

  1. Go实现Http并发请求数据
  2. 设计正则表达式匹配数据
  3. 存储爬取的数据入库MySQL

1. 明确爬取目标 URL 地址

首先在浏览器打开 豆瓣电影 Top250 的网站地址: https://movie.douban.com/top250
打开后我们可以看到每一页显示 25 部电影,点击最下面的第二页按钮后分页参数变为 ?start=25&filter= ,第三页分页参数 ?start=50&filter= 。。。

可以看出 250 部电影分为 10 页显示,要抓取全部电影,我们只需要循环生成这 10 页 URL 链接即可。下面展示的是每一页的链接地址:

https://movie.douban.com/top250?start=0&filter=   #首页,相当于 https://movie.douban.com/top250

https://movie.douban.com/top250?start=25&filter=  #第2页

https://movie.douban.com/top250?start=50&filter=  #第3页

...

https://movie.douban.com/top250?start=225&filter=  #第10页

通过分析链接地址,我们得出规律:start=0 显示 1 到 25 部电影数据,start=25 显示 26 到 50 部电影,同理在抓取下一页的数据时,只需要改变 start 的数值。

2. 发送请求获取响应数据

  1. 新建一个 main.go 文件,并新建 main() 主函数控制循环次数,用于生成 URL 的输入参数:
func main() {
	// 固定爬取的起始页 TOP250 每页25条共10页
	start := 1
	end := 10

	channel := make(chan int)
	for i := start; i <= end; i++ {
		SpiderDouBan(i, channel)
	}
}
  1. 下一步新建 SpiderDouBan() 函数用于爬取控制器:
func SpiderDouBan(index int) {
	body := map[string]string{}
    // strconv.Itoa() 将数字转字符串
	urls := "https://movie.douban.com/top250?start=" + strconv.Itoa((index-1)*25) + "&filter="
	headers := map[string]string{"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36"}
	// Request() 函数是封装的简单请求方法,见第 3 步
    result, err := Request(urls, "GET", body, headers, 4)
	//fmt.Println(result, err)
	if err != nil {
		fmt.Println("HttpGet err: ", err)
	}
}
  1. 封装 HTTP 请求方法 Request()

对于很多网站都有反爬虫的措施,如果没有 headers 头信息的请求一律认为是爬虫请求,就会禁止请求。所以我们每次爬取网页时,都会加上一些 headers 的头信息。

但是对于访问过于频繁的请求,客户端的 IP 就会被服务端禁止访问,因此设置代理 proxies 也可以将请求伪装成来自不同 IP 的访问,前提是保证代理的 IP 地址是有效的。

// 简单封装一个Http请求方法
// rawUrl 请求的接口地址
// method 请求的方法,GET/POST
// bodyMap 请求的 body 内容
// header 请求的头信息
// timeout 超时时间
func Request(rawUrl, method string, bodyMaps, headers map[string]string, timeout time.Duration) (result string, err error) {
	if timeout <= 0 {
		timeout = 5
	}
	client := &http.Client{
		Timeout: timeout * time.Second,
	}
	// 请求的 body 内容
	data := url.Values{}
	for key, value := range bodyMaps {
		data.Set(key, value)
	}
	// 提交请求
	request, err1 := http.NewRequest(method, rawUrl, strings.NewReader(data.Encode())) // URL-encoded payload
	if err1 != nil {
		err = err1
		return
	}
	// 增加header头信息
	for key, val := range headers {
		request.Header.Set(key, val)
	}
	// 处理返回结果
	response, _ := client.Do(request)
	defer response.Body.Close()

	if response.StatusCode != http.StatusOK {
		return "", fmt.Errorf("get content failed status code is %d ", response.StatusCode)
	}
	res, err2 := ioutil.ReadAll(response.Body)
	if err2 != nil {
		err = err2
		return
	}
	return string(res), nil
}

3. 过滤标签提取有用信息–每一部电影

通过上面的封装函数,我们已经可以获取豆瓣电影 Top250 的全部网页数据。为了测试我们通过查看网页源代码的方法分析网站数据:每一部电影在 HTML 网页中的标签都是固定的,我们只需要找出固定格式的规律,编写 正则表达式,提取出电影名称、主演、图片、评分等信息。

其中一部电影的网页源码:

<li>
            <div class="item">
                <div class="pic">
                    <em class="">1</em>
                    <a href="https://movie.douban.com/subject/1292052/">
                        <img width="100" alt="肖申克的救赎" src="https://img3.doubanio.com/view/photo/s_ratio_poster/public/p480747492.webp" class="">
                    </a>
                </div>
                <div class="info">
                    <div class="hd">
                        <a href="https://movie.douban.com/subject/1292052/" class="">
                            <span class="title">肖申克的救赎</span>
                                    <span class="title">&nbsp;/&nbsp;The Shawshank Redemption</span>
                                <span class="other">&nbsp;/&nbsp;月黑高飞(港)  /  刺激1995(台)</span>
                        </a>


                            <span class="playable">[可播放]</span>
                    </div>
                    <div class="bd">
                        <p class="">
                            导演: 弗兰克·德拉邦特 Frank Darabont&nbsp;&nbsp;&nbsp;主演: 蒂姆·罗宾斯 Tim Robbins /...<br>
                            1994&nbsp;/&nbsp;美国&nbsp;/&nbsp;犯罪 剧情
                        </p>

                        
                        <div class="star">
                                <span class="rating5-t"></span>
                                <span class="rating_num" property="v:average">9.7</span>
                                <span property="v:best" content="10.0"></span>
                                <span>2153218人评价</span>
                        </div>

                            <p class="quote">
                                <span class="inq">希望让人自由。</span>
                            </p>
                    </div>
                </div>
            </div>
        </li>

可以看到每一部电影的标签都在 <li> 标签的 <div class="item"> 节点里面,可以先用站长工具 https://tool.oschina.net/regex/ 测试编写的正则表达式提取到的数据是否正确::

  1. class 为 pic 的 div 节点为电影的排名 ID 号和电影图片以及电影名称的信息的正则:
regExp := `<div class="item">[\s\S]*?<div class="pic">[\s\S]*?<em class="">(.*?)<\/em>[\s\S]*?<a href=".*?">[\s\S]*?<img width=".*?" alt="(.*?)" src="(.*?)" class=".*?">`
  1. 节点内包含的是电影的别名其它信息,正则表达式为:
regExp := `<div class="item">[\s\S]*?<div class="pic">[\s\S]*?<em class="">(.*?)<\/em>[\s\S]*?<a href=".*?">[\s\S]*?<img width=".*?" alt="(.*?)" src="(.*?)" class=".*?">[\s\S]*?div class="info[\s\S]*?class="hd"[\s\S]*?class="title">(.*?)<\/span>[\s\S]*?class="other">(.*?)<\/span>`
  1. 的标签内包含了电影的导演和主演信息,其中

    标签内是电影的导演和演员信息,其中使用
    换行,所以可以提取出导演和演员的数据,正则表达式改写为:

regExp := `<div class="item">[\s\S]*?<div class="pic">[\s\S]*?<em class="">(.*?)<\/em>[\s\S]*?<a href=".*?">[\s\S]*?<img width=".*?" alt="(.*?)" src="(.*?)" class=".*?">[\s\S]*?div class="info[\s\S]*?class="hd"[\s\S]*?class="title">(.*?)<\/span>[\s\S]*?class="other">(.*?)<\/span>[\s\S]*?<div class="bd">[\s\S]*?<p class=".*?">([\s\S]*?)<br>([\s\S]*?)<\/p>`
  1. 的标签中包含的是电影的星级和评分数据。提取星级和评分的规则和上面分析的类型,所以最终的正则表达式为:
regExp := `<div class="item">[\s\S]*?<div class="pic">[\s\S]*?<em class="">(.*?)<\/em>[\s\S]*?<a href=".*?">[\s\S]*?<img width=".*?" alt="(.*?)" src="(.*?)" class=".*?">[\s\S]*?div class="info[\s\S]*?class="hd"[\s\S]*?class="title">(.*?)<\/span>[\s\S]*?class="other">(.*?)<\/span>[\s\S]*?<div class="bd">[\s\S]*?<p class=".*?">([\s\S]*?)<br>([\s\S]*?)<\/p>[\s\S]*?span class="rating_num".*?average">(.*?)<\/span>`

完整的提取每页中所有电影数据的代码如下:

    // 使用正则匹配
	regExp := `<div class="item">[\s\S]*?<div class="pic">[\s\S]*?<em class="">(.*?)<\/em>[\s\S]*?<a href=".*?">[\s\S]*?<img width=".*?" alt="(.*?)" src="(.*?)" class=".*?">[\s\S]*?div class="info[\s\S]*?class="hd"[\s\S]*?class="title">(.*?)<\/span>[\s\S]*?class="other">(.*?)<\/span>[\s\S]*?<div class="bd">[\s\S]*?<p class=".*?">([\s\S]*?)<br>([\s\S]*?)<\/p>[\s\S]*?span class="rating_num".*?average">(.*?)<\/span>`

	// 使用正则匹配
	find := regexp.MustCompile(regExp)
	// 其中 result 为上一步获取的网页HTML数据
	content := find.FindAllStringSubmatch(result, -1) // 返回匹配的详细二维切片

其中 content 数据格式为 [][]string 二维切片,所以我们可以使用 for 语句遍历数组获取有用信息。

4. 使用分析得到有效数据

  1. 创建MySQL的一张数据表存储保存的数据
CREATE TABLE `top250` (
  `id` int(20) NOT NULL AUTO_INCREMENT,
  `title` varchar(20) DEFAULT '',
  `image` varchar(100) DEFAULT '',
  `subtitle` varchar(255) DEFAULT '',
  `other` varchar(255) DEFAULT NULL,
  `personnel` varchar(255) DEFAULT '',
  `info` varchar(255) DEFAULT '',
  `score` varchar(10) DEFAULT '',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
  1. 使用 database/sql 包并使用 MySQL 驱动:MySQL drivers

其中数据库教程点击: Go SQL 数据库教程 查看

func insert(movies [][]string) {
	// 存放 (?, ?, ...) 的slice
	valueStrings := make([]string, 0, len(movies))
	// 存放values的slice
	valueArgs := make([]interface{}, 0, len(movies)*8)
	// 遍历切片准备数据
	for _, val := range movies {
		// 占位符
		valueStrings = append(valueStrings, "(?, ?, ?, ?, ?, ?, ?, ?)")
		for i := 1; i < len(val); i++ {
			valueArgs = append(valueArgs, val[i])
		}
	}

	db, err := sql.Open("mysql",
		"root:root@tcp(127.0.0.1:3306)/test")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// 自行拼接要执行的具体语句
	sqlName := fmt.Sprintf("INSERT INTO `top250` (id,title,image,subtitle,other,personnel,info,score) VALUES %s",
		strings.Join(valueStrings, ","))
	fmt.Println(sqlName)
	// insert
	stmt, err := db.Prepare(sqlName)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(valueArgs)
	res, err := stmt.Exec(valueArgs...)
	if err != nil {
		log.Fatal(err)
	}
	lastId, err := res.LastInsertId()
	if err != nil {
		log.Fatal(err)
	}
	rowCnt, err := res.RowsAffected()
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("ID = %d, affected = %d\n", lastId, rowCnt)
}

5.启动 goroutine

我们只需要在 main()函数中创建 chan 通道就可以并发调用 SpiderDouBan() 函数:

    channel := make(chan int)
    for i := start; i <= end; i++ {
	go SpiderDouBan(i, channel)
    }
    for i := start; i <= end; i++ {
        fmt.Println("第" + strconv.Itoa(<-channel) + "页任务完成")
    }

下面在 SpiderDouBan() 函数执行的最后面,向 chan 通道中发送数据:

func SpiderDouBan(index int, ch chan int) {
    // ... 省略代码
    // ...
    // chan 记录
    ch <- index
}

其中完整 Go爬取豆瓣电影Top250 的代码请点击: https://gitee.com/lisgroup/go-learn/blob/master/spider/douban.go 查看

总结实现步骤

  1. 根据输入的起始页,创建工作函数 SpiderDouBan()
  2. 循环爬取每页生成URL
  3. 封装 Request() 方法爬取每个网页数据内容,通过result返回
  4. 创建数据库保存爬取的数据
  5. 使用goroutine

评论区

励志做一条安静的咸鱼,从此走上人生巅峰。

2

2

1

举报