零基础Python爬虫实战:豆瓣电影TOP250

我们曾经抓取过猫眼电影TOP100,并进行了简单的分析。但是众所周知,豆瓣的用户比较小众、比较独特,那么豆瓣的TOP250又会是哪些电影呢?

我在整理代码的时候突然发现一年多以前的爬虫代码竟然还能使用……那今天就用它来演示下,如何通过urllib+BeautifulSoup来快速抓取解析豆瓣电影TOP250。

一、观察网页地址

首先我们观察url地址,连续点击几页之后我们发现,豆瓣电影TOP250一共分10页,每页有25部电影,每页的url地址的格式为https://movie.douban.com/top250?start={0}&filter=,大括号中的部分用这一页的第一部电影的编号代替,编号从0开始一直到249。

因此我们可以通过格式化字符串来生成所有的url地址:

url_init = 'https://movie.douban.com/top250?start={0}&filter='
urls = [url_init.format(x * 25) for x in range(10)]
print(urls)

输出为:

['https://movie.douban.com/top250?start=0&filter=',
 'https://movie.douban.com/top250?start=25&filter=',
 'https://movie.douban.com/top250?start=50&filter=',
 'https://movie.douban.com/top250?start=75&filter=',
 'https://movie.douban.com/top250?start=100&filter=',
 'https://movie.douban.com/top250?start=125&filter=',
 'https://movie.douban.com/top250?start=150&filter=',
 'https://movie.douban.com/top250?start=175&filter=',
 'https://movie.douban.com/top250?start=200&filter=',
 'https://movie.douban.com/top250?start=225&filter=']

当然,这个地址未必就是我们需要请求的地址。我们先打开TOP250的第一页,右键检查,单击进入网络(Network)选项卡,刷新一下,可以看到出现了一大堆请求的返回结果,我们先打开第一个document类型的请求。

我们切换到Headers标签,可以看到,在General下,Request URL的取值与网页地址一样。也就是说,我们上边生成的10条url,正是我们需要请求的地址。

image

二、定位信息位置

接下来我们看一下电影的信息藏在哪里。我们切换到Response标签下,搜索网页中的内容,比如我们先搜一下榜首的名称:肖申克的救赎。

image

可以看到,所有的电影信息都在一个class="grid_view"<ol>(有序列表)标签下,每一部电影是一个<li>标签。

在每个<li>下:

  • 标题藏在一个class="title"<span>标签下;
  • 导演、主演、上映年份、地区、类型藏在一个<div class="bd">的子标签<p>中;
  • 得分和评分人数在<div class="star">
image

好,接下来我们就开始抓取并解析这些内容。

三、抓取并解析

首先我们定义一个函数,用来打开url并返回BeautifulSoup对象。

# -*- coding:utf-8 -*-

from urllib.request import urlopen
from bs4 import BeautifulSoup
from collections import defaultdict
import pandas as pd
import time
import re


class DoubanMovieTop():
    def __init__(self):
        self.top_urls = ['https://movie.douban.com/top250?start={0}&filter='.format(x*25) for x in range(10)]
        self.data = defaultdict(list)
        self.columns = ['title', 'link', 'score', 'score_cnt', 'top_no', 'director', 'writers', 'actors', 'types',
                        'edit_location', 'language', 'dates', 'play_location', 'length', 'rating_per', 'betters',
                        'had_seen', 'want_see', 'tags', 'short_review', 'review', 'ask', 'discussion']
        self.df = None

    def get_bsobj(self, url):
        html = urlopen(url).read().decode('utf-8')
        bsobj = BeautifulSoup(html, 'lxml')
        return bsobj

在这个函数中,我们使用urllib.requests.urlopen来发起请求,对返回的响应结果,我们使用.read()方法来读取内容。但是这里读取到的内容是字节(bytes),我们要将其转化为字符串,所以我们要再使用字节对象的.decode('utf-8')方法进行转化;然后我们使用bs4.BeautifulSoup()从字符串中生成BeautifulSoup对象。

这里我们为什么选择utf-8编码进行解码呢?这是因为这个网页的编码正是utf-8。一般情况下,我们可以从网页的<head>中找到编码信息,这样我们就可以对于不同编码的网页进行针对性的解码了。

image

接下来我们开始解析电影的信息。我们定义一个函数,以上边get_bsobj(url)函数输出的BeautifulSoup对象为输入,以数据列表为输出。

def get_info(self):
    for url in self.top_urls:
        bsobj = self.get_bsobj(url)
        main = bsobj.find('ol', {'class': 'grid_view'})

        # 标题及链接信息
        title_objs = main.findAll('div', {'class': 'hd'})
        titles = [i.find('span').text for i in title_objs]
        links = [i.find('a')['href'] for i in title_objs]

        # 评分信息
        score_objs = main.findAll('div', {'class': 'star'})
        scores = [i.find('span', {'class': 'rating_num'}).text for i in score_objs]
        score_cnts = [i.findAll('span')[-1].text for i in score_objs]

        for title, link, score, score_cnt in zip(titles, links, scores, score_cnts):
            self.data[title].extend([title, link, score, score_cnt])
            bsobj_more = self.get_bsobj(link)
            more_data = self.get_more_info(bsobj_more)
            self.data[title].extend(more_data)
            print(self.data[title])
            print(len(self.data))
            time.sleep(1)

我们在榜单列表页面直接获取了标题、电影详情页面地址、评分、评分人数信息。

