设为首页收藏本站

Crossin的编程教室

 找回密码
 立即加入
查看: 25838|回复: 21
打印 上一主题 下一主题

「一道大数据习题」豆瓣评论最多的三千部电影

[复制链接]
回帖奖励 21 金钱 回复本帖可获得 3 金钱奖励! 每人限 1 次

174

主题

45

好友

11万

积分

管理员

Rank: 9Rank: 9Rank: 9

跳转到指定楼层
楼主
发表于 2013-10-17 17:33:32 |只看该作者 |正序浏览
现在到处都说“大数据”,我也跟着标题党一下。今天要说的这个,还算不上大数据,只能说跟以前的习题相比,数据量略大了一点。

前阵子我们做了个抓取热映电影的程序。有个朋友看到了就说,他正好需要一项数据:豆瓣上的电影按评价人数从高到底排序。他认为,单是评分高低并不能说明一部电影的受关注度,比如有些分超低的奇葩大烂片照样火得很。但豆瓣本身并没有提供类似的功能。所以他想找我帮忙。我说你要排出多少?他说三千部。我说你这是要开录像厅吗!一天看一部也得看个八、九年。他说这你甭管,我这是要用来做观影决策参考的。

我想了想,觉得这事也不是太难搞定,只是有些复杂,要处理几个问题。不过这倒是一个用来练手的好题目。于是秉着授人以鱼不如授人以渔的原则,我决定把这个问题整理一下,抛给大家。

问题描述:
抓取豆瓣上的电影,按评价人数从高到低排序,列出前3000部。
每部电影显示名称、评价人数、豆瓣评分、豆瓣链接。

问题分析:
基本思路是:1.抓下所有的电影;2.按评价人数排序

然而豆瓣上并没有提供“所有电影”这样一个列表,也没有一个全覆盖的分类,有的只是:标签。

http://movie.douban.com/tag/?view=type

这个页面就是豆瓣的标签页面,上面列出了常用的标签。但一个电影可能有很多个标签,也可能不含有这里列出的标签。另外我尝试了下,每个标签只能显示前50页也就是1000部电影。

所以我想到的方法就是:先抓取这个页面上的所有标签,然后进入每个标签页的前50页抓取电影列表。我们要的信息列表页上都已经有了,不用再进入影片页面。但在记录电影的时候,需要去除重复。已经记录下的电影就不再重复记录。这个操作可以在抓取的时候进行,也可以先全部抓取,再去重。

这样做其实未必能抓到所有电影,不过对于我们这种精确度不需要很高的需求来说,应该足够了。

得到所有影片的信息之后,接下来排序就比较容易了。Python中提供了sort方法。但这里可能遇到的问题是,影片数量太多,导致读写和排序都很慢。一个供参考的优化方法是:在抓取的时候就分段存储,预先给评价人数设定一些值,按这些值来存储不同级别的电影。最后排序的时候可以每一段分别排序,而如果高评价人数的电影已经超过三千部,就无需再排后面的影片。

结果展示:
最后的结果建议保存成一个html文件,并且把影片的豆瓣地址做成链接。这样在浏览器中查看的时候,就可以直接链接到对应的豆瓣页面上。

你还可以增加一些数据统计,比如评价人数的分布,评分的分布,评价人数与评分的关系等。对web开发熟悉的同学,甚至可以把这些数据做成一个小网站,用来对豆瓣电影进行多维的搜索排序,成为一个个性化的电影推荐应用。

题外话:
前面提到,这样可能也无法抓到所有电影。一种更全面的方法是,在数据库里维护标签和影片的数据,然后从每部电影的标签和相关电影中不断寻找没有抓过的标签和电影,递归地抓取。但这样做,消耗的时间是远远超出现在的方法。

知乎上有个问题:“豆瓣有多少部电影?”。有人用抽样统计的方法推算出大约在2万部左右,但加上各种短片、动画、广告片,可能有几十万。做完这个程序,你也可以对这个问题有一个自己的答案。

