商品评论获取与词云图可视化分析
商品评论获取解析与可视化词云图制作
本文旨在向读者展示如何利用手工编写的小程序完成电商平台评论数据的抓取与分析过程,并通过这些分析结果将收集到的商品评价信息导入MySQL数据库中存储起来,并对整理后的数据进行频率统计并生成相应的词云图表以便直观展示高频关键词。
- 电商网站页面分析
- 基于Python实现简单的爬虫程序
- 使用Java语言开发的基于WebCollector的爬虫框架
- 分别采用Python和Java进行JSON文件解析
- 其中Java版本结合MapReduce算法进行处理
- 通过MySQL数据库完成数据爬取并导入系统中
- 对收集到的数据执行清洗处理流程
- 采用MapReduce算法统计关键词频率
- 使用Hive平台完成关键词频率统计工作
- 生成词云图用于数据可视化展示
- 使用ECharts工具制作完整的词云图展示界面

博主由于热爱色彩鲜艳的事物,在她的绘画作品中也常常会加入这种风格的作品。
电商网站页面分析
这里需要注意的是我们对网站进行了深入研究以确定爬虫在其中的活动范围和影响方式。此前我也曾参与过相关领域的探索当时由于对数据可视化表现出了浓厚兴趣便专门研究了这一领域并完成了关于新冠肺炎疫情的数据可视化项目从中积累了宝贵的经验。为了获取所需数据我的第一反应就是利用浏览器开发者工具快速定位目标页面并查看其中的关键信息这个过程在互联网上已有许多相关介绍但我就不赘述了如果仅凭这些途径无法获得有效信息比如无法从某些平台(如某宝)找到相关信息时那么才会转而考虑直接提取网页源代码并通过解析手段获取所需数据



值得注意的是这种情况是最简单的情形之一即评论信息以JSON格式存储较为简便然而在实际应用中并非如此顺利因此需要从原始网页中提取相关信息相对而言较为复杂涉及HTML文档的解析技术其中XPath用来提取信息可能更为繁琐如需了解更多信息可参考其他优质博客资源由于时间和精力限制此处不做进一步阐述
Python简单爬虫加json文件解析与mysql数据库存储
这个由于项目中的各个模块被整合到一起,并且整体规模较小。可以直接粘贴源代码,并在其中添加注释说明的方式让读者一目了然。
import json
import time
import pymysql
import requests
class MySpider:
def __init__(self):
self.urls1 = ["审核通不过具体地址请私信"
.format(i) for i in range(50)]
self.urls2 = ["审核通不过具体地址请私信".format(i)
for i in range(50)] # url
self.headers = {"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.53"} # 请求头这个最好加上不然很容易出现爬取不到内容的情况,如果出现不妨缓缓或者更换请求头
self.fileName = []
self.conn = pymysql.Connect(
host='127.0.0.1',
port=3360, #这个是博主本地的mysql数据库,也可以不在这里连接大家随意按照自己需求更改
user='root', # 这里的具体配置根据您的数据库来
password='*******',
database='my_db',
)
self.cursor = self.conn.cursor()
self.fileListName = "dataNameList.txt" # 将爬取的文件名存在一个文件里这样在解析的时候不再需要重新爬取并不必要
def parse_url(self):
# 爬取网站数据
i = 0
urls = self.urls1+self.urls2
for url in urls:
response = requests.get(url,headers=self.headers)
time.sleep(2)
filename = "E:\SpiderData\commentWithoutProcessing"+str(i)+".json"
i += 1
with open(self.fileListName,'a') as f:
f.write(filename+";")
with open(filename,'w') as f:
text = response.text.split(')')[0] # 将非json部分过滤掉
text = text.split('(')[1]
f.write(text)
print("########数据获取结束######")
def parse_data(self):
# 对爬取的数据进行解析
with open(self.fileListName,'r') as f:
nameAll = f.read()
self.fileName = nameAll.split(';')
i = 0
# 存储地址可指定
filenames = ["E:\SpiderData\commentWithoutProcessing{}.json".format(i) for i in range(100)]
sql = """create table comments(
ID bigint,
Contents Text,
Score int ,
Type int)ENGINE=innodb DEFAULT CHARSET=utf8"""
self.cursor.execute(sql)
for filename in filenames:
if i<50:
type = 1
else:
type = 2
i += 1
with open(filename,'r') as f:
try: # 这里加入try-except来处理文件解析失败的异常亲测会出现异常
# 大家可以试试这与我对爬取到的文件的处理方式有关,我的处理方式会出现一些文件只剩一般导致json解析失败因此加入这样的异常处理,如果大家有更好的想法我将不胜感激!!!
data = json.loads(f.read())
print("开始解析" + filename + "文件,存入数据库...........")
# sql = 'insert into "comments"("ID","Contents","Score") values (,%(Contents)s,%(Score)s)'
for info in data['comments']:
id = int(info['id'])
content = info['content']
score = int(info['score'])
information = (id, content, score,type)
sql = "INSERT INTO `comments`(`ID`,`Contents`,`Score`,`Type`) VALUES (%s,%s,%s,%s)"
rows = self.cursor.execute(sql, information)
self.conn.commit()
print(filename + "解析结束......")
except:
print("解析失败!"+filename)
self.conn.close()
if __name__ == "__main__":
spider = MySpider()
cpp