接下来,可以看到我对所有的电影详情页面进行了一个循环抓取解析,这是因为在榜单页面中信息展示不全,且这里的信息不够丰富,在详情页中,我们可以获取非常丰富的数据,包括导演、编剧、演员、上映时间和地区、语言、别名、短评数、影评数、多少人想看、多少人看过……

获得了这么多信息之后,我们可以进行更加深入的分析,因此我们选择进入详情页进一步抓取更多信息。因此我们需要定义一个函数,用来解析详情页。

image

豆瓣的页面抓取难度较小,不过我们这里定义了较多的字段,因此这个函数会显得比较长。这个函数里我们使用了两个try...except...来应对异常,这是因为有些电影没有编剧或者主演,这会导致抓取异常,针对这种情况,我们直接将该字段留空即可。

每个字段抓取的表达式都是根据返回的源码得到的,BeautifulSoup的使用非常简单,几分钟就可以上手,半小时就可以入门。事实上我现在更喜欢使用XPath表达式,更灵活、更强大,对XPath的使用不了解的同学可以去看我的另一篇抓取网易云音乐的文章。在这个例子中,有些BeautifulSoup不太容易实现的部分,我们结合了re正则表达式来完成。

关于BeautifulSoup的使用,可以参考这份教程:https://docs.pythontab.com/beautifulsoup4/。至于学习程度,仍然是按照我们的二八法则,不需深究,学习最少的内容,覆盖最多的应用即可,剩下的在实战中遇到了再去检索学习即可。

def get_more_info(self, bsobj):
    # 榜单排名
    top_no = bsobj.find('span', {'class': 'top250-no'}).text.split('.')[1]

    # 更多信息
    main = bsobj.find('div', {'id': 'info'})

    # 导演
    dire_obj = main.findAll('a', {'rel': 'v:directedBy'})
    director = [i.text for i in dire_obj]

    # 编剧
    try:
        writer_obj = main.findAll('span', {'class': 'attrs'})[1]
        writers = [i.text for i in writer_obj.findAll('a')]
    except Exception as e:
        writers = []
        print(e)

    # 主演
    try:
        actor_obj = main.findAll('a', {'rel': 'v:starring'})
        actors = [i.text for i in actor_obj]
    except Exception as e:
        actors = []
        print(e)

    # 类型
    type_obj = main.findAll('span', {'property': 'v:genre'})
    types = [i.text for i in type_obj]

    # 制片地区
    pattern = re.compile('地区: (.*?)\n语言', re.S)
    edit_location = re.findall(pattern, main.text)[0]

    # 语言
    pattern2 = re.compile('语言: (.*?)\n上映日期')
    language = re.findall(pattern2, main.text)[0]

    # 上映日期/地区
    date_obj = main.findAll('span', {'property': 'v:initialReleaseDate'})
    dates = [i.text.split('(')[0][:4] for i in date_obj]
    play_location = [i.text.split('(')[1][:-1] for i in date_obj]

    # 片长
    length = main.find('span', {'property': 'v:runtime'})['content']

    # 5星到1星比例
    rating_obj = bsobj.findAll('span', {'class': 'rating_per'})
    rating_per = [i.text for i in rating_obj]

    # 好于
    better_obj = bsobj.find('div', {'class': 'rating_betterthan'})
    betters = [i.text for i in better_obj.findAll('a')]

    # 想看/看过
    watch_obj = bsobj.find('div', {'class': 'subject-others-interests-ft'})
    had_seen = watch_obj.find('a').text[:-3]
    want_see = watch_obj.findAll('a')[-1].text[:-3]

    # 标签
    tag_obj = bsobj.find('div', {'class': 'tags-body'}).findAll('a')
    tags = [i.text for i in tag_obj]

    # 短评
    short_obj = bsobj.find('div', {'id': 'comments-section'})
    short_review = short_obj.find('div').find('span', {'class': 'pl'}).find('a').text.split(' ')[1]

    # 影评
    review = bsobj.find('a', {'href': 'reviews'}).text.split(' ')[1]

    # 问题
    ask_obj = bsobj.find('div', {'id': 'askmatrix'})
    ask = ask_obj.find('h2').find('a').text.strip()[2:-1]

    # 讨论
    discuss_obj = bsobj.find('p', {'class': 'pl', 'align': 'right'}).find('a')
    discussion = discuss_obj.text.strip().split('(')[1][2:-2]

    more_data = [top_no, director, writers, actors, types, edit_location, language, dates, play_location,
                 length, rating_per, betters, had_seen, want_see, tags, short_review, review, ask, discussion]

    return more_data

成功抓取之后,我们还需要定义一个函数,用来将数据缓存到本地或其他途径(比如数据库),用于后续分析。

def dump_data(self):
    data = []
    for title, value in self.data.items():
        data.append(value)
    self.df = pd.DataFrame(data, columns=self.columns)
    self.df.to_csv('douban_top250.csv', index=False)

好了,一个针对豆瓣电影TOP250的爬虫就写完了,接下来我们执行抓取。

if __name__ == '__main__':
    douban = DoubanMovieTop()
    douban.get_info()
    douban.dump_data()

抓取完成后,我们就可以看到我们的数据了。

image

那么有人可能会问,我们抓取到数据就结束了吗?

当然没有,在下一篇文章中,我们会实战演练如何对我们得到的数据进行数据清洗和分析挖掘。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容