上次讲了抓天气的程序之后,天气网的接口就有些不稳定了。希望这次不会给豆瓣带来压力。


#==== Crossin的编程教室 ====#
微信ID:crossincode
网站:http://crossincode.com
回复

使用道具 举报

1

主题

0

好友

120

积分

注册会员

Rank: 2

22#
发表于 2013-11-13 20:35:12 |只看该作者

回帖奖励 +3 金钱

jxgx072037 发表于 2013-10-25 02:55
目前搞出来一个理论上可行的版本,这个版本在抓取豆瓣电影时会出现请求过多,被豆瓣屏蔽的现象……采取的方 ...

请问,那条正则是什么意思呢?阅读遇到障碍了,谢谢啦
回复

使用道具 举报

0

主题

0

好友

17

积分

新手上路

Rank: 1

21#
发表于 2013-11-9 00:01:58 |只看该作者

回帖奖励 +3 金钱

我也写了一份自己的版本,但是还比较简陋,只是抓取了豆瓣主要类别标签页中前9页的电影,然后排了一个序,总数大概4000多,有时候会有点问题,数量每次运行不太一致,估计和网络环境,以及有些编码问题没解决好有关(欢迎大家拍砖):

#!usr/bin/python
#coding:utf-8
import sys
import os
import re

'''
@author:wxfengyun
新浪围脖:http://weibo.com/u/1490668587
'''
import urllib2
from bs4 import BeautifulSoup

def FindMovie(mysoup):
        #获取电影名称
        nameFind = mysoup.find('a', {"class":"nbg"})
        name = nameFind['title']

        #获取相应电影的评价人数
        pinglunFind = mysoup.find('span', {"class":"pl"})
        pinglun = pinglunFind.get_text()

        match = re.search(r'\d+', pinglun)


        if match:
                g_Movie[name] = int(match.group())
       
        while True:
               
                nameFind = nameFind.find_next('a', {"class":"nbg"})
                if nameFind == None:
                        break

                name = nameFind['title']

                pinglunFind = pinglunFind.find_next('span', {"class":"pl"})
                if pinglunFind:
                        pinglun = pinglunFind.get_text()

                if pinglun:
                        match = re.search(r'\d+', pinglun)
                        if match:
                                g_Movie[name] = int(match.group())
       

reload(sys)
sys.setdefaultencoding('utf8')

g_Movie = {}

url = "http://movie.douban.com/tag/?view=type"
html = urllib2.urlopen(url).read()
soup = BeautifulSoup(html)

div_hot = soup.tbody

#获取标签页
for i in div_hot.find_all('a'):

        url = "http://movie.douban.com/tag/%s" % i.get_text()

        html = urllib2.urlopen(url).read()

        if html == None:
                break

        soup = BeautifulSoup(html)
        nameFind = soup.find('span', {"class":"thispage"})

        #FindMovie(soup)

        for j in range(0,9) :
                #获取同一标签页下的前9个版面       
                nameFind = nameFind.find_next('a')

                html = urllib2.urlopen(nameFind['href']).read()
                if html == None:
                        break

                soup = BeautifulSoup(html)               
               
                if soup:
                        FindMovie(soup)
               

       
print u"\t\t\t  豆瓣电影排行榜\n"

ff = sorted(g_Movie.items(), key=lambda e:e[1], reverse=True)

count = 1

for t in ff:
        print u"%d: %s(%s)" % (count, t[0], t[1])
        count += 1
回复

使用道具 举报

2

主题

0

好友

161

积分

注册会员

Rank: 2

20#
发表于 2013-11-4 00:26:08 |只看该作者
这周荒废了……没有练习,我先去看看豆瓣API咋用
回复

使用道具 举报

174

主题

45

好友

11万

积分

管理员

Rank: 9Rank: 9Rank: 9

19#
发表于 2013-10-29 20:38:57 |只看该作者
jxgx072037 发表于 2013-10-29 18:20
嚓,我昨天只在群上喊了一下,忘记直接问你了。。我是群里的Qiao

