项目概述
- 爬取目标:爬取TOP250上每一部的电影详情页,清洗数据,保存为 CSV 文件;
- 涉及知识:requests、lxml、Xpath、re、csv、list 操作(Python 爬虫基础 里都有)
- 完整代码:GitHub - Shawshank-LIUYU/Python3-Crawler-projects
文章目录
# 1. 获取详情页连接模块
# 2. 解析详情页模块
## 2.1 获取响应内容
## 2.2 Xpath 解析 [排名, 电影名, 评分, 评价人数]
## 2.3 re 解析 [制片地区, 语言]
## 2.4 Xpath 解析 [主演]
### 2.4.1 /text() 与 //text()
### 2.4.2 Xpath 的 string(.) 用法
### 2.4.3 例外:纪录片
## 2.5 返回解析数据
# 3. 数据储存模块
## 3.1 整理成列表格式
## 3.2 列表存入 csv
# 4. 完整代码
# 5. 数据截图
# 6. 需要改进之处
# 1. 获取详情页连接模块
""" 获取榜单页单页的详情页地址
用列表的形式返回 """
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36'}
def index_page(film_rank_number):
index_url = 'https://movie.douban.com/top250?start={}&filter='.format(film_rank_number)
index_res = requests.get(url=index_url, headers=headers)
encoding = cchardet.detect(index_res.content)['encoding']
index_html = index_res.content.decode(encoding)
index_tree = etree.HTML(index_html)
details_urls = index_tree.xpath("//li/div/div/a/@href")
return details_urls
# 2. 解析详情页模块
""" 获取单个电影详情页html结构,用 Xpath 解析html / 正则表达式(re)解析,拼接成列表,
返回一个列表,参照csv表格一行的格式 """
## 2.1 获取响应内容
先对 #1模块 获取到的详情页 URL 发送请求,获取响应内容,然后再使用 Xpath 提取相关信息,
# 这里的 cchardet模块 是可以自动检测网页的 document.charset 的,可以做到全自动解析 html
movie_page_res = requests.get(url=url, headers=headers) movie_page_encoding = cchardet.detect(movie_page_res.content)['encoding'] movie_page_html = movie_page_res.content.decode(movie_page_encoding) movie_page_tree = etree.HTML(movie_page_html)
## 2.2 Xpath 解析 [排名, 电影名, 评分, 评价人数]
对于这几个参数,简单的 Xpath 即可。无非是通过节点、属性或者text()进行筛选。
# 排名
ranking = movie_page_tree.xpath('//span[@]/text()')
# 电影名
name = movie_page_tree.xpath('//h1/span[1]/text()')
# 评分
score = movie_page_tree.xpath('//strong/text()')
# 评价人数
review_number = movie_page_tree.xpath('//a/span[@property="v:votes"]/text()')
## 2.3 re 解析 [制片地区, 语言]
这两个参数,在页面结构里的位置比较尴尬,没有属性可以检索,所包含的节点层次关系也有点复杂,因此选择 正则表达式(re) 暴力匹配
# 制片国家/地区 nation = re.findall(r'制片国家/地区:(.*?)
', movie_page_html) # 语言 language = re.findall(r'语言:(.*?)
', movie_page_html)
如图,"美国" 后是
,但是在正则表达式中填入
返回值是 [],但换成
则正确.
事实上,
是 "美国" 的结尾,在 HTML 中,
标签没有结束标签,而在 XHTML 中,
标签必须被正确地关闭:
,有的时候还有
。显示出的是
,但为了这些模式都兼容,选择用正则表达式的 ? ,即匹配 (.*?)
,具体内容见 re库笔记 ,有思维导图方便记忆.
## 2.4 Xpath 解析 [主演]
这是相对最复杂的一个解析,涉及到 html 结构的子孙节点,Xpath 的 string(.) 用法,以及特殊情况的排除.
### 2.4.1 /text() 与 //text()
/ 是子节点,//是子孙节点,子节点的文本没有东西,有点东西的是孙子,结果如图:
输出一下是这样:
因此处理需要先 del performer_list[0] 两次,再 "".join(performer_list),最后的结果就是 「张国荣/张丰毅/…」 ,即我们所需的效果,代码如下:
performer_list = movie_page_tree.xpath("//div[@id='info']/span[3]//text()")
del performer_list[0]
del performer_list[0]
performer = ["".join(performer_list)]
### 2.4.2 Xpath 的 string(.) 用法
先看 span[3] 的 RESULTS,这是整个节点包含的内容,但是程序中 Xpath 的返回类型是个 [element],输出是 [
不过这时候,爬出来的结果有 "主演: " 字样,需要给他搞掉,这里想了两种方法:第一种是 actor_list.lstrip(),一个是用简单的正则表达式 re.sub(r'主演: ', "",actor_list) ,我用的是前者:
value = parse_movie.xpath("//div[@id='info']/span[3]")
performer = value[0].xpath('string(.)') # [0]指list的第一个元素是element
performer = [performer.lstrip('主演:')]
### 2.4.3 例外:纪录片
问题在于,有的纪录片是没有主演的,如我很喜欢的郭柯导演的电影《二十二》,他就没有主演这个选项,用 span[3] 返回的是 "类型: 纪录片",因此为之单设一种情况,即纪录片,选择返回string "类型: 纪录片" 即可.
performer_list = movie_page_tree.xpath("//div[@id='info']/span[3]//text()")
if len(performer_list) >= 3:
del performer_list[0]
del performer_list[0]
performer = ["".join(performer_list)] # 纪录片是例外
else:
performer = performer_list
## 2.5 返回解析数据
列表之间的连接,只要 + 就可以.
return ranking + name + score + review_number + types + nation + language + time_span + director + scripter + performer + imdb_url
# 3. 数据储存模块
## 3.1 整理成列表格式
设置表头,每条电影的信息 append 入表格即可,最后调用列表存入函数,见 ## 3.2
csv_headers = ['排名','电影名','评分','评价人数','电影类型','国家','语言','时长','导演','编剧','主演','IMDb链接']
csv_rows = [csv_headers]
try:
for i in range(0, 10): # 应该是10,这里用作测试,250需加随机时间
movie_urls = index_page(i * 25)
for movie_url in movie_urls:
res = parse_movie_page(movie_url) # return list
csv_rows.append(res)
print('第 {} 条电影信息存入内存!'.format(len(csv_rows) - 1))
time.sleep(random.random() * 3)
except:
traceback.print_exc()
## 3.2 列表存入 csv
思路是用列表进行存储,即 cursor.writerows(rows) 函数,因此只需要把数据整合成列表的列表即可,即一条电影的信息为一个列表,250个列表组成一个新的列表即可.
def save_rows_to_csv(rows):
with open('douban.csv', 'a', encoding="utf-8-sig", newline='') as file:
f_csv = csv.writer(file)
f_csv.writerows(rows)
# 读写方式选择 a 模式(添加),这样不用总是删除,也可以对比之前的错误情况是否有改进,encoding 选择 "utf-8-sig" ,可以规避乱码(在对csv文件写入有日韩文字的时候,需要用 "utf-8-sig",对txt文件写入中日韩文字的时候,"utf-8-sig" 和 "utf-8" 均可,具体参照 utf-8-sig 编码格式)
# newline = '' 可以解决输入csv表有空行的情况,如图
其实还有 字典写入csv 的方法,即游标为 DictWriter(file,headers),在解析详情页模块先设立个headers,然后再列表转dict —— return(dict(zip(headers,list))),最后写入就先 writeheader() ,再writerows(rows).
# 4. 完整代码
# -*- coding = utf-8 -*-
# @Time : 2021/10/14 10:28
# @Author : LIUYU
# @File : douban250_crawler.py
import random
import time
import requests
from lxml import etree
import cchardet
import re
import csv
import traceback
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36'}
def index_page(film_rank_number):
index_url = 'https://movie.douban.com/top250?start={}&filter='.format(film_rank_number)
index_res = requests.get(url=index_url, headers=headers)
encoding = cchardet.detect(index_res.content)['encoding']
index_html = index_res.content.decode(encoding)
index_tree = etree.HTML(index_html)
details_urls = index_tree.xpath("//li/div/div/a/@href")
return details_urls
def parse_movie_page(url):
"""
xpath, [].append()
return []
"""
# 1. get tree
movie_page_res = requests.get(url=url, headers=headers)
movie_page_encoding = cchardet.detect(movie_page_res.content)['encoding']
movie_page_html = movie_page_res.content.decode(movie_page_encoding)
movie_page_tree = etree.HTML(movie_page_html)
# 2. Xpath
# 排名
ranking = movie_page_tree.xpath('//span[@]/text()')
# 电影名
name = movie_page_tree.xpath('//h1/span[1]/text()')
# 评分
score = movie_page_tree.xpath('//strong/text()')
# 评价人数
review_number = movie_page_tree.xpath('//a/span[@property="v:votes"]/text()')
# 类型
list_type = movie_page_tree.xpath('//span[@property="v:genre"]/text()')
types = ["/".join(list_type)]
# 制片国家/地区
nation = re.findall(r'制片国家/地区:(.*?)
', movie_page_html)
# 语言
language = re.findall(r'语言:(.*?)
', movie_page_html)
# 时长
time_span = movie_page_tree.xpath('//span[@property="v:runtime"]/text()')
# 导演
director_list = movie_page_tree.xpath("//div[@id='info']/span[1]/span[@class='attrs']/a/text()")
director = ["".join(director_list)]
# 编剧
scripter_list = movie_page_tree.xpath("//div[@id='info']/span[2]/span[@class='attrs']/a/text()")
scripter = ["".join(scripter_list)]
# 主演
performer_list = movie_page_tree.xpath("//div[@id='info']/span[3]//text()") # 主演; /text()是子节点, //text()是子孙节点,可用
if len(performer_list) >= 3:
del performer_list[0]
del performer_list[0]
performer = ["".join(performer_list)] # 纪录片是例外
else:
performer = performer_list
# IMDb
imdb_url = re.findall('IMDb:(.*?)
', movie_page_html)
# return list
return ranking + name + score + review_number + types + nation + language + time_span + director + scripter + performer + imdb_url
def save_rows_to_csv(rows):
with open('douban.csv', 'a', encoding="utf-8-sig", newline='') as file:
f_csv = csv.writer(file)
f_csv.writerows(rows)
if __name__ == '__main__':
csv_headers = ['排名','电影名','评分','评价人数','电影类型','国家','语言','时长','导演','编剧','主演','IMDb链接']
csv_rows = [csv_headers]
try:
for i in range(0,10):
movie_urls = index_page(i*25)
for movie_url in movie_urls:
res = parse_movie_page(movie_url) # return list
csv_rows.append(res)
print('第 {} 条电影信息存入内存!'.format(len(csv_rows)-1))
time.sleep(random.random()*3)
except:
traceback.print_exc()
save_rows_to_csv(csv_rows)
# 5. 数据截图
', movie_page_html) # 语言 language = re.findall(r'语言:(.*?)
', movie_page_html) # 时长 time_span = movie_page_tree.xpath('//span[@property="v:runtime"]/text()') # 导演 director_list = movie_page_tree.xpath("//div[@id='info']/span[1]/span[@class='attrs']/a/text()") director = ["".join(director_list)] # 编剧 scripter_list = movie_page_tree.xpath("//div[@id='info']/span[2]/span[@class='attrs']/a/text()") scripter = ["".join(scripter_list)] # 主演 performer_list = movie_page_tree.xpath("//div[@id='info']/span[3]//text()") # 主演; /text()是子节点, //text()是子孙节点,可用 if len(performer_list) >= 3: del performer_list[0] del performer_list[0] performer = ["".join(performer_list)] # 纪录片是例外 else: performer = performer_list # IMDb imdb_url = re.findall('IMDb:(.*?)
', movie_page_html) # return list return ranking + name + score + review_number + types + nation + language + time_span + director + scripter + performer + imdb_url def save_rows_to_csv(rows): with open('douban.csv', 'a', encoding="utf-8-sig", newline='') as file: f_csv = csv.writer(file) f_csv.writerows(rows) if __name__ == '__main__': csv_headers = ['排名','电影名','评分','评价人数','电影类型','国家','语言','时长','导演','编剧','主演','IMDb链接'] csv_rows = [csv_headers] try: for i in range(0,10): movie_urls = index_page(i*25) for movie_url in movie_urls: res = parse_movie_page(movie_url) # return list csv_rows.append(res) print('第 {} 条电影信息存入内存!'.format(len(csv_rows)-1)) time.sleep(random.random()*3) except: traceback.print_exc() save_rows_to_csv(csv_rows)
# 6. 需要改进之处
豆瓣电影有反爬机制,这次我是用 time.sleep(random.random()*3),即随机时间暂停的方法,无疑是很花费时间的,总共用的时间差不多是 0.5*3*250 = 375s —— 一共访问了260个网页,意识到了爬虫的局限性。之后可以考虑使用 多个代理IP、多个 User-Agent 去爬取网页.



