# Python3 爬虫实战 — 豆瓣电影TOP250

TRHX

2019-09-28

Python3 学习笔记 / 爬虫实战

字数统计:

2,462字

阅读时长≈

11分

> 爬取时间：2019-09-27
> 爬取难度：★★☆☆☆☆
> 请求链接：https://movie.douban.com/top250 以及每部电影详情页
> 爬取目标：爬取榜单上每一部电影详情页的数据，保存为 CSV 文件；下载所有电影海报到本地
> 涉及知识：请求库 requests、解析库 lxml、Xpath 语法、正则表达式、CSV 和二进制数据储存、列表操作
> 完整代码：https://github.com/TRHX/Python3-Spider-Practice/tree/master/douban-top250
> 其他爬虫实战代码合集（持续更新）：https://github.com/TRHX/Python3-Spider-Practice
> 爬虫实战专栏（持续更新）：https://itrhx.blog.csdn.net/article/category/9351278

# 【1x00】循环爬取网页模块

观察豆瓣电影 Top 250，请求地址为：https://movie.douban.com/top250

每页展示25条电影信息，照例翻页观察 url 的变化：

第一页：https://movie.douban.com/top250

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

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

一共有10页，每次改变的是 start 的值，利用一个 for 循环，从 0 到 250 每隔 25 取一个值拼接到 url，实现循环爬取每一页，由于我们的目标是进入每一部电影的详情页，然后爬取详情页的内容，所以我们可以使用 Xpath 提取每一页每部电影详情页的 URL，将其赋值给 `m_urls`，并返回 `m_urls`，`m_urls` 是一个列表，列表元素就是电影详情页的 URL

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    def index_pages(number):
        url = 'https://movie.douban.com/top250?start=%s&filter=' % number
        index_response = requests.get(url=url, headers=headers)
        tree = etree.HTML(index_response.text)
        m_urls = tree.xpath("//li/div/div/a/@href")
        return m_urls

    if __name__ == '__main__':
        for i in range(0, 250, 25):
            movie_urls = index_pages(i)

# 【2x00】解析模块

定义一个解析函数 `parse_pages()`，利用 for 循环，依次提取 `index_pages()` 函数返回的列表中的元素，也就是每部电影详情页的 URL，将其传给解析函数进行解析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    def index_pages(number):
    	expressions

    def parse_pages(url):
    	expressions

    if __name__ == '__main__':
        for i in range(0, 250, 25):
            movie_urls = index_pages(i)
            for movie_url in movie_urls:
                results = parse_pages(movie_url)

详细看一下解析函数 `parse_pages()`，首先要对接收到的详情页 URL 发送请求，获取响应内容，然后再使用 Xpath 提取相关信息

    1
    2
    3

    def parse_pages(url):
        movie_pages = requests.get(url=url, headers=headers)
        parse_movie = etree.HTML(movie_pages.text)

## 【2x01】Xpath 解析排名、电影名、评分信息

其中排名、电影名和评分信息是最容易匹配到的，直接使用 Xpath 语法就可以轻松解决：

    1
    2
    3
    4
    5
    6
    7
    8

    # 排名
    ranking = parse_movie.xpath("//span[@class='top250-no']/text()")

    # 电影名
    name = parse_movie.xpath("//h1/span[1]/text()")

    # 评分
    score = parse_movie.xpath("//div[@class='rating_self clearfix']/strong/text()")

## 【2x02】Xpath 解析参评人数

接下来准备爬取有多少人参与了评价，分析一下页面：

<fancybox>
</fancybox>

如果只爬取这个 `<span>` 标签下的数字的话，没有任何提示信息，别人看了不知道是啥东西，所以把 `人评价` 这三个字也爬下来的话就比较好了，但是可以看到数字和文字不在同一个元素标签下，而且文字部分还有空格，要爬取的话就要把 `class="rating_people"` 的 `a` 标签下所有的 `text` 提取出来，然后再去掉空格：

    1
    2
    3
    4
    5
    6
    7
    8
    9

    # 参评人数
    # 匹配a节点
    value = parse_movie.xpath("//a[@class='rating_people']")
    # 提取a节点下所有文本
    string = [value[0].xpath('string(.)')]
    # 去除多余空格
    number = [a.strip() for a in string]

    # 此时 number = ['1617307人评价']

这样做太麻烦了，我们可以直接提取数字，得到一个列表，然后使用另一个带有提示信息的列表，将两个列表的元素合并，组成一个新列表，这个新列表的元素就是提示信息+人数

    1
    2
    3
    4
    5
    6

    # 参评人数
    value = parse_movie.xpath("//span[@property='v:votes']/text()")
    # 合并元素
    number = [" ".join(['参评人数：'] + value)]

    # 此时 number = ['参评人数：1617307']

## 【2x03】正则表达式解析制片国家、语言

接下来尝试爬取制片国家/地区、语言等信息：

<fancybox>
</fancybox>