我回来后看到了
#==== Crossin的编程教室 ====#
微信ID:crossincode
网站:http://crossincode.com
回复

使用道具 举报

2

主题

0

好友

161

积分

注册会员

Rank: 2

18#
发表于 2013-10-29 18:20:54 来自手机 |只看该作者
本帖最后由 jxgx072037 于 2013-10-29 18:26 编辑
crossin先生 发表于 2013-10-28 23:31
我偷了个懒,直接拿了豆瓣api的search,然后把几个分类标签扔进去,“爱情”和“剧情”分别抓了一两万, ...


嚓,我昨天只在群上喊了一下,忘记直接问你了。。我是群里的Qiao
回复

使用道具 举报

174

主题

45

好友

11万

积分

管理员

Rank: 9Rank: 9Rank: 9

17#
发表于 2013-10-28 23:31:40 |只看该作者
jxgx072037 发表于 2013-10-28 22:39
哇,期待~~嘿嘿,我这两天也把代码更新了下~~~~另外,今晚在交大看见拉夫琴大神本人啦~~ ...

我偷了个懒,直接拿了豆瓣api的search,然后把几个分类标签扔进去,“爱情”和“剧情”分别抓了一两万,后来觉得没必要,其他标签都只抓了前几百个,去重排序。
而且为了进一步偷懒,我没用评价数(这个有点不符合题目最初要求了),用了search结果列表里直接附带的collect_count,也就是“看过”的人数。这个数和评价数是相关的,所以不会差太多。就不用进一步去请求每部电影的数据了。

在抓的时候,当发现电影已经在列表中,就不保存了。
为了让列表不要太大,我按看过数在10万以上、1万以上、1千以上、1千以下分了四个文件。
写文件的时候用了a模式,抓到一部写一部,即使中途断掉也无所谓,改一下初始位置继续抓。

我这个方法不是很严谨,没有用“评价”数,也不能保证所有影片都抓到。不过误差应该不会很大。另外结果里好像还是有重复的,可能中间哪里有漏洞。

PS:你今天说不定也看到我本人了。我也在现场。

top3k.html

465.97 KB, 下载次数: 51

get_movie.py

2.42 KB, 下载次数: 38

#==== Crossin的编程教室 ====#
微信ID:crossincode
网站:http://crossincode.com
回复

使用道具 举报

2

主题

0

好友

161

积分

注册会员

Rank: 2

16#
发表于 2013-10-28 22:47:13 |只看该作者
本帖最后由 jxgx072037 于 2013-10-28 22:50 编辑

更新日志:2013-10-28
1. 增加了步骤计时监控;
2. 每个页面完成后暂停2s再继续,躲避豆瓣的屏蔽;
3. 增加了去重;
4. 去除超过了预期的电影数目;
5. 修改了评论人数的抓取规则,初步抓取时包涵“还未上映”的情况;
6. 取消了超过3000部停止抓取的设置,改为先抓取全部标签,再去掉超过3000的部分;

更新总结:试了两次,2s/页的暂停已经可以让豆瓣开心了,不会屏蔽抓取请求……但是,出现了新的问题。每次抓取到差不多2000部电影,程序就会死机……目前猜测是电 影数量多了,所以每一次抓取后的“去重”计算量过大。 另外,真心觉得每次问题的解决方法都要比开始想象的复杂。要实现一个想法,往往会碰到之前从未考虑过的细节问题。

