京公网安备 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-10-08随着市场竞争日趋饱和,同质化低价竞争逐渐陷入内卷僵局,传统以价格、渠道、促销为核心的营销模式边际效益持续递减。在此背景下 ...
2026-10-08 很多数据分析师画过趋势图、做过业绩预测,但当被问到“这个月销售额增长20%,到底是长期趋势自然增长,还是促销活动的短期 ...
2026-10-08你有没有想过,手机里点外卖、刷社交软件、转一笔账,背后到底是谁在替你"记着账"? 答案其实很简单:数据库,以及跟它对话的那 ...
2026-10-07CDA数据分析师 出品 作者:李诗怡 一、数据分析四大思维 1. 对比思维:没有对比就没有分析 核心观点:单独一个数字没有意义,有 ...
2026-10-05Kimball 是方法,星型模型是它产出的形状。 很多人把"Kimball vs 星型模型"当成一道选择题——这本身就是个误会:Kimball 是动词 ...
2026-10-05写在开头 老板在微信上甩来一句: "帮我看下为什么销量跌了。" ” 你回工位,打开 SQL,开始写。查订单表、拉近三个月、按 ...
2026-10-03CDA数据分析师 出品 作者:李诗怡 1. 事实表 vs 维度表 对比维度 事实表 维度表 核心问题 记录“业务发生了什么事” 描述 ...
2026-10-02做数据聚合时,PySpark的groupBy()确实能完成统计,这也是它的本职工作。但它有一个根本性局限:每一组数据,最终只能返回一行 ...
2026-10-01热力地图是数据可视化中极具辨识度与实用性的空间分析图表,结合地理空间维度与数据密度特征,通过颜色深浅、色阶渐变直观展示数 ...
2026-09-30 很多数据分析师做过按月份的销售额趋势图,画过按天的流量折线图,但当被问到“时间序列和普通数据有什么本质区别”“季节性 ...
2026-09-30同样是“银行数据岗”,在国有大行总行数据中心、在一家城商行的零售部、在银行系金融科技子公司、在保险公司,工作内容、成长节 ...
2026-09-29在数据分析与统计学研究中,数据往往不是独立存在的,不同变量之间普遍存在相互关联、相互影响的关系。相关性统计分析是挖掘变量 ...
2026-09-29 导读:大多数人只把 dataclasses 当成偷懒工具,用来少写 __init__、__repr__ 这类魔法方法。但它的能力远不止于此。本文带 ...
2026-09-29 很多数据分析师能熟练地计算指标、搭建标签体系,但当被问到“画像到底在解决什么问题”“画像和标签是什么关系”“画像如何 ...
2026-09-29在MySQL数据库运维与业务开发中,行业普遍存在“数据达到千万级就必须分表”的说法。但在实际生产环境中,千万条数据并不是强制 ...
2026-09-28CDA数据分析师 出品 作者:李诗怡 1. 5W1H 分析法 定义:经典系统性思维框架,通过六个核心维度对问题进行全方位拆解与剖析,确 ...
2026-09-28 很多分析师在设计标签时思路清晰,但真到落地环节却面临“数据在手,不知如何转化为可用标签”的困境:或因加工方式选择不当 ...
2026-09-28CDA数据分析师 出品 作者:李诗怡 1. 用户标签体系 定义: 通过一系列高度精炼的特征标识,对用户属性、行为与偏好进行量化刻画 ...
2026-09-24Pandas是Python生态中用于表格数据处理的核心库,广泛应用于数据清洗、统计运算、报表输出、数据分析建模等场景。在处理极大数值 ...
2026-09-24