使用Python的Text Processing方法爬取IMDb

作者: JohnnyMOON | 来源:发表于2017-04-08 08:28 被阅读333次

    0 准备工作

    这篇文章将以爬取IMDb(Internet Movie Database)著名的TOP 250电影榜单为例,简单叙述用Python编写一个网络爬虫的方法和内在逻辑。

    首先我们来看一下这个电影榜单的样子。

    IMDb TOP 250

    可以看到,这个榜单总共有250条记录,每条记录由排名和题名、IMDb评分和我的评分三列构成的。在本例中,我们只对排名,题名,年份,IMDb评分感兴趣,并将在下面的工作中,试图把它们导出到CSV文件。阅读过拙作《使用pandas-datareader包下载雅虎财经股价数据》的朋友们,可能会对如何将DataFrame类型导出到CSV文件有一些印象。我们将在本篇再次应用该方法。

    在正式开始工作之前,你要对你所看到的网页是怎样形成的有一个不多不少的了解。比如,你要知道你正在阅读的内容,它的形状,它的颜色,它的样式,是由一行行的代码,而非我们所熟知的类似于MS WORD的所见即所得的编写形式表现的。在主流浏览器,如Google Chrome中,你只需要轻按‘Ctrl + U’组合键,即可查看当前网页源代码。

    以下是榜单界面的部分源代码。

    IMDb TOP 250 Source Code

    非常丑。

    再比如,你要知道浏览器的网址栏里的那行网址,它有一个比较正式的名字叫做URL. 我们可以发现,对于任意一个URL,都有一段源代码与其对应。这段源代码或长或短,而且可能用不同语言编写而成,但是在我们面前,它只是一个有待我们处理的字符串,如鱼在砧。

    对于网页的形成,你只需要了解到这里,你甚至不需要知道网页是什么语言编写成的,就可以开始工作了。下面,打开你的Python编辑器,并在你的浏览器(如Google Chrome)中打开IMDb TOP 250电影榜单,你便做好了所有的准备工作。

    1 获取网页源代码

    获取网页源代码有很多种方法,下面记录一种我在知名MOOC——《Udacity CS101 计算机科学导论》中学到的方法,即使用urllib2包。

    import urllib2
    
    
    def get_page(url):
        try:
            return urllib2.urlopen(url).read()
        except:
            return ''
    

    在这段代码中,我们定义了一个叫做get_page的函数,它的作用是将我们传入的URL的源代码返回,返回类型为string.

    我们在定义get_page函数时,用到了try/except语句。该语句的作用是,先执行try后面的语句,如果这些语句出现了任何error,则执行except后面的语句。此例中,如果urllib2.urlopen(url).read()函数未能正确运行,get_page函数将返回一个空的字符串''。

    用这个函数来试着获取榜单的源代码,并将它保存到一个名为page的变量中。

    page = get_page('http://www.imdb.com/chart/top?ref_=nv_ch_250_4')
    

    2 观察源代码所包含信息的规律

    page现在就是我们要进行处理的字符串了,它包括了所有我们需要的排名、题名、年份和评分信息。我们现在要观察这些信息处于源代码的哪些位置。

    仍然使用你的Google Chrome浏览器,使用'Ctrl + U'组合键,查看榜单网页的源代码。使用'Ctrl + F'组合键,呼出查找命令,帮助你探索这些信息的规律。

    我们记得,排在榜单第一名的是1994年犯罪剧情电影《肖申克的救赎》,因此我们在查找框中查找关键词‘shawshank’,可以看到,这个关键词在整个网页中只出现过一次。

    查找结果

    这个关键词直接定位到了《肖申克的救赎》出现的位置。我们可以观察一下包含了《肖申克的救赎》电影信息的数十行代码。

    ...
    <td class="titleColumn">
    1.
    <a href="/title/tt0111161/?pf_rd_m=A2FGELUUNOQJNL&pf_rd_p=2398042102&pf_rd_r=0HCQTT49N5CQ4Y7B95CP&pf_rd_s=center-1&pf_rd_t=15506&pf_rd_i=top&ref_=chttp_tt_1"
    title="Frank Darabont (dir.), Tim Robbins, Morgan Freeman" >The Shawshank Redemption</a>
    <span class="secondaryInfo">(1994)</span>
    </td>
    <td class="ratingColumn imdbRating">
    <strong title="9.2 based on 1,794,787 user ratings">9.2</strong>
    </td>
    <td class="ratingColumn">
    <div class="seen-widget seen-widget-tt0111161 pending" data-titleid="tt0111161">
    <div class="boundary">
    <div class="popover">
    <span class="delete"> </span><ol><li>1<li>2<li>3<li>4<li>5<li>6<li>7<li>8<li>9<li>10</ol> </div>
    </div>
    <div class="inline">
    <div class="pending"></div>
    <div class="unseeable">NOT YET RELEASED</div>
    <div class="unseen"> </div>
    <div class="rating"></div>
    <div class="seen">Seen</div>
    </div>
    </div>
    </td>
    <td class="watchlistColumn">
    ...
    

    仔细观察后我们发现,这一段源代码不但包含了我们所需的排名、题名、年份、评分等信息,还包含了导演、主演、IMDb索引号、评分数等信息。这真是一个惊喜,我决定将导演和IMDb索引号也爬下来。所以现在我们关注的数据有排名、题名、年份、导演、评分、索引号等六个。我们可以发现以下规律。

    1. 排名出现在...<td class="titleColumn">1.<a href="/title/tt0111161/?pf_rd_...这段代码中。但我们发现,对于这整个榜单,每一部电影的排名就是它出现在网页中的顺序,因此,我决定偷一个懒,不对排名进行处理。但是,通过'Ctrl + F'组合键,对'titleColumn'进行查找后,可以发现整段源代码中,只有250个'titleColumn',因此我们可以认为,'titleColumn'字符串标记着每一部电影的信息的开始。

    2. 题名、导演出现在<a href="/title/tt0111161/?pf_rd_m=A2FGELUUNOQJNL&pf_rd_p=2398042102&pf_rd_r=0HCQTT49N5CQ4Y7B95CP&pf_rd_s=center-1&pf_rd_t=15506&pf_rd_i=top&ref_=chttp_tt_1" title="Frank Darabont (dir.), Tim Robbins, Morgan Freeman" >The Shawshank Redemption</a>这段代码中。我们可以观察到,导演"Frank Darabont"的名字后面标注有"(dir.)",题名则出现在导演、主演之后和'</a>'之前。

    3. 年份信息出现在<span class="secondaryInfo">(1994)</span>这一行代码中,它被包含在括号里。用来标记它位置的关键词是"secondaryInfo".

    4. 评分信息出现在<strong title="9.2 based on 1,794,787 user ratings">9.2</strong>这一行代码中,用来标记它位置的关键词是"user ratings".

    5. IMDb索引号出现在<div class="seen-widget seen-widget-tt0111161 pending" data-titleid="tt0111161">这一行代码中,用来标记它位置的关键词是"data-titleid".

    3 爬取第一个电影信息

    我们首先来想一个简单的思路:IMDb TOP 250榜单中有250部电影,我们要想出爬取一部电影的方法,使之循环250次,来爬取所有的电影。于是开始试着爬取第一部电影《肖申克的救赎》的影片信息。

    第一步,我们要定位《肖申克的救赎》这部电影的电影信息开始的位置,即找到之前确定的关键词"titleColumn"。

    在这里,要简单讲一下string字符型的两个使用方法。

    1. find方法。以str1.find('titleColumn')为例,这个命令返回的是'titleColumn'这个字符串在page这个字符串中第一次出现的位置,如果str1的值是'2tnkl09a?"{U(EFtitleColumn9fq309v-sjjj=',那么,str1.find('titleColumn')将返回15(字符串位置的排序是从0开始的,如str1[0]的值是'2')。

    2. 截取string变量的某一段,如str1[3:10],它返回的是str1这个字符串从第4位和第11位中间的字符串,包含第4位,但不包含第11位。特别地,str1[3:]返回的是str1从第4位开始一直到最后的字符串,str1[:10]返回的是str1从头开始一直到第10位的字符串,str1[3:-1]返回的是str1从第4位开始一直到倒数第2位的字符串。

    到此为止,你已经对text processing有了一定的了解,这已经足够我们进行下面的工作了。

    page = page[page.find('titleColumn'):]
    

    这段代码的含义是,首先找到'titleColumn'在字符串page中第一次出现的位置,然后截取从这个位置开始一直到结尾的字符串片段,并覆盖到变量page上。之所以对page进行覆盖操作,是因为我们对'titleColumn'之前的字符串一律不感兴趣。

    现在我们将以导演信息为例,应用text processing方法。

    导演Frank Darabont的名字,出现在'titleColumn'这一字符串出现后,第一个'title="'出现的位置之后的第7个位置,和' (dir.)'字符串出现之前。因此,我们可以通过find方法来确定这两个标志字符串的位置,并通过这两个位置来找到导演的名字。

    d_name = page[page.find('title="') + 7:page.find(' (dir.)')]
    print d_name
    

    这里我们使用page.find('title="') + 7,是因为希望使截取字符串片段从't'往右移七位处开始,即导演Frank Darabont的'F'所在的位置。

    将截取的字符串片段赋值给变量d_name并查看打印结果。

    Frank Darabont
    

    我们使用我们掌握的信息规律、关键词和text processing方法,可以一一爬取其他我们需要的信息,下面是爬取《肖申克的救赎》所有目标信息的方法。

    # director
    page = page[page.find('titleColumn'):]
    d_name = page[page.find('title="') + 7:page.find(' (dir.)')]
    
    # title
    page = page[page.find(d_name):]
    m_name = page[page.find('>') + 1:page.find('</a>')]
    
    # year
    page = page[page.find('secondaryInfo'):]
    year = page[page.find('(') + 1:page.find(')')]
    
    # rating
    page = page[page.find('user ratings'):]
    rating = page[page.find('>') + 1:page.find('</strong>')]
    rating = float(rating)
    
    # imdb_id
    page = page[page.find('data-titleid'):]
    imdb_id = page[page.find('"') + 1:page.find('">')]
    
    print m_name
    print year
    print d_name
    print rating
    print imdb_id
    

    由于评分rating本身应该是一个浮点型数据,但是我们爬取的rating是string类型数据,因此使用函数float(rating)来使其转化为float类型。这样在之后我们可以通过Python对其进行数值运算。

    观察一下输出的内容。

    The Shawshank Redemption
    1994
    Frank Darabont
    9.2
    tt0111161
    

    《肖申克的救赎》,出品于1994年,导演为Frank Darabont,IMDb评分9.2,IMDb中的ID是tt0111161.

    到此为止,除了排名(rank)以外(我们知道是第一),我们已经爬取了所有我们感兴趣的关于影片《肖申克的救赎》的信息。

    4 爬取榜单所有电影信息

    我们之前的代码爬取了榜单中排名第一的电影《肖申克的救赎》的信息,接下来我们要做的工作是使我们之前的代码通用化,以爬取榜单每一个电影的信息。

    我们掌握了一个信息是这个榜单有250个电影,所以可以使用循环语句,使其运行250次(我们将在最后讨论如果我们不知道榜单有多少电影的情况),并将所有的信息分别写入建好的list中。

    def get_info(page, d_list=[], m_list=[], y_list=[], r_list=[], id_list=[]):
        # director
        page = page[page.find('titleColumn'):]
        d_name = page[page.find('title="') + 7:page.find(' (dir.)')]
        d_list.append(d_name)
    
        # title
        page = page[page.find(d_name):]
        m_name = page[page.find('>') + 1:page.find('</a>')]
        m_list.append(m_name)
    
        # year
        page = page[page.find('secondaryInfo'):]
        year = page[page.find('(') + 1:page.find(')')]
        y_list.append(year)
    
        # rating
        page = page[page.find('user ratings'):]
        rating = page[page.find('>') + 1:page.find('</strong>')]
        rating = float(rating)
        r_list.append(rating)
    
        # imdb_id
        page = page[page.find('data-titleid'):]
        imdb_id = page[page.find('"') + 1:page.find('">')]
        id_list.append(imdb_id)
    
        # return uncrawled page
        return page
    

    这个function有六个参数,其中一个是源代码段page,另外五个是五个list类型的参数。

    首先来看返回值,这个function的返回值是经过修剪后的新page,如果我们不返回这一变量,实参page的值在循环中将不会因为形参page的值发生变化而发生变化。当page第二次查找'titleColumn'这一字符串时,将不会定位在电影《教父》的代码段附近,而是仍定位在《肖申克的救赎》代码段的附近。因此我们需要返回一个变化后的page值,来覆盖掉之前的page值。

    list这种数据类型具有一个Attribute叫做append. 如d_list.append(d_name)这个命令,是将d_name的值作为一个新的list元素,添加在list的最末端。

    list这种数据类型,它有一个指针的性质。举例来说,对于传入function的int,float,string等参数来说,传入的只是这些参数的值(将值赋给形参),并不会影响function外变量的值。但是传入list后,对于list的修改,实际上是对于list所指向地址的值的修改,因此传入function中的list(形参),如果被function所影响,function外的list(实参)也将发生改变。这是因为将list的值赋给形参后,形参和实参指向的是一个地址,代表的是同一个列表。

    通过这个性质,我们可以建立5个空的list类型变量,并将它们传入get_info中,如此循环250次,就可以获取所有电影信息。

    d_list=[]
    m_list=[]
    y_list=[]
    r_list=[]
    id_list=[]
    
    for _ in range(0, 250):
        page = get_info(page, d_list, m_list, y_list, r_list, id_list)
    

    在以上代码中,我们建立了5个空的列表,来存放五种不同数据,并写了一个循环来爬取250个电影信息。

    for循环语句for _ in range(0, 250):中,'_'可以理解为一个匿名变量,它的唯一作用就是作为循环子,因为在for循环要执行的语句中,它并没有什么作用,所以我们不想给它起名字,这种情况下,可以使用'_'作为匿名变量。

    `range(0, 250)'这个函数返回的是一个list,它从0开始,每次加一,到249截止,长度为250。在这里,它的实际数值也没有意义,只要它的长度是250,就可以保证循环执行250次。

    循环语句只有一句话,把get_info返回的修剪过的新page,覆盖原来的page,但实际上,由于list的指针性质,所有的list都发生了改变。

    如果我们不知道循环次数是250, 我们可以使用while循环。当所有电影都爬完了以后,剩下的page中将不再含有关键字'titleColumn',因此,只要每次循环之前,用while语句判断'titleColumn'是否存在于字符串之中即可。

    改良后的循环如下。

    while 'titleColumn' in page:
        page = get_info(page, d_list, m_list, y_list, r_list, id_list)
    

    5 整合、查看和导出数据

    在拙作《使用pandas-datareader包下载雅虎财经股价数据》中,我讨论过pandas包的部分用法,在这里,我将介绍一下如何使用Python的一大数据结构——字典,来生成DataFrame类型变量。

    字典是一种一对一的映射数据结构,它的每一个元素皆由两部分组成,即key和value. key是查找到value的唯一索引。

    对于key和value可以是什么,字典的要求十分松散。在此例中,我们需要建立一个形式为字典的数据类型。它的每一个元素的key是列名,而value是这一列的值。

    data = {'Title':m_list, 'Year':y_list, 'Director':d_list,
            'Rating':r_list, 'IMDb ID':id_list}
    

    在这里,我们自己建立一个rank列表,将它的key设为‘Rank’并加入到data字典中。

    data['Rank'] = range(1, 251)
    

    到这里为止,我们需要的所有数据,都已经包含在了字典data中。pandas有一个从字典生成DataFrame的方法,它对字典的要求是作为字典中value的列表是等长的。我们的字典中,每一个列表长度都是250. 因此可以使用该方法。

    import pandas as pd
    
    dataset = pd.DataFrame(data, columns=['Rank', 'Title', 'Year', 
                                          'Director', 'Rating', 'IMDb ID'])
    print dataset.head()
    

    在码代码时,一个列表中间,是可以换行的。但有一些不能换行的情况,我们将在以后讨论。

    在这个函数中,columns参数是一个字典各key值组成的列表,长度为列数,规定了每一列出现的顺序。如果不规定columns参数,生成的DataFrame的列将根据字典默认顺序排列,字典默认顺序一般情况下并不是我们的理想顺序。

    观察所得数据的前六行。

       Rank                     Title  Year              Director  Rating  \
    0     1  The Shawshank Redemption  1994        Frank Darabont     9.2   
    1     2             The Godfather  1972  Francis Ford Coppola     9.2   
    2     3    The Godfather: Part II  1974  Francis Ford Coppola     9.0   
    3     4           The Dark Knight  2008     Christopher Nolan     8.9   
    4     5              12 Angry Men  1957          Sidney Lumet     8.9   
    
         IMDb_ID  
    0  tt0111161  
    1  tt0068646  
    2  tt0071562  
    3  tt0468569  
    4  tt0050083  
    

    最后,使用to_csv命令,将得到的结果导入到CSV文件。

    dataset.to_csv('.\out\imdb250.csv', index=False)
    

    index参数设置为False,即使index列(0, 1, 2, 3, 4...)不输入到CSV文件中。

    其实,针对网页的源代码,已有前辈写好如Beautiful Soup的功能包供方便地使用,但是十分惭愧,我还没有试用过。

    此例还有一个方法是使用RegEx,可以借助软件RegExBuddy简单地进行处理,之后将有一篇笔记专门介绍这种方法

    这个爬虫程序是我写的第一个爬虫程序,它的特点就是非常Naive,逻辑容易理解,运行速度也非常快。其核心部分就是观察我们感兴趣的信息的规律(颇有RegEx的感觉)。我也将在之后爬取EDGAR数据库信息的笔记中讨论在爬取数据时翻页的操作。如果您发现任何问题或有任何疑问,欢迎指正或讨论。

    by JohnnyMOON
    COB @UIUC
    EM: gengyug2@illinois.edu

    相关文章

      网友评论

        本文标题:使用Python的Text Processing方法爬取IMDb

        本文链接:https://www.haomeiwen.com/subject/bhdhattx.html