分析页面可以观察到，制片国家/地区和语言结构比较特殊，没有特别的 class 或者 id 属性，所包含的层次关系也太复杂，所以这里为了简便，直接采用正则表达式来匹配信息，就没有那么复杂了：

    1
    2
    3
    4
    5
    6
    7

    # 制片国家/地区
    value = re.findall('<span class="pl">制片国家/地区:</span>(.*?)<br/>', movie_pages.text)
    country = [" ".join(['制片国家:'] + value)]

    # 语言
    value = re.findall('<span class="pl">语言:</span>(.*?)<br/>', movie_pages.text)
    language = [" ".join(['语言:'] + value)]

# 【3x00】返回解析数据

其他剩下的信息皆可利用以上方法进行提取，所有信息提取完毕，最后使用 `zip()` 函数，将所有提取的对象作为参数，将对象中对应的元素打包成一个个元组，然后返回由这些元组组成的列表

    1

    return zip(ranking, name, score, number, types, country, language, date, time, other_name, director, screenwriter, performer, m_url, imdb_url)

# 【4x00】数据储存模块

定义一个数据保存函数 `save_results()`

    1
    2
    3
    4

    def save_results(data):
        with open('douban.csv', 'a', encoding="utf-8-sig") as fp:
            writer = csv.writer(fp)
            writer.writerow(data)

注意：编码方式要设置为 `utf-8-sig`，如果设置为 `utf-8`，则文件会乱码，不设置编码，则可能会报一下类似错误：

    1

    UnicodeEncodeError: 'gbk' codec can't encode character '\ub3c4' in position 9: illegal multibyte sequence

可以看到错误出现在 `\ub3c4` 上，将该 Unicode 编码转换为中文为 `도`，发现正是排名第 19 的电影：熔炉 도가니，因为标题有韩文，所以在储存为 CSV 文件时会报编码错误，而将编码设置为 `utf-8-sig` 就不会报错，具体原因参见：《Python 中文日文汉字乱码处理utf-8-sig》

接下来是保存电影的海报到本地：

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    # 保存电影海报
    poster = parse_movie.xpath("//div[@id='mainpic']/a/img/@src")
    response = requests.get(poster[0])
    name2 = re.sub(r'[A-Za-z\:\s]', '', name[0])
    poster_name = str(ranking[0]) + ' - ' + name2 + '.jpg'
    dir_name = 'douban_poster'
    if not os.path.exists(dir_name):
        os.mkdir(dir_name)
    poster_path = dir_name + '/' + poster_name
    with open(poster_path, "wb")as f:
        f.write(response.content)

解析电影详情页，使用 Xpath 提取海报的 URL，向该 URL 发送请求

图片以 `排名+电影名.jpg` 的方式命名，但是由于提取的电影名部分含有特殊字符，比如排名第 10 的电影：忠犬八公的故事 Hachi: A Dog’s Tale，其中有个冒号，而 Windows 文件命名是不能包含这些字符的，所以我们直接去除电影名包含的英文字符、空白字符、特殊字符，只留下中文，代码实现： `name2 = re.sub(r'[A-Za-z\:\s]', '', name[0])`

定义一个文件夹名称 `douban_poster`，利用 `os` 模块判断当前是否存在该文件夹，若不存在就创建一个

最后以二进制形式保存海报到当前目录的 douban_poster 文件夹下

