python爬虫:豆瓣《神秘巨星》2921篇影评

背景音乐:雨还是不停地落下 - 孙燕姿

继续水一篇文章……


前段时间看了电影《神秘巨星》,路转粉啊。
我个人是很喜欢啦,尽管剧情方面简单了点,但是音乐很赞呀。
那其他人是怎么看待《神秘巨星》的呢?让我们去豆瓣上的影评上了解一下。

可以看到有2861条影评(截止2018年2月10日),每条影评可以收集的数据包括:作者、评分、日期、影评内容、点赞数、反对数以及评论数

其中,影评部分是被折叠的,想要看到全部内容,要么点击“展开”,要么点击题目跳转到指定页。从操作上来看,前者更容易,因为后者跳转后还涉及到一个返回的过程,会造成更多的不确定性。


遇到的问题

1 豆瓣的反爬虫机制

在写python脚本进行爬虫的时候,最开始是直接用requests模块发起get请求,结果爬了几页后,服务器返回的是脏数据。

因为最开始爬虫是成功的,说明豆瓣的服务器是通过分析我的行为才识别出爬虫,那么在这种情况下通常有三种比较简单的做法:
1)【改用户】设置代理服务器,不断修改IP;
2)【改行为】延长请求间隔时间,减少被检测出来的概率;
3)【改行为】模拟人类行为,用selenium开启浏览器爬取;

由于此时我用脚本的方式已经无法正常发起请求了(尽管我尽可能构造了合理的头信息和用有效的cookie还是无能为力),但是浏览器可以正常访问,为了赶时间,我决定采用方法3,同时降低我的爬虫速度,毕竟,你好我好大家好嘛。

愉快地决定了!

2 selenium的等待问题

在用selenium的过程中,我发现常常出现点击<展开>失败的情况,后来查了一下,发现这其实是因为该元素没有被及时加载。

现在大部分的网络应用都使用AJAX技术。当浏览器加载页面时,该页面内的元素可能会以不同的时间间隔加载,并且加载速度同时还取决于你的网络状况。因此,这使得定位元素变得困难:如果DOM中还没有元素,则定位函数将引发ElementNotVisibleException异常。所以,有必要对selenium引入等待,即在执行的操作之间保持一些间隔。

Selenium Webdriver提供了两种类型的等待:隐式和显式。显式的等待是指WebDriver在继续执行之前会等待特定的条件发生。隐式的等待是指WebDriver在尝试查找元素时会轮询DOM一段时间。

2.1 显式等待

以百度首页为例,我希望进入后能点击右上角的“地图”button。
其实并不需要等待,这里只是作为示意。
通过查看网页代码,可以发现该button的name属性为tj_trmap。

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# 初始化浏览器
driver = webdriver.Chrome() 

# 打开网页
driver.get('https://www.baidu.com')

# 等待并点击
wait = WebDriverWait(driver, 10)
element = wait.until(EC.element_to_be_clickable((By.NAME, 'tj_trmap')))
element.click()

上面的脚本的意思是,浏览器会最多等待10秒直到指定的元素是clickable的,然后再点击。
所谓clickable的,就是该元素可见(不管在不在视窗之外)并且已启用。

2.2 隐式等待

继续用上面的case,

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# 初始化浏览器
driver = webdriver.Chrome() 
driver.implicitly_wait(10)

# 打开网页
driver.get('https://www.baidu.com')

# 点击“地图”
element = driver.find_element_by_name('tj_trmap')
element.click()

3. selenium的定位问题

我发现,对于已经加载好的元素,如果它是在可视范围之外,即不滚动则看不到,那么对它的点击操作将会失败!

豆瓣的招聘页为例,我的目标是让浏览器自动从顶部滚动到底部并点击“联系我们”。

from selenium import webdriver

# 初始化浏览器
driver = webdriver.Chrome() 
driver.implicitly_wait(10)

# 打开网页
driver.get('https://jobs.douban.com/')

# 滚动到指定元素并点击
element = driver.find_element_by_xpath("//a[@href='https://www.douban.com/about?topic=contactus']")
driver.execute_script("arguments[0].scrollIntoView();", element) 
element.click()

上面的脚本里,我用execute_script来执行一个js操作来实现滚动的效果,参考了python中selenium操作下拉滚动条方法汇总


爬影评

环境:python 2.7
系统:macOS 10.13.1
模块:BeautifulSoup、selenium、pandas、numpy、os、sys、time

解决了上面三个比较关键的问题后,开始完善脚本,爬起来!

from __future__ import print_function
import os
import sys
import time
import pandas as pd
import numpy as np
from selenium import webdriver  
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from BeautifulSoup import BeautifulSoup


# 初始化浏览器
def reset_driver(current_url):
    # 设置Chrome浏览器参数为不加载图片
    chrome_options = webdriver.ChromeOptions()
    prefs = {"profile.managed_default_content_settings.images":2}
    chrome_options.add_experimental_option("prefs",prefs)
    driver = webdriver.Chrome('./chromedriver', chrome_options=chrome_options) 
    driver.implicitly_wait(10)  #设置智能等待10秒

    # 登陆(后来发现也可以不登录啦)
    # driver.get("https://www.douban.com/accounts/login")  
    # elem_user = driver.find_element_by_id("email")  
    # elem_user.send_keys("用户名")  
    # elem_pwd = driver.find_element_by_id("password")  
    # elem_pwd.send_keys("密码")  
    # elem_pwd.send_keys(Keys.RETURN)  

    # 跳转到指定页
    driver.get(current_url)

    return driver


