这周公司组织了电影赏析,看的电影是《狗十三》。我之前并没有看过这部电影,就想着先去豆瓣上看一下评论。这电影的评论还不少,有好几百条,完全可以全爬下来,分析一下。拉到页面下面,点击后页,url就会跟着变化(start=那里),说明这也不是啥动态网页,完全可以写个循环,用rvest包一页一页的爬。但实际爬取的时候,遇到了问题,就是未登陆的状态下,只能爬前220条评论。我搜索了一下模拟登录的办法,似乎是成功了,但后续该怎么弄,我就不知道了。我在这里卡了一天,没想到解决办法。昨天早上躺在被窝里,突然想到,我之前研究了下用RSelenium爬取动态网页,这里我完全可以先用RSelenium模拟登录,然后把网页当成动态网页爬啊。试了一下,成功了,下面就是相关的操作过程。

首先还是载入需要用的包,要使用RSelenium包,还要先进行一些配置,具体内容可以看RSelenium包的官方网站(这网站好像需要科学上网):

library(tidyverse)
library(RSelenium)
library(rvest)
library(jiebaR)
library(wordcloud2)
library(knitr)

接下来跟Selenium Server进行连接,这里我用的是Chrome浏览器(变量名rd本应该在第一行,不知道为什么跑到下边去了……):

 rd <- remoteDriver(
   remoteServerAddr = "localhost",
   port = 4444L,
   browserName = "chrome"
)

然后模拟打开豆瓣电影的登录页面,输入用户名和密码,点击登录按键,就可以登录了:

rd$open()
rd$navigate('https://www.douban.com/accounts/login?source=movie')

we <- rd$findElement(using = 'xpath', '//*[@id="email"]')
we$sendKeysToElement(list('用户名'))

we <- rd$findElement(using = 'xpath', '//*[@id="password"]')
we$sendKeysToElement(list('密码'))

we <- rd$findElement(using = 'xpath', '//*[@id="lzform"]/div[6]/input')
we$clickElement()

如果没接触过爬虫的,看着上面的代码可能有点懵,但实际上没啥太玄奥的东西。RSelenium包中的函数名就明白显示了它是干什么的,而参数中的那些xpath,在Chrome浏览器中都是可以直接复制出来的。

后面就可以开始爬虫了。我只爬了评价星级、短评时间、有帮助次数和短评文本四项信息。需要说明的是,有些用户虽然写了短评,但不会打分,这种情况下,我认为的将其评价星级定位“无评价”。因为不打分也会影响后面内容的xpath,所以那部分用了一些if条件。另外,虽然不知道会不会用上,在每页的内容爬取完之后,我也会让程序随机休息几秒,省得被轻易地认定为是爬虫程序。

rd$navigate('https://movie.douban.com/subject/25716096/comments?start=0&limit=20&sort=new_score&status=P')

dog13 <- tibble()
for (i in 1:50) {
  
  rank <- character(0)
  time <- character(0)
  help <- character(0)
  text <- character(0)
  temp <- tibble()
  
  for (j in 1:20) {
    
    xpath_rank <- str_c('//*[@id="comments"]/div[', j, ']/div[2]/h3/span[2]/span[2]')
    we <- rd$findElement(using = 'xpath', xpath_rank)
    rank[j] <- ifelse(str_length(we$getElementAttribute('title') %>% unlist()) > 2, 
                      '无评价', we$getElementAttribute('title') %>% unlist())
    
    if (str_length(we$getElementAttribute('title') %>% unlist()) < 3) {
      
      xpath_time <- str_c('//*[@id="comments"]/div[', j, ']/div[2]/h3/span[2]/span[3]')
      we <- rd$findElement(using = 'xpath', xpath_time)
      time[j] <- we$getElementText() %>% unlist()
      
    } else {
      
      xpath_time <- str_c('//*[@id="comments"]/div[', j, ']/div[2]/h3/span[2]/span[2]')
      we <- rd$findElement(using = 'xpath', xpath_time)
      time[j] <- we$getElementText() %>% unlist()
      
    }
    
    xpath_help <- str_c('//*[@id="comments"]/div[', j, ']/div[2]/h3/span[1]/span')
    we <- rd$findElement(using = 'xpath', xpath_help)
    help[j] <- we$getElementText() %>% unlist()
    
    xpath_text <- str_c('//*[@id="comments"]/div[', j, ']/div[2]/p/span')
    we <- rd$findElement(using = 'xpath', xpath_text)
    text[j] <- we$getElementText() %>% unlist()
    
    df <- tibble(rank, time, help, text)
    
  }
  
  dog13 <- bind_rows(dog13, df)
  
  rest <- sample(1:10, 1)
  
  if (i < 2) {
    
    we <- rd$findElement(using = 'xpath', '//*[@id="paginator"]/a')
    we$clickElement()
    Sys.sleep(rest)
    
  } else {
    
    we <- rd$findElement(using = 'xpath', '//*[@id="paginator"]/a[3]')
    we$clickElement()
    Sys.sleep(rest)
    
  }
  
}