# 【5x00】完整代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124

    # =============================================
    # --*-- coding: utf-8 --*--
    # @Time    : 2019-09-27
    # @Author  : TRHX
    # @Blog    : www.itrhx.com
    # @CSDN    : https://blog.csdn.net/qq_36759224
    # @FileName: douban.py
    # @Software: PyCharm
    # =============================================

    import requests
    from lxml import etree
    import csv
    import re
    import time
    import os

    headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36'}

    def index_pages(number):
        url = 'https://movie.douban.com/top250?start=%s&filter=' % number
        index_response = requests.get(url=url, headers=headers)
        tree = etree.HTML(index_response.text)
        m_urls = tree.xpath("//li/div/div/a/@href")
        return m_urls

    def parse_pages(url):
        movie_pages = requests.get(url=url, headers=headers)
        parse_movie = etree.HTML(movie_pages.text)

        # 排名
        ranking = parse_movie.xpath("//span[@class='top250-no']/text()")

        # 电影名
        name = parse_movie.xpath("//h1/span[1]/text()")

        # 评分
        score = parse_movie.xpath("//div[@class='rating_self clearfix']/strong/text()")

        # 参评人数
        value = parse_movie.xpath("//span[@property='v:votes']/text()")
        number = [" ".join(['参评人数：'] + value)]
        # value = parse_movie.xpath("//a[@class='rating_people']")
        # string = [value[0].xpath('string(.)')]
        # number = [a.strip() for a in string]
        # print(number)

        # 类型
        value = parse_movie.xpath("//span[@property='v:genre']/text()")
        types = [" ".join(['类型：'] + value)]

        # 制片国家/地区
        value = re.findall('<span class="pl">制片国家/地区:</span>(.*?)<br/>', movie_pages.text)
        country = [" ".join(['制片国家:'] + value)]

        # 语言
        value = re.findall('<span class="pl">语言:</span>(.*?)<br/>', movie_pages.text)
        language = [" ".join(['语言:'] + value)]

        # 上映时期
        value = parse_movie.xpath("//span[@property='v:initialReleaseDate']/text()")
        date = [" ".join(['上映日期：'] + value)]

        # 片长
        value = parse_movie.xpath("//span[@property='v:runtime']/text()")
        time = [" ".join(['片长：'] + value)]

        # 又名
        value = re.findall('<span class="pl">又名:</span>(.*?)<br/>', movie_pages.text)
        other_name = [" ".join(['又名:'] + value)]

        # 导演
        value = parse_movie.xpath("//div[@id='info']/span[1]/span[@class='attrs']/a/text()")
        director = [" ".join(['导演:'] + value)]

        # 编剧
        value = parse_movie.xpath("//div[@id='info']/span[2]/span[@class='attrs']/a/text()")
        screenwriter = [" ".join(['编剧:'] + value)]

        # 主演
        value = parse_movie.xpath("//div[@id='info']/span[3]")
        performer = [value[0].xpath('string(.)')]

        # URL
        m_url = ['豆瓣链接：' + movie_url]

        # IMDb链接
        value = parse_movie.xpath("//div[@id='info']/a/@href")
        imdb_url = [" ".join(['IMDb链接：'] + value)]

        # 保存电影海报
        poster = parse_movie.xpath("//div[@id='mainpic']/a/img/@src")
        response = requests.get(poster[0])
        name2 = re.sub(r'[A-Za-z\:\s]', '', name[0])
        poster_name = str(ranking[0]) + ' - ' + name2 + '.jpg'
        dir_name = 'douban_poster'
        if not os.path.exists(dir_name):
            os.mkdir(dir_name)
        poster_path = dir_name + '/' + poster_name
        with open(poster_path, "wb")as f:
            f.write(response.content)

        return zip(ranking, name, score, number, types, country, language, date, time, other_name, director, screenwriter, performer, m_url, imdb_url)

    def save_results(data):
        with open('douban.csv', 'a', encoding="utf-8-sig") as fp:
            writer = csv.writer(fp)
            writer.writerow(data)

    if __name__ == '__main__':
        num = 0
        for i in range(0, 250, 25):
            movie_urls = index_pages(i)
            for movie_url in movie_urls:
                results = parse_pages(movie_url)
                for result in results:
                    num += 1
                    save_results(result)
                    print('第' + str(num) + '条电影信息保存完毕！')
                    time.sleep(3)

# 【6x00】数据截图

<fancybox>
</fancybox> <fancybox>
</fancybox>

# 【7x00】程序不足的地方

程序不足的地方：豆瓣电影有反爬机制，当程序爬取到大约 150 条数据的时候，IP 就会被封掉，第二天 IP 才会解封，可以考虑综合使用多个代理、多个 User-Agent、随机时间暂停等方法进行爬取

您的喜欢是作者写作最大的动力！❤️

- PayPal
- AliPay
- WeChatPay
- QQPay

Donate

最后更新于 2019年10月21日

爬虫

豆瓣电影

###### 上一页

#### 使用 Hexo-Git-Backup 插件备份你的 Hexo 博客

###### Hexo 备份

###### 下一页

#### Python3 爬虫实战 — 猫眼电影TOP100

###### 爬虫 猫眼电影

#### 评论

## ๑۩ﺴ TRHX ﺴ۩๑

本文目录
站内导航

- 所有文章
- 文章归档
- RSS订阅
- 我的友链
- 留言板
- 关于博主

欢迎光临
访客地图
交流群组

QQ 交流群 Telegram 交流群

文章分类

- BLOG
  (4)
- CDN
  (1)
- Hexo
  (9)
- Java
  (1)
- Linux
  (2)
- Markdown
  (2)
- Python3 学习笔记
  (39)
- 基础学习
  (9)
- 学习经验
  (4)
- 爬虫学习
  (18)
- 爬虫实战
  (8)
- VPS
  (1)
- WEB前端
  (2)
- 图床
  (1)

热门标签
12306 58同城 Ajax BLOG Beautiful Soup CDN Coding Pages Deepin Elicpse GitHub Pages Github Github Pages HTTPS Hexo ICP备案 JDBC JS 预加载 Markdown Material X MongoDB MySQL PEP8 PicGo Pygame Python Redis SEO SQL Server 2012 SSR Selenium Ubuntu User-Agent VMware VPS XPath if语句 input()函数 instant.page jsDelivr lxml pyspider requests spfk urllib while循环 主题个性化 代理 公安备案 函数 列表 变量 哔哩哔哩 图床 图形验证码 垃圾 备份 字典 安居客 年终总结 异常 技巧 操作列表 数据类型 文件 文件储存 无界面浏览器 模块 正则表达式 滑动验证码 点触验证码 爬虫 猫眼电影 瓜子二手车 类 继承 编辑器 虎扑论坛 豆瓣电影
最近在听