爬虫实战二:爬取电影天堂的最新电影

图片来自 unsplash

前面两篇文章介绍 requests 和 xpath 的用法。我们推崇学以致用,所以本文讲解利用这两个工具进行实战。

1 爬取目标

本次爬取的站点选择电影天堂,网址是: www.dytt8.net。爬取内容是整个站点的所有电影信息,包括电影名称,导演、主演、下载地址等。具体抓取信息如下图所示:

点击查看大图

2 设计爬虫程序

2.1 确定爬取入口

电影天堂里面的电影数目成千上万,电影类型也是让人眼花缭乱。我们为了保证爬取的电影信息不重复, 所以要确定一个爬取方向。目前这情况真让人无从下手。但是,我们点击主页中的【最新电影】选项,跳进一个新的页面。蓦然有种柳暗花明又一村的感觉。


点击查看大图

由图可知道,电影天堂有 5 个电影栏目,分别为最新电影、日韩电影、欧美电影、国内电影、综合电影。每个栏目又有一定数量的分页,每个分页有 25 条电影信息。那么程序的入口可以有 5 个 url 地址。这 5 个地址分别对应每个栏目的首页链接。

2.2 爬取思路

知道爬取入口,后面的工作就容易多了。我通过测试发现这几个栏目除了页面的 url 地址不一样之外,其他例如提取信息的 xpath 路径是一样的。因此,我把 5 个栏目当做 1 个类,再该类进行遍历爬取。

我这里“最新电影”为例说明爬取思路。
1)请求栏目的首页来获取到分页的总数,以及推测出每个分页的 url 地址;
2)将获取到的分页 url 存放到名为 floorQueue 队列中;
3)从 floorQueue 中依次取出分页 url,然后利用多线程发起请求;
4)将获取到的电影页面 url 存入到名为 middleQueue 的队列;
5)从 middleQueue 中依次取出电影页面 url,再利用多线程发起请求;
6)将请求结果使用 Xpath 解析并提取所需的电影信息;
7)将爬取到的电影信息存到名为 contentQueue 队列中;
8)从 contentQueue 队列中依次取出电影信息,然后存到数据库中。

2.3 设计爬虫架构

根据爬取思路,我设计出爬虫架构。如下图所示:


点击查看大图

2.4 代码实现

主要阐述几个重要的类的代码

  • main 类

主要工作两个:第一,实例化出一个dytt8Moive对象,然后开始爬取信息。第二,等爬取结束,将数据插入到数据库中。

处理爬虫的逻辑代码如下:

# 截止到2017-08-08, 最新电影一共才有 164 个页面
LASTEST_MOIVE_TOTAL_SUM = 6 #164

# 请求网络线程总数, 线程不要调太好, 不然会返回很多 400
THREAD_SUM = 5

def startSpider():
    # 实例化对象

    # 获取【最新电影】有多少个页面
    LASTEST_MOIVE_TOTAL_SUM = dytt_Lastest.getMaxsize()
    print('【最新电影】一共  ' + str(LASTEST_MOIVE_TOTAL_SUM) + '  有个页面')
    dyttlastest = dytt_Lastest(LASTEST_MOIVE_TOTAL_SUM)
    floorlist = dyttlastest.getPageUrlList()

    floorQueue = TaskQueue.getFloorQueue()
    for item in floorlist:
        floorQueue.put(item, 3)

    # print(floorQueue.qsize())

    for i in range(THREAD_SUM):
        workthread = FloorWorkThread(floorQueue, i)
        workthread.start()

    while True:
        if TaskQueue.isFloorQueueEmpty():
            break
        else:
            pass

    for i in range(THREAD_SUM):
        workthread = TopWorkThread(TaskQueue.getMiddleQueue(), i)
        workthread.start()

    while True:
        if TaskQueue.isMiddleQueueEmpty():
            break
        else:
            pass

    insertData()
    
if __name__ == '__main__':
    startSpider()

创建数据库以及表,接着再把电影信息插入到数据库的代码如下:

def insertData():
    DBName = 'dytt.db'
    db = sqlite3.connect('./' + DBName, 10)
    conn = db.cursor()

    SelectSql = 'Select * from sqlite_master where type = "table" and name="lastest_moive";'
    CreateTableSql = '''
        Create Table lastest_moive (
            'm_id' INTEGER PRIMARY KEY,
            'm_type' varchar(100),
            'm_trans_name' varchar(200),
            'm_name' varchar(100),
            'm_decade' varchar(30),
            'm_conutry' varchar(30),
            'm_level' varchar(100),
            'm_language' varchar(30),
            'm_subtitles' varchar(100),
            'm_publish' varchar(30),
            'm_IMDB_socre' varchar(50),
            'm_douban_score' varchar(50),
            'm_format' varchar(20),
            'm_resolution' varchar(20),
            'm_size' varchar(10),
            'm_duration' varchar(10),
            'm_director' varchar(50),
            'm_actors' varchar(1000),
            'm_placard' varchar(200),
            'm_screenshot' varchar(200),
            'm_ftpurl' varchar(200),
            'm_dytt8_url' varchar(200)
        );
    '''

    InsertSql = '''
        Insert into lastest_moive(m_type, m_trans_name, m_name, m_decade, m_conutry, m_level, m_language, m_subtitles, m_publish, m_IMDB_socre, 
        m_douban_score, m_format, m_resolution, m_size, m_duration, m_director, m_actors, m_placard, m_screenshot, m_ftpurl,
        m_dytt8_url)
        values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
    '''

    if not conn.execute(SelectSql).fetchone():
        conn.execute(CreateTableSql)
        db.commit()
        print('====  创建表成功  ====')
    else:
        print('====  创建表失败, 表已经存在  ====')

    count = 1

    while not TaskQueue.isContentQueueEmpty():
        item = TaskQueue.getContentQueue().get()
        conn.execute(InsertSql, Utils.dirToList(item))
        db.commit()
        print('插入第 ' + str(count) + ' 条数据成功')
        count = count + 1

    db.commit()
    db.close()
  • TaskQueue 类

维护 floorQueue、middleQueue、contentQueue 三个队列的管理类。之所以选择队列的数据结构,是因为爬虫程序需要用到多线程,队列能够保证线程安全。

  • dytt8Moive 类

dytt8Moive 类是本程序的主心骨。程序最初的爬取目标是 5 个电影栏目,但是目前只现实了爬取最新栏目。如果你想爬取全部栏目电影,只需对 dytt8Moive 稍微改造下即可。