到这里,需要的内容就爬取完毕了,不过,既然已经爬到了,还是简单分析一下吧。进行分析前,先简单处理一下:

dog13 <- read.csv('dog13.csv', stringsAsFactors = FALSE) %>% 
  mutate(score = case_when(rank == '力荐' ~ 5, rank == '推荐' ~ 4,
                           rank == '还行' ~ 3, rank == '较差' ~ 2,
                           rank == '很差' ~ 1) %>% as.integer(),
         year = str_sub(time, 1, 4) %>% as.integer())

现在的数据是这样的:

dog13 %>% head() %>% kable()
rank time help text score year
力荐 2018-01-28 11204 “你看,这个孩子好懂事啊。”“你怎么知道她是不是害怕、沉默、妥协呢?” 5 2018
力荐 2016-09-02 8650 原来我们都是这样长大的……或者说,如果没有共鸣,你知不知道你有多幸运…… 5 2016
力荐 2014-02-19 4540 我之所以坚决认为不该要孩子,没有别的原因,仅仅是因为我实在没有把握让一个充满灵气的生命不被“成长”为一个认为《时间简史》只是给孩子看的、用一个谎言去圆另一个谎言的大人。PS:这应该就是今年你能看到的最好的华语电影 5 2014
还行 2013-10-16 4335 狗是引题,13是成长,狗B是伪善的成人世界。家长不知教育孩子的根本,而拿狗出气,这就是中国式教育的最大问题。新狗代替老狗去死,女孩意识到狗狗回来受罪不如在外人那好吃好伺候着,这是她向成人世界的妥协也是她逐渐迈向成人的成长。 3 2013
力荐 2018-02-21 3896 李玩的名字是胡乱取的,弟弟的名字是认真取的,因为“男孩的名字不能太随便了”。 继母随便找只狗来敷衍李玩,爸爸还强迫她承认这就是爱因斯坦。 是继母买来了新狗,也是继母要把新狗卖了。狗只是畜生,利用完了就可以扔了。 李玩推倒了爷爷,遭到了爸爸的暴打;弟弟打了奶奶,爸爸却反过来哄弟弟。 弟弟挑衅新狗在先,爸爸却去打新狗。 李玩永远是错的,弟弟永远是对的。 于是,李玩变得“成熟懂事”了,她会不让爸爸难堪,忍痛吃狗肉;她会为了不让爱因斯坦跟她受苦,忍痛离开。 但我还是想念最真实的她,希望她能做回自己。 我们人人都是李玩,被生活磨平了棱角,失去了个性,扔掉了脾气,忘记了初心,还美其名曰“这就是成长”。 我们“提高了”情商,“学会了”做违心的事,说违心的话,再也不遵循自己的内心。 狗永远是狗,人有时却不是人。 5 2018
推荐 2018-12-02 3016 这毕竟还是大城市的较为体面的家庭的故事了,女孩要在父亲面前挑衅地吹啤酒瓶才会挨一顿打,打完还能得到道歉与补偿。在我们十八线小城市的版本里,女孩准备出门找狗的时候就已经可以赢得两记耳光了,没有发出尖叫的机会,没有摔门摔碗的机会,只能把头深深地埋进被窝里无声哭一场,第二天起来,就长大了。 4 2018

看看愿意花时间写短评的人们对该片的评分:

mean(dog13$score, na.rm = TRUE) %>% round(1) * 2
## [1] 7.6

比主页面上的8.2分好像低不少的。

看看评分随时间的变化:

dog13 %>% group_by(year) %>% 
  summarise(n = n(), 
            score = mean(score, na.rm = TRUE) %>% round(1) * 2) %>% 
  select(年份 = 1, 评价数 = 2, 分数 = 3) %>% 
  kable()
年份 评价数 分数
2013 35 7.8
2014 17 8.2
2015 6 8.4
2016 26 8.0
2017 6 7.6
2018 408 7.6
2019 2 6.0

评分随时间先增后减,短评主要集中在解禁后的18年。

最后画个词云吧:

worker <- worker(stop_word = 'stopwords_cn.txt')

dog13_overall <- str_c(dog13$text, collapse = '')

dog13_word_overall <- worker[dog13_overall]

dog13_word_overall_freq <- dog13_word_overall %>% 
  table() %>% 
  as.tibble() %>% 
  select(word = 1, freq = 2) %>%
  filter(!str_detect(word, '(\\d+)|([A-Za-z]+)|(\\s+)')) %>% 
  filter(str_length(word) > 1) %>% 
  arrange(-freq) %>% 
  filter(freq > 20)

wordcloud2(dog13_word_overall_freq, size = .6)

这部电影的主题应该很明显了。

P.S.再附上另一种不知道该如何进一步使用的模拟登录的方法:

url <- 'https://accounts.douban.com/login?source=movie'
login <- html_session(url)
form <- html_form(login)[[1]]

filled_form <- set_values(form,
                          'form_email' = '用户名',
                          'form_password' = '密码')

submit <- submit_form(login, filled_form)