这里展示一下结果:

这里还涉及利用pymysql操作mysql的知识,这里的坑也很多,mysql经常崩溃,博主也修改了很久,要注意mysql的引号是反过来的·!!!大家可以查找相关资料
这里如果是windows的话关机后mysql会关闭,要重新开启有个笨方法:

博主水平有限,在此代码的基础上可能不太容易理解哦!大家都可以以此为基础进行修改完善呢?还有就是博主已测试可获取数据哦!如果您遇到问题无法运行,请您考虑调整请求头设置!同时博主水平有限如果有错误还望批评指正!非常感谢您的指正!
java语言的webCollector爬虫框架使用
这个框架还是我之前从未接触过的初次体验。
> <dependency>
> <groupId>cn.edu.hfut.dmic.webcollector</groupId>
> <artifactId>WebCollector</artifactId>
> <version>2.73-alpha</version>
> </dependency>
>
>
>
>
>
>
>
>
>
>
>
引用一些关于这个爬虫框架组件的简单介绍:
Crawler
整合了完整的采集流程以及所有可扩展的插件槽位。Crawler通过其核心框架提供高度定制化爬虫功能的可能性。DBManager
提供了数据管理模块的基础接口,在高并发场景中自动处理URL去重问题(无需手动配置去重逻辑)。通过插件机制支持多种数据库解决方案(如伯克利DB或RocksDB)。Visitor
设计用于自定义用户在每个页面的行为模式,默认包括页面内容抽取以及新链接发现功能。Fetcher
实现了高效的抓取调度算法,在有限内存资源下支持数十亿级别网页的并行抓取操作。Requester
负责发起HTTP请求并接收响应流。支持自定义各种HTTP头参数配置以及代理设置等高级功能。Plugin
系统内置多种组件均提供插件化扩展接口,默认情况下即可实现高度定制化的爬虫构建过程。
如需更多信息,请访问 GitHub 的 webCollector 主页 https://github.com/CrawlScript/WebCollector
package my.webcollector;
import java.io.*;
import okhttp3.Request;
import cn.edu.hfut.dmic.webcollector.model.CrawlDatum;
import cn.edu.hfut.dmic.webcollector.model.CrawlDatums;
import cn.edu.hfut.dmic.webcollector.model.Page;
import cn.edu.hfut.dmic.webcollector.plugin.berkeley.BreadthCrawler;
import cn.edu.hfut.dmic.webcollector.plugin.net.OkHttpRequester;
public class JDCommentCrawler extends BreadthCrawler {
public JDCommentCrawler(String crawlPath) {
// 第二个参数表示不需要自动探测URL
super(crawlPath, false);
// 设置线程数为1
setThreads(1);
// 添加种子(评论API对应的URL,这里翻页10次)
for (int pageIndex = 0; pageIndex < 100; pageIndex++) {
String seedUrl = String
.format("具体地址请私信",
pageIndex);
// 在添加种子的同时,记录对应的页号
addSeedAndReturn(seedUrl).meta("pageIndex", pageIndex);
}
}
@Override
public void visit(Page page, CrawlDatums crawlDatums) {
// 模拟人访问网页的速度
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取之前保存的页号信息
int pageIndex = page.metaAsInt("pageIndex");
String body = page.html();
// 保存当前访问的productPageComments页面信息
JDCommentCrawler.createFile(body, "D:\ BigDataWordCloud\ src\ htmlTexts\ 10038246700670-page"
+ pageIndex + ".html");
}
/** * 将字符串保存到文件
*/
public static boolean createFile(String content, String filePath) {
// 标记文件生成是否成功
boolean flag = true;
try {
// 保证创建一个新文件
File file = new File(filePath);
if (!file.getParentFile().exists()) { // 如果父目录不存在,创建父目录
file.getParentFile().mkdirs();
}
if (file.exists()) { // 如果已存在,删除旧文件
file.delete();
}
file.createNewFile();
// 将格式化后的字符串写入文件
Writer write = new OutputStreamWriter(new FileOutputStream(file),
"UTF-8");
write.write(content);
write.flush();
write.close();
} catch (Exception e) {
flag = false;
e.printStackTrace();
}
return flag;
}
/** * 模拟普通用户使用浏览器访问
*/
public static class MyRequester extends OkHttpRequester {
String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36 Edg/102.0.1245.33";
// 每次发送请求前都会执行这个方法来构建请求
@Override
public Request.Builder createRequestBuilder(CrawlDatum crawlDatum) {
// 这里使用的是OkHttp中的Request.Builder
// 可以参考OkHttp的文档来修改请求头
return super.createRequestBuilder(crawlDatum)
.removeHeader("User-Agent") //移除默认的UserAgent
.addHeader("Referer", "https://item.jd.com/")
.addHeader("User-Agent", userAgent);
}
}
public static void main(String[] args) throws Exception {
// 实例化一个评论爬虫,并设置临时文件夹为crawl
JDCommentCrawler crawler = new JDCommentCrawler("crawl");
// 抓取1层
crawler.setRequester(new MyRequester()); // 设置请求头
crawler.start(1);
}
}
cpp

