京公网安备 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
在MySQL数据库数据查询与数据分析中,GROUP BY与ORDER BY是使用频率极高的核心关键字。二者语法结构相似,常搭配使用,但核心功 ...
2026-09-14随着数字化治理、智慧运营、数字孪生技术的普及,数字体征成为衡量业务状态、系统运行、城市治理与企业经营健康度的核心体系。数 ...
2026-09-14 很多数据分析师沉迷于复杂的模型和算法,却忽略了数据分析的一项基础能力——描述性统计。事实上,大量商业分析问题,用描述 ...
2026-09-14在MySQL数据库运维与开发实践中,经常出现一种典型现象:数据库实际存储的数据量很小,数据表条数少、文件体积低,但服务器整体 ...
2026-09-11 很多数据分析师能熟练计算均值、标准差,但当被问到“总体和样本有什么区别”“参数和统计量有什么关系”“数据级别的高低如 ...
2026-09-11CDA数据分析师 出品 作者:李诗怡 定义: 将同一时间段内因具备相同属性或共同经历的用户划分为群体,分析其留存与生命周期价值 ...
2026-09-11在零售、商超、餐饮、线下门店等实体商业运营中,客流与销售额是衡量门店经营状态的两大核心指标。销售额是门店经营的最终结果, ...
2026-09-10在数据可视化体系中,柱形图是最基础、应用最广泛的图表类型,其中**累计柱形图(堆积柱状图)**是兼顾整体总量与内部结构的核心 ...
2026-09-10 许多数据分析师精通Excel函数和SQL查询,但当面对一张上万行的销售明细表,要快速回答“哪个地区销量最高”“哪款产品增长最 ...
2026-09-10在Python Pandas数据分析中,DataFrame是承载结构化数据的核心载体,数据清洗、数据修正、条件赋值、字段更新等实操场景,都离不 ...
2026-09-09 很多数据分析师掌握了Excel函数、会写SQL查询,但当被问到“数据从哪里来”“数据加工有哪些步骤”“如何使用分析工具连接数 ...
2026-09-09卡方检验(Chi-Square Test)是统计学中针对分类数据的经典显著性检验方法,核心用于判断两个离散分类变量是否相互独立、数据实 ...
2026-09-09CDA数据分析师 出品 作者:李诗怡 1. 销售漏斗阶段判断 题目:销售漏斗模型中,通过广告、社交媒体等方式触达品牌信息(如浏览品 ...
2026-09-07在Python数据分析中,Pandas库的DataFrame是最核心、最常用的结构化数据表对象,类似于Excel的二维表格,具备规整的行列结构、字 ...
2026-09-07在数据分析、经营复盘、业绩预测与经济统计工作中,平均增速(平均增长率)是衡量数据长期变化趋势、业务发展快慢的核心指标。不 ...
2026-09-07 很多数据分析师精通Excel单元格操作,但当被问到“表结构数据的基本处理单位是什么”“字段和记录的本质区别”“为什么表结 ...
2026-09-07随着大数据技术的快速发展,商业竞争逐步从传统的经验式经营转变为数据驱动的精细化运营。海量的用户行为数据、交易数据、运营数 ...
2026-09-04CDA数据分析师 出品 作者:李诗怡 1. 波士顿矩阵(BCG Matrix) 定义: BCG于1970年提出的业务组合分析工具,以"市场增长率"(纵 ...
2026-09-04 数据分析师八成以上的时间在和数据表格打交道,但许多人拿到Excel后习惯性地先算、先分析,结果回头发现漏了一列关键数据, ...
2026-09-04数据透视表是Excel与Power BI中最核心的数据分析工具,具备快速汇总、维度拆分、动态筛选的能力,可高效完成数据归类与统计展示 ...
2026-09-03