# 浏览器滚动到指定元素的位置并点击
def scroll_and_click(element):
    wait = WebDriverWait(driver, 10)
    element = wait.until(EC.visibility_of(element))
    driver.execute_script("arguments[0].scrollIntoView();", element)
    element.click()


# 合并结果
def merge_results():
    df_list = []
    # 遍历临时文件夹下的所有文件
    for f in os.listdir(path_tmp):
        if not f.startswith('.') and not f.startswith('all') and f.endswith('.csv'):
            index = int(f.split('.')[0])
            df = pd.read_csv(path_tmp + f)
            df_list.append([index, df])

    # 按照文件名的数字排序
    df_list = sorted(df_list)
    df_list = list(zip(*df_list))[1]

    # 合并
    df_all = pd.concat(df_list)

    # 保存到指定文件
    file_target = path_tmp + 'all.csv'
    df_all.to_csv(file_target, index=False)

    print('{} files -> {}'.format(len(df_list), file_target))


# 爬当前页的影评数据
def crawl():
    # 评分字典,中文→数字
    rating_dict = {u'力荐':5, u'推荐':4, u'还行':3, u'较差':2, u'很差':1}
    comments = []

    # 若发现折叠,则点击展开
    unfolders = driver.find_elements_by_xpath("//a[@class='btn-unfold']")
    if len(unfolders):
        scroll_and_click(unfolders[0])

    # 用BeautifulSoup对网页进行处理
    page = BeautifulSoup(driver.page_source)
    comment_grids = page.findAll('div', {'typeof':'v:Review'})
    total_num = len(comment_grids)  # 影评总数
    page_index = int(page.find('span', {'class':'thispage'}).text) # 当前页码

    for N, comment in enumerate(comment_grids):
        # 收集本页的基本信息:姓名、评分、日期
        name = comment.find('a', {'class':'name'}).text
        rating = comment.find('span', {'property':'v:rating'})
        rating = rating_dict[rating.get('title')] if rating is not None else np.nan  # 没有评分时用缺失值代替
        date = comment.find('span', {'property':'v:dtreviewed'}).text

        # 点击展开获得完整影评
        element = driver.find_elements_by_xpath("//div[@class='short-content']")[N]
        scroll_and_click(element)
    
        # 根据data_cid来定位完整影评,等待完全加载
        data_cid = comment.get('data-cid').encode('utf-8')
        xpath = "//div[@property='v:description'][@data-url='https://movie.douban.com/review/{}/']".format(data_cid)
        wait = WebDriverWait(driver, 10)
        element = wait.until(EC.visibility_of(driver.find_element_by_xpath(xpath)))
        text = element.text

        # 添加到列表
        comment = {
            'name': name,
            'rating': rating,
            'time': date,
            'text': text
        }
        comments.append(comment)

        # 打印进度
        print('items: {0:4.0f}:{1:4.0f} / pages: {2:4.0f}:{3:4.0f}'.format(N + 1, total_num, page_index, total_page), end='\r')
        sys.stdout.flush()

        # 设置点击的时间间隔在2~4秒
        time.sleep(2 + np.random.uniform() * 2)

    # 保存到临时文件
    file_tmp_result = path_tmp + str(page_index) + '.csv'
    df = pd.DataFrame(comments)
    df.to_csv(file_tmp_result, encoding='utf-8', index=False)


# 在当前页创建一个临时文件用于存储临时数据
path_tmp = './tmp/'
if not os.path.exists(path_tmp):
    os.mkdir(path_tmp)

# 初始化浏览器
current_url = 'https://movie.douban.com/subject/26942674/reviews?start=0'
driver = reset_driver(current_url)

# 设置总页数
page = BeautifulSoup(driver.page_source)
total_page = int(page.find('span', {'class':'next'}).findPrevious('a').text)

# 开始爬
for i in range(total_page):
    crawl()  # 爬取当前页
    if i < total_page - 1:
        scroll_and_click("//a[text()='后页>']")  # 翻页

# 合并结果
merge_results()

运行起来吧!估算完成时间为2.5个小时。

中途可以很方便地看到目前的进展,爬到了第86页的第9个影评

items:    9:  20 / pages:   86: 147

最后可以看到147个文件合并成all.csv:

147 files -> ./tmp/all.csv

打开看结果,还不错:


后记

  1. 在爬的过程中,发现极少数影评居然没有评分,导致报错,这个也是蛮神奇的,后来不得不在脚本里额外加了个判断。

  2. 这次先把爬虫的脚本写好,之后有时间了再对这些文本进行数据分析。

  3. 有部分影评是被折叠的,打开看了以后并没有觉得不合适,所以这次都爬了。

  4. 2~4秒的点击间隔是多次尝试的结果,对于我的网络状况而言,这样刚好不被豆瓣服务器判定为爬虫。如果网络状况不太好的话,得把点击间隔设得更大一些,同时等待时间也要设得长一些。

  5. 爬好的数据在如下链接,仅供学习:https://pan.baidu.com/s/1hsTQLPa 密码:1vki

往期相关文章

python爬虫:豆瓣电影TOP100

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

推荐阅读更多精彩内容