这样就可以爬到结果如果内容为空可以修改请求头。
利用mapreduce解析大量文件
这里涉及对刚刚通过webcollector爬取到的HTML文件进行JSON解析操作,并提取其中的数据信息以实现数据转换的目的。Python脚本已经完成过初步处理并输出相应结果。具体流程如图所示:

这里的代码MapReduce框架的实现相对容易理解;然而真正运行起来则需要您自行搭建环境配置;因此建议小心尝试。
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import java.io.IOException;
public class MRDataClean4JDComments {
public static void main(String[] args) {
try {
Job job = Job.getInstance();
job.setJobName("MRDataClean4JDComments");
job.setJarByClass(MRDataClean4JDComments.class);
job.setMapperClass(doMapper.class);
// job.setReducerClass(doReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
Path in = new Path("hdfs://:8020/input/newHTML");
Path out = new Path("hdfs://hadoop102:8020/out/newOutput");
FileInputFormat.addInputPath(job, in);
FileOutputFormat.setOutputPath(job, out);
try {
System.exit(job.waitForCompletion(true) ? 0 : 1);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static class doMapper extends Mapper<Object, Text, Text, Text> {
@Override
protected void map(Object key, Text value, Mapper<Object, Text, Text, Text>.Context context) throws IOException, InterruptedException {
String initJsonString = value.toString();
JSONObject initJson = JSONObject.parseObject(initJsonString);
if (!initJsonString.contains("productCommentSummary") && !initJsonString.contains("comments")) {
// 过滤掉不符合要求的,这里其实打开文件就会发现文件只有一行
return;
}
JSONObject myjson = initJson.getJSONObject("ten");
JSONObject productCommentSummary = myjson.getJSONObject("productCommentSummary");
String productId = productCommentSummary.get("productId").toString();
String commentCount = productCommentSummary.get("commentCount").toString();
String goodCount = productCommentSummary.get("goodCount").toString();
String generalCount = productCommentSummary.get("generalCount").toString();
String poorCount = productCommentSummary.get("poorCount").toString();
String goodRateShow = productCommentSummary.get("goodRateShow").toString();
String generalRateShow = productCommentSummary.get("generalRateShow").toString();
String poorRateShow = productCommentSummary.get("poorRateShow").toString();
/* comments 包括十条评论 */
JSONArray comments = myjson.getJSONArray("comments");
for (int i = 0; i < comments.size(); i++) {
JSONObject comment = comments.getJSONObject(i);
String guid = comment.getString("guid");
String content = comment.getString("content").replace('\n', ' ');
String creationTime = comment.getString("creationTime");
String score = comment.getString("score");
String nickname = comment.getString("nickname");
String userLevelName = comment.getString("userLevelName");
String userClientShow = comment.getString("userClientShow");
String isMobile = comment.getString("isMobile");
String days = comment.getString("days");
StringBuilder sb = new StringBuilder();
sb.append(productId);
sb.append("\t");
sb.append(commentCount);
sb.append("\t");
sb.append(goodCount);
sb.append("\t");
sb.append(generalCount);
sb.append("\t");
sb.append(poorCount);
sb.append("\t");
sb.append(goodRateShow);
sb.append("\t");
sb.append(generalRateShow);
sb.append("\t");
sb.append(poorRateShow);
sb.append("\t");
sb.append(guid);
sb.append("\t");
sb.append(content);
sb.append("\t");
sb.append(creationTime);
sb.append("\t");
sb.append(score);
sb.append("\t");
sb.append(nickname);
sb.append("\t");
sb.append(userLevelName);
sb.append("\t");
sb.append(userClientShow);
sb.append("\t");
sb.append(isMobile);
sb.append("\t");
sb.append(days);
String result = sb.toString();
context.write(new Text(result), new Text(""));
}
}
}
}
cpp

核心其实很简单。这个方法直接将文件分成片段后再依次处理。由于这里只需分别处理每个切片无需在整合后再调用reduce函数因此可以跳过reduce步骤。这相当于让一个大系统的默认reduce功能自动执行即可。看来博主的理解有误。
java文本分词
我对Java对JSON解析及分词的应用并不熟练。相比而言,在效率上并没有比Python简介得多。也可能是我的个人偏见。同样可以在Hadoop环境下处理。我不推荐在本地处理即可。如果您有兴趣且愿意投入精力的话,请您考虑一下哦!鉴于时间和资源的限制以后有机会再为大家介绍吧!
Python文本分词
在Python语言环境下进行中文文本分词时非常流行的工具是jieba包,在安装过程中非常简便即可完成操作。该工具其使用方法较为传统化且未引入创新技术,默认配置即可满足大部分需求。以下直接上代码:
from typing import List, Any
import pymysql
import jieba
def judge(x):
if len(x) == 1 or x[0] in [',', '。', '!', '?', '了', '啊', '啦', '呀',';']:
return False
return True
db = pymysql.Connect(
host='127.0.0.1',
port=3360,
user='root',
password='******',
database='my_db',
)
cursor = db.cursor()
sql = 'SELECT Contents FROM comments' # 这里是因为我在上面python处理是将评论存储在mysql数据库中因此要访问数据库获取信息。
cursor.execute(sql)
filename = "result.txt"
# 清洗停用词
stopWordsFileName = "stopwords.txt"
f_stop_words_list: list[Any] = []
with open(stopWordsFileName,'r',encoding='utf-8') as f:
f_stop_text = f.read()
f_stop_words_list = f_stop_text.split('\n')
with open(filename, 'w',encoding='utf-8') as f:
for i in range(1000):
contents = cursor.fetchone()[0]
seg_list = jieba.cut(contents)
"""line = " ".join(
seg_list 这样的话单个语气词以及分隔符都放进去了,对其进行清理获取关键词汇,不过清晰的还不够干净
)"""
my_wordList = []
for myword in seg_list:
if judge(myword) and myword not in f_stop_words_list:
my_wordList.append(myword)
line = " ".join(my_wordList)
f.write(line)
cpp

涵盖评论内容的清理工作。
具体来说,则是去除语气词、标点符号以及停用词汇。
其中这个术语属于。
注释部分不变的内容:具体如何清洗看注释即可!!

未经预处理生成的词云图中包含了诸如语气词这类非重点词汇占据了较大比重。

这是清洗以后的词云图明显效果好很多!。
mapreduce统计词频
这个词频统计就很简单因为前面已经得到了相应的文本文件类似下图:

接着将文件上传至HDFS后, 通过调用WordCount程序进行统计计算变得异常简便。关于这一技术细节, 我相信大多数读者都已经掌握过MapReduce的基本实现方法, 因此不再赘述, 请参考官方文档
说明

下面重点介绍hive统计词频。
hive统计词频
Hive是一款基于Hadoop开发的开源数据分析平台软件,在存储于Hadoop文件中的结构化或半结构化数据上实现了文件到数据库表的映射功能,并基于表提供了类似于SQL的数据查询模型(即HQL),该系统可被用来访问并分析存储于Hadoop文件中的大规模数据集。该软件不仅功能强大且安装配置极为简便。

用到的创建表的指令
create table words(string line);
用到的导入文件的指令
load data inpath ‘/comments/newtry.txt’ into table words;
用到的统计词频的语句:
Group words and calculate their frequency.
From a table of words, split each line into individual words, then group them and calculate their frequency.
用到的导出至新表的语句:
创建一个名为word_count的表,并通过以下步骤计算每个单词的出现次数:首先从words表中split每一行line为单词列表并explode;然后将这些单词分组汇总,并按计数排序结果输出。
导出表至hdfs中:
export table word_count to ‘/path’;
下面展示结果







这样就可以啦!
以上就是两种实现词频统计的方式下面就是绘制词云图。
Python利用wordcloud绘制词云图:
Wordcloud是一个用于生成词云图的Python库。直接上代码:
import wordcloud
import matplotlib.pyplot as plt
from imageio import imread
filename = "part-r-00000.txt"
word_dic = {}
with open(filename,'r',encoding='utf-8') as f:
for line in f.readlines(): # hive导出文件这里需要改一下
info = line.strip('\n').split('\t') # 之前数据没清洗干净这里再清洗一下
if info[0][0] not in ['.','-',',','+','*','&','!','/',';','?']:
word_dic[info[0]] = float(info[1])
wordCloud = wordcloud.WordCloud(font_path="STKAITI.TTF",height=1000,width=2000,mode="RGBA",background_color='white',
).fit_words(word_dic)
plt.imshow(wordCloud)
plt.axis('off')
plt.savefig('wordcloud_huawei.png')
cpp

虽然始终坚持清晰直接的特点 也是我喜欢Python的主要原因 正是因为我偏爱简洁高效的语言 这里对字体的要求不容忽视 如果你需要特定的字体 请记得添加对应的TTF文件 可以参考图片找到适合自己的TTF文件 点击后其中都是可以直接复制使用的



java实现词云图制作tomcat+Echarts
准备工作包括下载并安装Tomcat服务器链接,至于具体的配置细节则在此从略。
在IDEA环境下进行配置参考链接是一项繁琐且难以掌握的操作, 最终我还是选择了更为方便的Eclipse工具完成任务。
整个配置流程相对复杂且不易掌握, 但关键点在于将获取的数据成功导入MySQL数据库, 此处提供相应的代码实现:
此乃Java程序的主要类。
import java.io.*;
import java.math.BigInteger;
public class Main {
public static void main(String[] args) {
DBUtil db = new DBUtil();
//更新操作(增加数据)
//Object[] obj = {null, "乔布斯", "2243736958", "Apple", "root"};
//int i = db.update("insert into teacher values(?,?,?,?,?)", obj);
//System.out.println(i);
//db.closeConnection();
//查询操作
String dir = "D:\ java_file\ WordCloud\ src\ main\ java\ 1.txt";
File file = new File(dir);
try {
BufferedReader br = new BufferedReader(new FileReader(file));
Object[] obj = new Object[2];
db.getConnection();
while(true){
try {
String line = br.readLine();
if(br.readLine() == null)
{
break;
}
else{
obj[0] = line.split("\u0001")[0];
obj[1] = Integer.parseInt(line.split("\u0001")[1]);
int i = db.update("insert into word_count values(?,?)", obj);
System.out.print(obj[1]+" ");
}
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
cpp

工具类:
import java.io.FileReader;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
/** * @author Peter Cheung
* @user PerCheung
* @date 2021/8/22 15:11
*/
public class DBUtil {
//连接信息
private static String driverName;
private static String url;
private static String username;
private static String password;
//注册驱动,使用静态块,只需注册一次
static {
//初始化连接信息
Properties properties = new Properties();
try {
properties.load(new FileReader("src/main/db.properties"));
driverName = properties.getProperty("driverName");
url = properties.getProperty("url");
username = properties.getProperty("username");
password = properties.getProperty("password");
} catch (IOException e) {
e.printStackTrace();
}
//1、注册驱动
try {
//通过反射,注册驱动
Class.forName(driverName);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
//jdbc对象
private Connection connection = null;
private PreparedStatement preparedStatement = null;
private ResultSet resultSet = null;
//获取连接
public void getConnection() {
try {
//2、建立连接
connection = DriverManager.getConnection(url, username, password);
} catch (SQLException e) {
e.printStackTrace();
}
}
//更新操作:增删改
public int update(String sql, Object[] objs) {
int i = 0;
try {
//3、创建sql对象
preparedStatement = connection.prepareStatement(sql);
for (int j = 0; j < objs.length; j++) {
preparedStatement.setObject(j + 1, objs[j]);
}
//4、执行sql,返回改变的行数
i = preparedStatement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
return i;
}
//查询操作
public ResultSet select(String sql, Object[] objs) {
try {
getConnection();
//3、创建sql对象
preparedStatement = connection.prepareStatement(sql);
for (int j = 0; j < objs.length; j++) {
preparedStatement.setObject(j + 1, objs[j]);
}
//4、执行sql,返回查询到的set集合
resultSet = preparedStatement.executeQuery();
} catch (SQLException e) {
e.printStackTrace();
}
return resultSet;
}
//断开连接
public void closeConnection() {
//5、断开连接
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
cpp

配置文件db.properties
driverName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3360/my_db?serverTimezone=Asia/Shanghai
username=root
password=****
cpp
每个人都可以根据自己的需求进行设置,并在安装完成后,在文末处提供下载链接,并包含完整的源代码。

至此本篇博客的内容已全部阐述完毕。Java确实较为复杂,在此不做深入探讨的情况下建议你可以选择使用Python来节省时间。关于Java的具体细节内容由于篇幅限制未做详细说明;如有任何疑问或不解之处欢迎随时提出建议进行交流。
