京公网安备 11010802034615号
经营许可证编号:京B2-20210330
用R语言做网页爬虫和文本分析
受到这篇情感分析的文章和这篇网页爬虫指南的双重启发,我决定尝试抓取并分析 Goodreads 网站的书评数据。这个项目将会呈现一个从数据收集到机器学习建模分析的完整案例,我在中途犯下的错误也会一并呈现。本文将以5本流行的爱情故事书的评论为研究对象,我很自觉地选了同类型的书,使得评论具有可比性。这五本书也足够畅销,我们可以轻松获取上千条评论,如果你不喜欢爱情故事,你也可以选择其他类型的书做研究。
为了使这篇文章更易读,我把它分成了三个部分:
Part 1: 网页抓取
Part 2: 探索性数据分析和情感分析
Part 3: 基于机器学习的预测分析
这篇文章更新到了Part 1,后续部分会持续更新。
Part 1 网页抓取
Goodreads上的评论很容易抓取,在每条评论左侧都有一个非本文类型的排名变量。然而评论页面的切换是通过一个javascript按钮而不是html链接来实现的,处理起来有一点难度。不过好在这个问题有一个简单有效的解决方法,只要使用RSelenium包就可以了,点 这里 可以阅读该包的小品文。
起步
让我们先加载好要用的包并定义几个变量
library(data.table) # 为了rbindlist函数
library(dplyr) # 为了数据整理
library(magrittr) # 为了管道操作符 %>%
library(rvest) # 为了read_html函数
library(RSelenium) # 为了使用JavaScript进行网页抓取
url <- "https://www.goodreads.com/book/show/18619684-the-time-traveler-s-wife#other_reviews"
book.title <- "The time traveler's wife"
output.filename <- "GR_TimeTravelersWife.csv"
请注意,对每本书而言我需要改变上述变量的值并重新运行脚本。如果你觉得麻烦,可以用代码自动实现这个过程,但此处我就采取手动的做法。这么做也可以避免Goodreads'网站服务器过载。
让我们启动RSelenium服务器,利用Firefox浏览器可能会有些问题,为此我重新安装了一个比较旧的版本
startServer()
remDr <- remoteDriver(browserName = "firefox", port = 4444) # instantiate remote driver to connect to Selenium Server
remDr$open() # 打开浏览器
remDr$navigate(url)
这些指令会打开浏览器并转向你制定好的url,之后我们需要建立一个数据框,方便后续数据的操作。
global.df <- data.frame(book=character(),
reviewer = character(),
rating = character(),
review = character(),
stringsAsFactors = F)
现在万事俱备,可以开始网页抓取了。
网页抓取流程
为了提取我们需要的内容,对于每本书,我们将扫描其100页的评论。这里我去掉了循环,只扫描一页的内容,并对代码的工作原理逐行解释。
首先,我们需要定义书评在页面中的位置。使用SelectorGadget就能完成这一步骤,利用Chrome的一个拓展能帮助你识别CSS selector。只要找到了正确的CSS selector(这里是#bookReviews.stacked),将其传递给RSelenium服务器的findElements函数就可以了。
reviews <- remDr$findElements("css selector", "#bookReviews .stacked")
我们把书评的html代码先提取出来,然后再分离其中的内容。
reviews.html <- lapply(reviews, function(x){x$getElementAttribute("outerHTML")[[1]]})
reviews.list <- lapply(reviews.html, function(x){read_html(x) %>% html_text()} )
reviews.text <- unlist(reviews.list)
现在我们已经用list的格式保存了评论,然而其中依旧混杂着很多无关内容,我们需要利用正则表达式(regex)来清洗数据。
利用正则表达式清洗数据
依照我的文本分析经验,正则表达式既是天使也是魔鬼。通过它你可以用一行短短的命令就把字符串中所有的非字母元素移除,可它本身也是一门晦涩难懂的语言,使你在重读自己的代码时会倍感艰辛。所以如果你能读懂下面的代码做了些什么,我会倍感欣慰。
# 移除字母和符号外的元素
reviews.text2 <- gsub("[^A-Za-z\\-]|\\.+", " ", reviews.text)
# 移除换行符和多余的空格
reviews.clean <- gsub("\n|[ \t]+", " ", reviews.text2)
关于正则表达式,下面几个网址很有用:
http://www.regular-expressions.info/
http://stat545.com/block022_regular-expression.html
https://stat.ethz.ch/R-manual/R-devel/library/base/html/regex.html
用表格格式存储评论数据
现在我们已经得到了很干净的评论数据,然而由于html暗含的数据结构,我们会遇到这样的问题:对每一条评论,评论者的姓名和评分会存在同一个字符串里,而评论内容存在后一个字符串中。此外,预览评论系统会使得每个评论的开头在字符串中重复出现两次。我们需要对这些做做处理,再次使用正则表达式,我们将得到表格格式的数据。
我们先数数一共有多少条评论(即字符串数量的一半),然后建立一个临时数据框来存储数据。
n <- floor(length(reviews)/2)
reviews.df <- data.frame(book = character(n),
reviewer = character(n),
rating = character(n),
review = character(n),
stringsAsFactors = F)
我们遍历所有的字符串,逐评论地提取需要的内容并存在数据框里。这里我们采用for循环实现遍历,但如果是工业级应用,你应该更喜欢向量化处理。
下面的代码可能有点难懂,我先来解释下:
1. 第一部分,我先列举了可能出现在评论人姓名和评分之间的一些表达式,再结合正则表达式来确定姓名的结束位置,以此提取姓名。
2. 第二部分,我列举了可以出现在评分之后的表达式,有时这些表达式并不会出现,因此我得把这一情况也考虑进去。通过这两种方式,我们就可以提取评分了。
3. 第三部分,我把每个评论的开头移除了,我会记录50个字符重复出现的起始位置和结束位置。有时,评论可能篇幅较短还不到50字符,这里就用和第二部分相似的方法处理。
4. 最后,请注意这个循环的结构,我并没有一一循环字符串,而是遍历了评论,每个评论包含两个字符串,因此用2*j和2*j-1索引。
for(j in 1:n){
reviews.df$book[j] <- book.title
# 提取评论人姓名
auth.rat.sep <- regexpr(" rated it | marked it | added it ",
reviews.clean[2*j-1])
reviews.df$reviewer[j] <- substr(reviews.clean[2*j-1], 5, auth.rat.sep-1)
# 提取评分
rat.end <- regexpr("· | Shelves| Recommend| review of another edition",
reviews.clean[2*j-1])
if (rat.end==-1){rat.end <- nchar(reviews.clean[2*j-1])}
reviews.df$rating[j] <- substr(reviews.clean[2*j-1], auth.rat.sep+10, rat.end-1)
# 移除评论中重复的部分
short.str <- substr(reviews.clean[2*j], 1, 50)
rev.start <- unlist(gregexpr(short.str, reviews.clean[2*j]))[2]
if (is.na(rev.start)){rev.start <- 1}
rev.end <- regexpr("\\.+more|Blog", reviews.clean[2*j])
if (rev.end==-1){rev.end <- nchar(reviews.clean[2*j])}
reviews.df$review[j] <- substr(reviews.clean[2*j], rev.start, rev.end-1)
}
现在我们的临时数据框已经填写完毕,我们可以把它的内容转移到主数据框中了。
global.lst <- list(global.df, reviews.df)
global.df <- rbindlist(global.lst)
最后,我们需要告诉RSelenium点击进入下一页的按钮,通过传递利用SelectorGadget定义CSS selector可以实现这个功能。同时,Relenium的效率比较低,可能在循环中不能及时响应,因此我们在每个循环的末尾让R等待3秒。
NextPageButton <- remDr$findElement("css selector", ".next_page")
NextPageButton$clickElement()
Sys.sleep(3)
结束所有循环后,我们要把最终结果保存成一个文件。
write.csv(global.df,output.filename)
最终结果如下:
数据分析咨询请扫描二维码
若不方便扫码,搜微信号:CDAshujufenxi
初中毕业想考证,先确认“我能不能报”,再比较证书价值。学历门槛不够的证书,宣传再多也与你无关;能报但对应不上岗位的证书, ...
2026-11-02市场岗确有一批报考不设专业限制的证书,但“不限专业”只解决能不能报,真正决定值不值的是三件事:对应市场岗哪条子路径、招聘 ...
2026-11-02“大学生必考证书有哪些”这个问题,本身就藏着一个需要先拆掉的假设:似乎存在一份人人适用的必考清单。实际上,除了少数与毕业 ...
2026-11-02“最好考的一级证书”这个问题,最容易踩的坑不是选错证书,而是没先弄清“一级”在目标证书体系里到底指什么。有的体系里,一级 ...
2026-11-02“职业证书哪个含金量高”这个问题,很难用一张榜单回答。原因不复杂:证书的“含金量”取决于你拿它做什么。想进入必须持证的行 ...
2026-11-02“证件含金量排名”是搜索量很高的词,但直接给你一份“十大证书排行榜”的文章,基本可以跳过。原因很简单:证书的含金量不是单 ...
2026-11-02在校大专生能考的证书,大体分三类:通用能力类(英语等级、计算机等级、普通话水平测试)、职业技能等级类,以及部分不要求工作 ...
2026-11-02在家闲着想考个证书,最容易踩的坑是先去搜“含金量排行榜”,然后被一堆培训广告牵着走。更稳妥的顺序是反过来的:先看自己属于 ...
2026-11-02“注册类证书”不是一个可以随手列全的清单,它更像一个制度口径:通常指国家职业资格目录中设有注册管理、与执业准入挂钩的资格 ...
2026-11-02“现在考什么证有用”很难有统一答案。同一张证书,对想进会计事务所的人有用,对想做新媒体运营的人可能帮助有限;对需要评职称 ...
2026-11-02“适合女生考的证书有哪些含金量高”,这个问题里藏着两个容易走偏的前提。一是把“女生”当成职业属性,仿佛有一份统一的女性证 ...
2026-11-02先说结论:工商管理专业女生不必追“女生必考”榜单。证书有没有用,取决于它能不能对上你准备投的岗位。比较稳妥的做法,是先在 ...
2026-11-02“什么证好考又实用”这个问题,最怕两种回答:一种是列一堆“通过率高”的证书,却不告诉你考完能干什么;另一种是反过来只谈含 ...
2026-11-02“女士可以考哪些证书”并没有一份统一答案。真正决定你能考什么的,不是性别,而是三件事:你现在的身份(在校、在职还是准备转 ...
2026-11-02大专学历想考运营岗位证书,先给结论:能报的证书不少,但值得考的集中在三类——平台官方运营/投放认证、职业技能等级评价、数 ...
2026-11-02大学生判断一张证书是否重要,不用先看宣传热度,而要看四件事:它是否匹配你的目标岗位,校招时是否真用得上,在校期间是否具备 ...
2026-11-02先把结论说清楚:在校期间以2—4张核心证书为宜,其中1—2张通用底证、1—2张与目标岗位对应的方向证;高壁垒资格证只在符合报考 ...
2026-11-02大学生问证书推荐,最容易掉进的坑是“别人考什么我也考什么”。证书本身没有统一的含金量排序,同一张证,对目标岗位是硬门槛, ...
2026-11-02经管毕业生没有一张“通吃”的证书。比较有用的选择,是先按财会、金融、市场、人力、综合管理几个方向锁定目标岗位,再把证书分 ...
2026-11-02“程序员考什么证书含金量高”,没有放之四海皆准的答案。同样是“高含金量”,对后端开发、云原生工程师、数据开发和安全工程师 ...
2026-11-02