class dytt_Lastest(object):

    # 获取爬虫程序抓取入口
    breakoutUrl = 'http://www.dytt8.net/html/gndy/dyzz/index.html'

    def __init__(self, sum):
        self.sum = sum


    # 获取【最新电影】有多少个页面
    # 截止到2017-08-08, 最新电影一共才有 164 个页面
    @classmethod
    def getMaxsize(cls):
        response = requests.get(cls.breakoutUrl, headers=RequestModel.getHeaders(), proxies=RequestModel.getProxies(), timeout=3)
        # 需将电影天堂的页面的编码改为 GBK, 不然会出现乱码的情况
        response.encoding = 'GBK'

        selector = etree.HTML(response.text)
        # 提取信息
        optionList = selector.xpath("//select[@name='sldd']/text()")
        return len(optionList) - 1   # 因首页重复, 所以要减1


    def getPageUrlList(self):
        '''
        主要功能:目录页url取出,比如:http://www.dytt8.net/html/gndy/dyzz/list_23_'+ str(i) + '.html
        '''
        templist = []
        request_url_prefix = 'http://www.dytt8.net/html/gndy/dyzz/'
        templist = [request_url_prefix + 'index.html']

        for i in range(2, self.sum + 1):
            templist.append(request_url_prefix + 'list_23_' + str(i) + '.html')

        for t in templist:
            print('request url is ###   ' + t + '    ###')
        return templist


    @classmethod
    def getMoivePageUrlList(cls, html):
        '''
        获取电影信息的网页链接
        '''
        selector = etree.HTML(html)
        templist = selector.xpath("//div[@class='co_content8']/ul/td/table/tr/td/b/a/@href")
        # print(len(templist))
        # print(templist)
        return templist

    @classmethod
    def getMoiveInforms(cls, url, html):
        '''
        解析电影信息页面的内容, 具体如下:
        类型        : 疾速特攻/疾速追杀2][BD-mkv.720p.中英双字][2017年高分惊悚动作]
        ◎译名      : ◎译\u3000\u3000名\u3000疾速特攻/杀神John Wick 2(港)/捍卫任务2(台)/疾速追杀2/极速追杀:第二章/约翰·威克2
        ◎片名      : ◎片\u3000\u3000名\u3000John Wick: Chapter Two
        ◎年代     : ◎年\u3000\u3000代\u30002017
        ◎国家     : ◎产\u3000\u3000地\u3000美国
        ◎类别     : ◎类\u3000\u3000别\u3000动作/犯罪/惊悚
        ◎语言     : ◎语\u3000\u3000言\u3000英语
        ◎字幕     : ◎字\u3000\u3000幕\u3000中英双字幕
        ◎上映日期  :◎上映日期\u30002017-02-10(美国)
        ◎IMDb评分  : ◎IMDb评分\xa0 8.1/10 from 86,240 users
        ◎豆瓣评分  : ◎豆瓣评分\u30007.7/10 from 2,915 users
        ◎文件格式   : ◎文件格式\u3000x264 + aac
        ◎视频尺寸  : ◎视频尺寸\u30001280 x 720
        ◎文件大小  : ◎文件大小\u30001CD
        ◎片长     : ◎片\u3000\u3000长\u3000122分钟
        ◎导演     : ◎导\u3000\u3000演\u3000查德·史塔赫斯基 Chad Stahelski
        ◎主演     :
        ◎简介      : 暂不要该字段
        ◎获奖情况   : 暂不要该字段
        ◎海报
        影片截图
        下载地址
        '''
        # print(html)
        contentDir = {
            'type': '',
            'trans_name': '',
            'name': '',
            'decade': '',
            'conutry': '',
            'level': '',
            'language': '',
            'subtitles': '',
            'publish': '',
            'IMDB_socre': '',
            'douban_score': '',
            'format': '',
            'resolution': '',
            'size': '',
            'duration': '',
            'director': '',
            'actors': '',
            'placard': '',
            'screenshot': '',
            'ftpurl': '',
            'dytt8_url': ''
        }

        selector = etree.HTML(html)
        content = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/td/p/text()")
        # 匹配出来有两张图片, 第一张是海报, 第二张是电影画面截图
        imgs = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/td/p/img/@src")
        # print(content)

        # 为了兼容 2012 年前的页面
        if not len(content):
            content = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/div/td/span/text()")

        # 有些页面特殊, 需要用以下表达式来重新获取信息
        # 电影天堂页面好混乱啊~
        if not len(content):
            content = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/td/div/text()")

        if not len(content):
            content = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/div/td/p/font/text()")
            if len(content) < 5:
                content = selector.xpath("//div[@class='co_content8']/ul/tr/td/p/font/text()")

        if not len(content):
            content = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/div/td/p/span/text()")

        if not len(content):
            content = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/div/td/div/span/text()")

        if not len(content):
            content = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/div/td/font/text()")

        if not len(content):
            content = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/div/td/p/text()")

        # print(content)

        # 不同渲染页面要采取不同的抓取方式抓取图片
        if not len(imgs):
            imgs = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/div/td/img/@src")

        if not len(imgs):
            imgs = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/div/td/p/img/@src")

        if not len(imgs):
            imgs = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/div/td/div/img/@src")

        if not len(imgs):
            imgs = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/td/div/img/@src")

        # 类型
        if content[0][0:1] != '◎':
            contentDir['type'] = '[' + content[0]
        actor = ''

        for each in content:
            if each[0:5] == '◎译\u3000\u3000名':
                # 译名 ◎译\u3000\u3000名\u3000  一共占居6位
                contentDir['trans_name'] = each[6: len(each)]
            elif each[0:5] == '◎片\u3000\u3000名':
                # 片名
                contentDir['name'] = each[6: len(each)]
            elif each[0:5] == '◎年\u3000\u3000代':
                # 年份
                contentDir['decade'] = each[6: len(each)]
            elif each[0:5] == '◎产\u3000\u3000地':
                # 产地
                contentDir['conutry'] = each[6: len(each)]
            elif each[0:5] == '◎类\u3000\u3000别':
                # 类别
                contentDir['level'] = each[6: len(each)]
            elif each[0:5] == '◎语\u3000\u3000言':
                # 语言
                contentDir['language'] = each[6: len(each)]
            elif each[0:5] == '◎字\u3000\u3000幕':
                # 字幕
                contentDir['subtitles'] = each[6: len(each)]
            elif each[0:5] == '◎上映日期':
                # 上映日期
                contentDir['publish'] = each[6: len(each)]
            elif each[0:7] == '◎IMDb评分':
                # IMDb评分
                contentDir['IMDB_socre'] = each[9: len(each)]
            elif each[0:5] == '◎豆瓣评分':
                # 豆瓣评分
                contentDir['douban_score'] = each[6: len(each)]
            elif each[0:5] == '◎文件格式':
                # 文件格式
                contentDir['format'] = each[6: len(each)]
            elif each[0:5] == '◎视频尺寸':
                # 视频尺寸
                contentDir['resolution'] = each[6: len(each)]
            elif each[0:5] == '◎文件大小':
                # 文件大小
                contentDir['size'] = each[6: len(each)]
            elif each[0:5] == '◎片\u3000\u3000长':
                # 片长
                contentDir['duration'] = each[6: len(each)]
            elif each[0:5] == '◎导\u3000\u3000演':
                # 导演
                contentDir['director'] = each[6: len(each)]
            elif each[0:5] == '◎主\u3000\u3000演':
                # 主演
                actor = each[6: len(each)]

        for item in content:
            if item[0: 4] == '\u3000\u3000\u3000\u3000':
                actor = actor + '\n' + item[6: len(item)]

        # 主演
        contentDir['actors'] = actor
        # 海报
        if imgs[0] != None:
            contentDir['placard'] = imgs[0]
        # 影片截图
        if imgs[1] != None:
            contentDir['screenshot'] = imgs[1]
        # 下载地址
        ftp = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/td/table/tbody/tr/td/a/text()")

        # 为了兼容 2012 年前的页面
        if not len(ftp):
            ftp = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/div/td/table/tbody/tr/td/font/a/text()")

        if not len(ftp):
            ftp = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/div/td/table/tbody/tr/td/a/text()")

        if not len(ftp):
            ftp = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/div/td/div/table/tbody/tr/td/font/a/text()")

        if not len(ftp):
            ftp = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/td/div/table/tbody/tr/td/a/text()")

        if not len(ftp):
            ftp = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/td/table/tbody/tr/td/a/text()")

        if not len(ftp):
            ftp = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/div/td/p/span/a/text()")

        if not len(ftp):
            ftp = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/div/td/div/div/table/tbody/tr/td/font/a/text()")

        if not len(ftp):
            ftp = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/div/td/span/table/tbody/tr/td/font/a/text()")

        if not len(ftp):
            ftp = selector.xpath("//div[@class='co_content8']/ul/tr/td/div/div/td/div/span/div/table/tbody/tr/td/font/a/text()")

        contentDir['ftpurl'] = ftp[0]
        # 页面链接
        contentDir['dytt8_url'] = url
        print(contentDir)
        return contentDir

getMoiveInforms 方法是主要负责解析电影信息节点并将其封装成字典。在代码中,你看到 Xpath 的路径表达式不止一条。因为电影天堂的电影详情页面的排版参差不齐,所以单单一条内容提取表达式、海报和影片截图表达式、下载地址表达式远远无法满足。

选择字典类型作为存储电影信息的数据结构,也是自己爬坑之后才决定的。这算是该站点另一个坑人的地方。电影详情页中有些内容节点是没有,例如类型、豆瓣评分,所以无法使用列表按顺序保存。

3 爬取结果

我这里展示自己爬取最新栏目中 4000 多条数据中前面部分数据。


点击查看大图

附:源代码地址


上篇文章:“干将莫邪” —— Xpath 与 lxml 库
推荐阅读:详解 Requests 库的用法


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

推荐阅读更多精彩内容