爬取时间: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 解析参评人数 接下来准备爬取有多少人参与了评价,分析一下页面:
如果只爬取这个 <span>
标签下的数字的话,没有任何提示信息,别人看了不知道是啥东西,所以把 人评价
这三个字也爬下来的话就比较好了,但是可以看到数字和文字不在同一个元素标签下,而且文字部分还有空格,要爬取的话就要把 class="rating_people"
的 a
标签下所有的 text
提取出来,然后再去掉空格:
1 2 3 4 5 6 7 8 9 value = parse_movie.xpath("//a[@class='rating_people']" ) string = [value[0 ].xpath('string(.)' )] number = [a.strip() for a in string]
这样做太麻烦了,我们可以直接提取数字,得到一个列表,然后使用另一个带有提示信息的列表,将两个列表的元素合并,组成一个新列表,这个新列表的元素就是提示信息+人数1 2 3 4 5 6 value = parse_movie.xpath("//span[@property='v:votes']/text()" ) number = [" " .join(['参评人数:' ] + value)]
【2x03】正则表达式解析制片国家、语言 接下来尝试爬取制片国家/地区、语言等信息:
分析页面可以观察到,制片国家/地区和语言结构比较特殊,没有特别的 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 import requestsfrom lxml import etreeimport csvimport reimport timeimport osheaders = {'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("//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(.)' )] m_url = ['豆瓣链接:' + movie_url] 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】数据截图
【7x00】程序不足的地方 程序不足的地方:豆瓣电影有反爬机制,当程序爬取到大约 150 条数据的时候,IP 就会被封掉,第二天 IP 才会解封,可以考虑综合使用多个代理、多个 User-Agent、随机时间暂停等方法进行爬取