爬取时间: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_urlsm_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 解析参评人数

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


01

如果只爬取这个 <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】正则表达式解析制片国家、语言

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


02

分析页面可以观察到,制片国家/地区和语言结构比较特殊,没有特别的 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】数据截图


03

04

【7x00】程序不足的地方

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

 评论


Copyright 2018-2019 TRHX   |   鄂ICP备19003281号-4  |   正在载入...   |   站点地图  |   站长统计

UV
PV
WordCount126.1k