代码如下:
  1. # -*- coding: utf-8 -*-
  2. import urllib2
  3. import re
  4. import time

  5. #抓取豆瓣电影标签
  6. f=urllib2.urlopen('http://movie.douban.com/tag/?view=cloud').read()
  7. n1=f.find('007')
  8. n2=f.find('宗教')        #n1是第一个标签,n2是第二个标签,利用这两个标签定位标签们在f中的位置
  9. f1=f[(n1-4):(n2+10)]    #去掉标签旁边的杂质
  10. f2=re.findall('\>\S{1,}?\<',f1)         #初步抓取标签
  11. movie_tags=[]   #movie_tags为电影标签
  12. for n in f2:
  13.     n=n[1:-1]
  14.     movie_tags.append(n)

  15. #抓取每个标签打开后的网页地址
  16. class Movie_list:
  17.     def __init__(self):
  18.         self.url1='http://movie.douban.com/tag/'
  19.     def request_open(self,n1,n2): #n1是标签list中的序号,n2是页码
  20.         self.url2='?start='+str(n2*20)+'&type=T'
  21.         self.page1=urllib2.urlopen(self.url1+movie_tags[n1]+self.url2).read()
  22.         return self.page1 #点击标签打开后的页面地址
  23.         
  24.    

  25. #抓取电影列表的页码数
  26. class Next_page:
  27.     def __init__(self):
  28.         self.url1='http://movie.douban.com/tag/'
  29.     def np(self,n1):
  30.         self.page1=urllib2.urlopen(self.url1+movie_tags[n1]).read()
  31.         self.url2=re.findall('amp.*\d{1,2}',self.page1)
  32.         if self.url2:
  33.             self.num2=self.url2[-1][13:]
  34.             if int(self.num2)<50 or int(self.num2)==50:
  35.                 return int(self.num2)
  36.             else:
  37.                 return 50       #电影列表页数超过50页则只扫描前50页
  38.         else:
  39.             return 1

  40. movie_list=Movie_list()
  41. movie=[]


  42. #抓取电影信息(名字,评价人数,排名,地址)
  43. class Movie_info:
  44.     def __init__(self):
  45.         pass
  46.     def m(self,n1,n2):
  47.     # 抓取每部电影的电影名称
  48.         self.movie_name2=[]
  49.         self.movie_name1=re.findall('title="\S{1,}?"',movie_list.request_open(n1,n2))
  50.         for x in range(len(self.movie_name1)-1):
  51.             self.movie_name2.append(self.movie_name1[x][7:-1])
  52.    
  53.     # 抓取每部电影的评价人数
  54.         self.movie_comment2=[]
  55.         self.movie_comment1=re.findall('span class="pl.*评价|span class="pl.*上映',movie_list.request_open(n1,n2))
  56.         for s in self.movie_comment1:
  57.             if re.findall('\d{1,}',s):
  58.                 self.movie_comment2.append(re.findall('\d{1,}',s)[0])
  59.             else:
  60.                 self.movie_comment2.append('0')

  61.     # 抓取每部电影的评分
  62.         self.movie_rating2=[]
  63.         self.movie_rating1=re.findall('class="star clearfix[\s\S]*?pl',movie_list.request_open(n1,n2))
  64.         for s in self.movie_rating1:
  65.             self.p2=re.findall('\d\.\d',s)
  66.             if self.p2:
  67.                 self.movie_rating2.append(self.p2[0])
  68.             else:
  69.                 self.movie_rating2.append('没有评分')   

  70.     # 抓取每部电影的链接
  71.         self.movie_url2=[]
  72.         self.n=0
  73.         self.movie_url1=re.findall('http://movie.douban.com/subject/\d{1,10}',movie_list.request_open(n1,n2))
  74.         for i in range(len(self.movie_url1)/2):
  75.             self.movie_url2.append(self.movie_url1[2*self.n])
  76.             self.n+=1   

  77. # 把每部电影的4个信息合并成一个list--self.dic,再依次存到movie这个大list中
  78.         for i in range(len(self.movie_name2)):
  79.             self.dic=[]                                        #用self.dic暂时存储电影信息
  80.             self.dic.append(self.movie_name2[i])               #名字
  81.             self.dic.append(self.movie_comment2[i])            #评价人数
  82.             self.dic.append(self.movie_rating2[i])             #评分
  83.             self.dic.append(self.movie_url2[i])                #地址
  84.             if int(self.dic[1])>25000:    #评价人数少于25000的直接放弃
  85.                 movie.append(self.dic)
  86.                 if len(movie)>2:
  87.                     for i in range(len(movie)-1):              #去重
  88.                         if movie[-1][0]==movie[i][0]:   #用电影名字判定是否重复
  89.                             del movie[-1]
  90.                             break
  91.                 print '%d/3000'%(len(movie))      #打印进度
  92.             else:
  93.                 continue

  94. next_page=Next_page()
  95. movie_info=Movie_info()


  96. #执行,开始抓取
  97. for x in range(len(movie_tags)):          #x代表标签在movie_tags这个list中的位置
  98.     print "正在抓取标签“%s”中的电影"%(movie_tags[x])
  99.     starttime2=time.time()
  100.     for i in range(next_page.np(x)):       #i代表正在抓取当前标签的第i页
  101.         print "开始抓取第%d页,抓取进度:"%(i+1)
  102.         starttime2=time.time()
  103.         movie_info.m(x,i)
  104.         endtime2=time.time()
  105.         print "抓取第%d页完毕,用时%.2fs"%(i+1,endtime2-starttime2)     #输出抓取每个页面所花费的时间
  106.         time.sleep(2)
  107.     endtime2=time.time()
  108.     print "抓取“%s”标签完毕,用时%.2fs\n"%(movie_tags[x],endtime2-starttime2)   #输出抓取每个标签所花费的时间


  109. #删除超过3000部的电影
  110. if len(movie)>3000:
  111.     for i in range(len(movie)-3000):
  112.         del movie[-i]

  113. #排序
  114. def comment(s):
  115.     return int(s[1])
  116. starttime4=time.time()
  117. print '开始排序……'
  118. movie.sort(key = comment, reverse=True)
  119. endtime4=time.time()
  120. print '排序完毕,共耗时%.2f'%(endtime4-starttime4)

  121. #写到html文件里面
  122. f=file('Douban_movies.html','w')
  123. f.write('<!DOCTYPE html>\n<html>\n<head>\n')
  124. f.write('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">\n')
  125. f.write('</head>\n\n<body>\n')
  126. f.write('<h1>豆瓣电影榜单</h1>'+' '+'<h3>按评价人数排名,共3000部</h3>')  #标题
  127. s=1     #s是电影序号
  128. for i in movie:
  129.     f.write('<p>'+str(s)+'. '+'<a href="'+i[3]+'">'+i[0]+'</a>'+',共'+i[1]+'人评价,'+'得分:'+i[2]+'分;'+'\n')
  130.     s+=1
  131. f.write('</body>')
  132. f.close()

  133. print '完成!请查看html文件,获取豆瓣电影榜单。'











  134.         
