用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)
最终结果如下:
数据分析咨询请扫描二维码
《Python数据分析极简入门》 第2节 7 Pandas分组聚合 分组聚合(group by)顾名思义就是分2步: 先分组:根据某列数据的值进行 ...
2024-11-25数据分析需要学习的内容非常广泛,涵盖了从理论知识到实际技能的多个方面。以下是数据分析所需学习的主要内容: 数学和统计学 ...
2024-11-24数据分析师需要具备一系列多方面的技能和能力,以应对复杂的数据分析任务和业务需求。以下是数据分析师所需的主要能力: 统计 ...
2024-11-24数据分析师需要学习的课程内容非常广泛,涵盖了从基础理论到实际应用的多个方面。以下是根据我搜索到的资料整理出的数据分析师需 ...
2024-11-24《Python数据分析极简入门》 第2节 6 Pandas合并连接 在pandas中,有多种方法可以合并和拼接数据。常见的方法包括append()、conc ...
2024-11-24《Python数据分析极简入门》 第2节 5 Pandas数学计算 importpandasaspdd=np.array([[81,&n ...
2024-11-23数据分析涉及多个方面的学习,包括理论知识和实践技能。以下是数据分析需要学习的主要方面: 基础知识: 数据分析的基本概念 ...
2024-11-22数据分析适合在多个单位工作,包括但不限于以下领域: 金融行业:金融行业对数据分析人才的需求非常大,数据分析师可以从事经 ...
2024-11-22数据分析是一种涉及从大量数据中提取有用信息和洞察力的过程。其工作内容主要包括以下几个方面: 数据收集与整理:数据分析师 ...
2024-11-22数据分析师需要掌握多种技能,以确保能够有效地处理和分析数据,并为业务决策提供支持。以下是数据分析师需要掌握的主要技能: ...
2024-11-22数据开发和数据分析是两个密切相关但又有所区别的领域。以下是它们的主要区别: 定义和目标: 数据开发:数据开发涉及数据的 ...
2024-11-22数据架构师是负责设计和管理企业数据架构的关键角色,其职责涵盖了多个方面,包括数据治理、数据模型设计、数据仓库构建、数据安 ...
2024-11-22数据分析师需要具备一系列技能,以确保能够有效地处理、分析和解释数据,从而支持决策制定。以下是数据分析师所需的关键技能: ...
2024-11-22数据分析师需要具备一系列技能,以确保能够有效地处理、分析和解释数据,从而支持决策制定。以下是数据分析师所需的关键技能: ...
2024-11-22数据分析师需要具备一系列的技能和能力,以确保能够有效地处理、分析和解释数据,从而支持业务决策。以下是数据分析师所需的主要 ...
2024-11-22需求持续增长 - 未来数据分析师需求将持续上升,企业对数据驱动决策的依赖加深。 - 预测到2025年,中国将需要高达220万的数据人 ...
2024-11-22《Python数据分析极简入门》 第2节 4 Pandas条件查询 在pandas中,可以使用条件筛选来选择满足特定条件的数据 importpanda ...
2024-11-22数据分析师的工作内容涉及多个方面,主要包括数据的收集、整理、分析和可视化,以支持商业决策和问题解决。以下是数据分析师的一 ...
2024-11-21数据分析师必须掌握的技能可以从多个方面进行归纳和总结。以下是数据分析师需要具备的主要技能: 统计学基础:数据分析师需要 ...
2024-11-21数据分析入门的难易程度因人而异,总体来看,入门并不算特别困难,但需要一定的学习和实践积累。 入门难度:数据分析入门相对 ...
2024-11-21