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

你可能感兴趣的:(python爬虫:豆瓣《神秘巨星》2921篇影评)