复制代码
回复

使用道具 举报

2

主题

0

好友

161

积分

注册会员

Rank: 2

15#
发表于 2013-10-28 22:39:33 |只看该作者
本帖最后由 jxgx072037 于 2013-10-28 22:48 编辑
crossin先生 发表于 2013-10-28 16:57
好像也不是。。。它的api写着total:200,但是start=设比200大的数字也可以。然后我就抓了一个版本,待会 ...

哇,期待~~嘿嘿,我这两天也把代码更新了下~~~~另外,今晚在交大看见拉夫琴大神本人啦~~
回复

使用道具 举报

174

主题

45

好友

11万

积分

管理员

Rank: 9Rank: 9Rank: 9

14#
发表于 2013-10-28 16:57:44 |只看该作者
jxgx072037 发表于 2013-10-27 20:41
干……我们有成为病毒制造者的潜质……

好像也不是。。。它的api写着total:200,但是start=设比200大的数字也可以。然后我就抓了一个版本,待会儿放上来
#==== Crossin的编程教室 ====#
微信ID:crossincode
网站:http://crossincode.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即加入

QQ|手机版|Archiver|Crossin的编程教室 ( 苏ICP备15063769号  

GMT+8, 2024-6-17 11:23 , Processed in 0.025137 second(s), 30 queries .

Powered by Discuz! X2.5

© 2001-2012 Comsenz Inc.

回顶部