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

762 阅读5分钟

简介

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

主要重难点是:

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

  1. 明确爬取目标 URL 地址 首先在浏览器打开 豆瓣电影 Top250 的网站地址: 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 的数值。

  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)
	}
}

下一步新建 SpiderDouBan() 函数用于爬取控制器:

func main() {
	// 固定爬取的起始页 TOP250 每页25条共10页
	start := 1
	end := 10

	channel := make(chan int)
	for i := start; i <= end; i++ {
		SpiderDouBan(i, channel)
	}
}

封装 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
}
  1. 过滤标签提取有用信息–每一部电影

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

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

<li>
            <div class="item">
                <div class="pic">
                    <em class="">1</em>
                    <a href="https://movie.douban.com/subject/1292052/">
                        ![](https://img3.doubanio.com/view/photo/s_ratio_poster/public/p480747492.webp)
                    </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>

可以看到每一部电影的标签都在

  • 标签的
    节点里面,可以先用站长工具 tool.oschina.net/regex/ 测试编写的正则表达式提取到的数据是否正确::

    class 为 pic 的 div 节点为电影的排名 ID 号和电影图片以及电影名称的信息的正则:

    regExp := `<div class="item">[\s\S]*?<div class="pic">[\s\S]*?<em class="">(.*?)<\/em>[\s\S]*?<a href=".*?">[\s\S]*?![]((.*?))`
    

    节点内包含的是电影的别名其它信息,正则表达式为:

    regExp := `<div class="item">[\s\S]*?<div class="pic">[\s\S]*?<em class="">(.*?)<\/em>[\s\S]*?<a href=".*?">[\s\S]*?![]((.*?))[\s\S]*?div class="info[\s\S]*?class="hd"[\s\S]*?class="title">(.*?)<\/span>[\s\S]*?class="other">(.*?)<\/span>`
    

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

    regExp := `<div class="item">[\s\S]*?<div class="pic">[\s\S]*?<em class="">(.*?)<\/em>[\s\S]*?<a href=".*?">[\s\S]*?![]((.*?))[\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>`
    

    的标签中包含的是电影的星级和评分数据。提取星级和评分的规则和上面分析的类型,所以最终的正则表达式为:

    regExp := `<div class="item">[\s\S]*?<div class="pic">[\s\S]*?<em class="">(.*?)<\/em>[\s\S]*?<a href=".*?">[\s\S]*?![]((.*?))[\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]*?![]((.*?))[\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 语句遍历数组获取有用信息。

    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;
    

    使用 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 的代码请点击: gitee.com/lisgroup/go… 查看

    总结实现步骤 根据输入的起始页,创建工作函数 SpiderDouBan() 循环爬取每页生成URL 封装 Request() 方法爬取每个网页数据内容,通过result返回 创建数据库保存爬取的数据 使用goroutine### 简介

    原文链接:www.sdk.cn/details/djg…

    SDK社区是一个中立的社区,这里有多样的前端知识,有丰富的api,有爱学习的人工智能开发者,有风趣幽默的开发者带你学python,还有未来火热的鸿蒙,当各种元素组合在一起,让我们一起脑洞大开共同打造专业、好玩、有价值的开发者社区,帮助开发者实现自我价值!