结对作业二
这个作业属于哪个课程 | 2021春软件工程实践|W班(福州大学) |
---|---|
这个作业要求在哪里 | 结对作业二 |
结对学号 | 221801113、221801128 |
这个作业的目标 | 顶会热词统计的实现 |
其他参考文献 | 简书/博客园/CSDN/B站 |
一、作业链接
网站链接
网站链接:链接
git仓库链接和代码规范链接
git仓库:https://github.com/imcjx/PairProject
代码规范链接:https://github.com/imcjx/PairProject/blob/main/221801113%26221801128/codestyle.md
二、PSP表格
Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|
计划 | 15 | 15 |
估计这个任务需要多少时间 | 15 | 15 |
开发 | 2435 | 3000 |
原型复审 | 10 | 10 |
需求分析 (包括学习新技术) | 60 | 200 |
生成设计文档 | 40 | 50 |
设计复审 | 10 | 10 |
代码规范 | 25 | 20 |
具体设计 | 30 | 60 |
具体编码 | 2100 | 2400 |
代码复审 | 40 | 30 |
网站部署 | 60 | 120 |
测试 | 60 | 100 |
报告 | 45 | 30 |
计算工作量 | 15 | 10 |
事后总结, 并提出过程改进计划 | 30 | 20 |
合计 | 2495 | 3045 |
三、成品展示
1、登录注册
2、论文搜索
3、论文内容详情以及添加至收藏夹
4、修改已收藏的论文并保存、将论文移出收藏夹
5、论文分析————热门领域展示以及词云图热词点击搜索
6、论文分析————2000-2020年间的热词走势对比
7、账号登出
8、论文爬取功能
四、结对讨论过程描述
- 刚看到题目的时候,其实想用
vue+springboot
前后端分离进行开发,但因为我们两个人都没有什么后端经验,怕一个人后端压力太大,思考后决定选用springboot+thymeleaf
来进行开发,接着我们就对这次作业进行模块划分(登录注册模块、论文列表模块、收藏夹模块、热词分析模块,走势对比模块),两个人负责不同的模块,这样在使用github
进行结对编程时不会产生什么冲突。 - 接着就简单的进行了需求分析和数据库表的设计,在导入助教的数据时也花费了一些时间,主要是json格式的问题,然后口头制定了此次的代码规范以防止后期编码时存在较大风格差异
(虽然codestyle.md最后几次commit才交)。 - 前期基本把时间花费在学习
springboot、mybatis
等时间里在编码过程中,遇到问题,就采取讨论的方式解决,我们在线下的作息时间相同,所以编码的时段也很统一,省去了一大部分不必要的时间的浪费。 - 后期基本就是对界面和一些细小
bug
的修改,期间顺便实现了论文爬取的功能,实现功能后,我们各自对彼此的代码以及本次作业所要求的所有功能进行复审,以防出现问题。 - 结对过程中的讨论图(因为是舍友,基本都是线下交流,聊天记录较少):
五、设计实现过程
数据库表:
共有四张表:collect(收藏夹)、data(论文列表)、hotword(热词图表)、user(用户)
Table: collect
Columns:
uid varchar(255) PK
did int PK
publicationTitle varchar(255)
persistentLink varchar(255)
keywords mediumtext
abstrac mediumtext
Table: data
Columns:
id int AI PK
authors varchar(1024)
keywords text
abstrac text
publicationTitle varchar(256)
publicationYear varchar(256)
persistentLink varchar(256)
typepaper varchar(64)
Table: hotword
Columns:
id varchar(32) PK
hot json
Table: user
Columns:
id int AI PK
username varchar(255)
password varchar(255)
项目框架:
采用springboot + mybatis + thyemleaf + JQuery
设计:
-
登录注册模块
- 前端:通过
绝对定位+transform
的位移来实现注册登录的动效切换,js
使用正则表达式对账号密码进行基本判断 - 后端:通过设置
session
来判断用户的登录状态,通过拦截器判断是否有登录字段来决定是否放行
- 前端:通过
-
侧边栏和顶部栏模块
- 前端:通过
thyemleaf
模板引擎,将其作为组件抽离,以实现复用,通过判断地址栏地址,解决侧边栏高亮和搜索、爬取按钮disabled
的问题
- 前端:通过
-
论文查询、收藏夹模块
- 前端:通过
flex
布局,设置页面的四个卡片,底部分页器使用的pagination.js
分页插件,显示具体信息通过点击卡片,展示弹窗,提示信息用的是toastr.js
。 - 后端:通过前端传输的搜索信息和分页器提供的页码从数据库查询指定位置数据并返回。爬虫则是根据搜索的信息通过
HttpURLConnection
获取所需json
字符串,通过fastjson
转化为所需map
传回前端。
- 前端:通过
-
词云图
- 前端:采用卡片形式布局,每张卡片中都应用了
highchart
模板来处理后端返回的数据,并以图表的方式呈现。 - 后端:采用
springboot,mybatis
框架,采用MVC
的设计模式,Bean
层设计实体类,存放热词相关属性key
和value
,mapper
层定义存取数据库表数据的方法,service
层调用接口,最后在controller
层调用实现;排序方法取出Top10热词;存入数据库以String
字符串的形式,使用时从数据库取出并转换为json
形式返回给前端js
处理。
- 前端:采用卡片形式布局,每张卡片中都应用了
-
动态柱状图
- 前端:走势对比页面设计一个
div
用来显示动态图表,js
应用相关图表模板处理后端返回的json
数据。 - 后端:
springboot,mybatis
框架实现,返回前端动态柱状图所需的两个json
文件,从数据库里取到的所有论文信息,以相应的json
结构来定义对象数组存放,并根据热词数量和年份递增排序,以实现图表按年份递增播放的效果。
- 前端:走势对比页面设计一个
功能结构图:
六、关键代码说明
1、论文爬取的实现
- 通过查看https://ieeexplore.ieee.org/的网络请求,发现需爬取信息在search这个网络请求里。通过
HttpURLConnection
获取爬取的数据
/**
* 获取爬取论文的json字符串
* @param searchInfo
* @return
*/
public String getJsonStr(String searchInfo) {
String params = "{\"newsearch\":true,\"queryText\":\""+searchInfo+"\",\"highlight\":true" +
",\"returnFacets\":[\"ALL\"],\"returnType\":\"SEARCH\",\"matchPubs\":true}";
HttpURLConnection httpURLConnection = null;
BufferedReader reader = null;
OutputStream out = null;
String res = "";
try {
httpURLConnection=setConnection();
httpURLConnection.connect();
out = httpURLConnection.getOutputStream();
out.write(params.getBytes());
if (httpURLConnection.getResponseCode() == 200) {
reader = new BufferedReader(
new InputStreamReader(httpURLConnection.getInputStream()));
res = getResStr(reader);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
out.flush();
out.close();
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return res;
}
- 将流里的数据读出
/**
* 获取输入流的json字符串
* @param reader
* @return
* @throws IOException
*/
public String getResStr(BufferedReader reader) throws IOException {
StringBuilder sb = new StringBuilder();
String str = "";
while ((str = reader.readLine()) != null) {
sb.append(str);
}
return sb.toString();
}
- 为请求设置请求头
/**
* 设置请求头参数
* @return
*/
public HttpURLConnection setConnection() {
HttpURLConnection httpURLConnection = null;
try {
URL url = new URL("https://ieeexplore.ieee.org/rest/search");
httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setRequestMethod("POST");
httpURLConnection.setConnectTimeout(10000);
httpURLConnection.setDoOutput(true);
httpURLConnection.setDoInput(true);
httpURLConnection.setRequestProperty("Content-Type", "application/json");
httpURLConnection.setRequestProperty("Accept", "application/json, text/plain, */*");
httpURLConnection.setRequestProperty("Accept-Language", "zh-CN,zh;q=0.9");
httpURLConnection.setRequestProperty("Connection", "keep-alive");
httpURLConnection.setRequestProperty("Referer", "https://ieeexplore.ieee.org/search/searchresult.jsp?newsearch=true&queryText=computer");
httpURLConnection.setRequestProperty("Accept-Encoding", "gzip, deflate, br");
} catch (IOException e) {
e.printStackTrace();
}
return httpURLConnection;
}
- 使用
fastjson
将获取到的json
字符串转为json
对象存入map
里
/**
* 将json字符串转变为json对象放入map里
* @param jsonStr
* @return
*/
public Map<List<Paper>, Integer> getMap(String jsonStr) {
Map<List<Paper>, Integer> map= new HashMap<>();
List<Paper> paperList=new ArrayList<>();
JSONObject jsonObject= JSONObject.parseObject(jsonStr);
JSONArray jsonArray = jsonObject.getJSONArray("records");
if (jsonArray == null) {
map.put(paperList, new Integer(0));
} else {
for (int i = 0; i < jsonArray.size(); i++) {
Paper paper = new Paper();
JSONObject jo = jsonArray.getJSONObject(i);
paper.setAbstrac(jo.getString("abstract"));
paper.setId(jo.getString("articleNumber"));
paper.setPersistentLink("https://ieeexplore.ieee.org"+jo.getString("documentLink"));
paper.setPublicationTitle(jo.getString("articleTitle"));
paperList.add(paper);
}
map.put(paperList, new Integer(jsonArray.size()));
}
return map;
}
2、Top10词云图的实现
@Mapper//预存入热词top10数据的mapper
public interface HotwordMapper {
@Select("select keywords from data where typepaper = 'ECCV'")
public List<String> getAllWord();
@Insert("insert into hotword values(111,#{json})")
public void insertHotword(@Param("json") String json);
@Select("select hot from hotword where id = #{type}")
public String getHotwordjson(@Param("type") String type);
}
public class HotwordService {
@Autowired
HotwordMapper hotwordMapper;
/**
* 获取数据库所有热词
* @return
*/
public List<String> getAllWord(){
return hotwordMapper.getAllWord();
}
/**
* 预存入数据
* @param json
*/
public void insertHotword(String json){
hotwordMapper.insertHotword(json);
}
/**
* 得到top10热词
* @param type
* @return
*/
public String getHotwordjson(String type){
return hotwordMapper.getHotwordjson(type);
}
}
- 将爬取到的存在数据库中的总数据进行分析,获取top10热词的
json
文件
/**
* 预存入热门领域数据
* @return
*/
@GetMapping("/hot2")
@ResponseBody
public void getWord2() throws JsonProcessingException {
HashMap<String,Integer> hashMap = new HashMap<String,Integer>();
List<HotWord> list = new ArrayList<>();
String a = new String();
List<String> liststr = new ArrayList<>();
liststr = hotwordService.getAllWord();
String str = new String();
for (int q = 0; q < liststr.size(); q++) {
a = liststr.get(q);//String
if(a==null){
continue;
}
str = a.replace("\"", "")
.replace("(","")
.replace(")","");
String[] chars = new String[2000];
chars = str.split(",");
for (int j = 0; j < chars.length; j++) {
if(hashMap.containsKey(chars[j].toLowerCase())){
hashMap.put(chars[j].toLowerCase(),hashMap.get(chars[j].toLowerCase())+1);
}
else {
hashMap.put(chars[j].toLowerCase(),1);
}
}
}
List<HashMap.Entry<String, Integer>> sortedList = getSortedList(hashMap);
HashMap<String,Integer> hashMap2 = new HashMap<String,Integer>();
int cnt = 0;
for(HashMap.Entry<String,Integer> entry:sortedList) {
hashMap2.put(entry.getKey(),entry.getValue());
cnt++;
if (cnt >= 10)//仅需要输出前十位
break;
}
ObjectMapper mapper = new ObjectMapper();
String jsonlist = mapper.writeValueAsString(hashMap2).toString();
hotwordService.insertHotword(jsonlist);
}
/**
* 返回前端热门领域数据
* @param type 顶会类型
* @return
*/
@GetMapping("/hot")
@ResponseBody
public String getWord(@RequestParam(value = "type") String type) {
String str = hotwordService.getHotwordjson(type);
return str;
}
- 前端
js
接收返回的json
数据并调用
function getChart(type) {
var str1 = [];
var str2 = [];
var data2 = [];
var cnt = 0;
var cnt2 = 0;
var cnt3 = 0;
$.ajax({//AJAX请求接口
type: "GET",
contentType: "application/json;charset=UTF-8",
url: "/hot",
data: {type: type},
success: function(re) {
console.log(re);
let res= JSON.parse(re);
for(var key in res){
str2[cnt2] = key;
data2[cnt2] = res[key];
cnt3+=data2[cnt2];
cnt2++;
for(var n = 0;n<res[key];n++) {
str1[cnt] = key;
cnt++;
}
}
var text = str1.toString();
//top10热词的数据data
var data = text.split(/[,\. ]+/g)
.reduce(function (arr, word) {
var obj = arr.find(function (obj) {
return obj.name === word;
});
if (obj) {
obj.weight += 1;
} else {
obj = {
name: word,
weight: 1
};
arr.push(obj);
}
return arr;
}, []);
//渲染词云图容器
Highcharts.chart('container', {
series: [{
type: 'wordcloud',
data: data
}],
title: {
text: ''
}
});
...
3、动态柱状图实现
- 对得到的
hashmap
进行排序
/**
* 对全部热词进行排序
* @param hashMap 存放关键词-数量
* @return
*/
public static List<HashMap.Entry<String, Integer>> getSortedList(HashMap<String, Integer> hashMap) {
List<HashMap.Entry<String, Integer>> list1 =
new ArrayList<HashMap.Entry<String, Integer>>(hashMap.entrySet());
Collections.sort(list1, new Comparator<Map.Entry<String,Integer>>() {
public int compare(Map.Entry<String,Integer> hash1, Map.Entry<String,Integer> hash2){
if(hash1.getValue().equals(hash2.getValue()))
return hash1.getKey().compareTo(hash2.getKey());
return hash2.getValue().compareTo(hash1.getValue());
}
});
return list1;
}
/**
* 对关键词数量及年份分别排序
* @param hashMap 存放关键词-年份
* @return
*/
public static List<HashMap.Entry<String, Integer>> getSortedList2(HashMap<String, Integer> hashMap) {
List<HashMap.Entry<String, Integer>> list1 = new ArrayList<HashMap.Entry<String, Integer>>(hashMap.entrySet());
Collections.sort(list1, new Comparator<Map.Entry<String,Integer>>() {
public int compare(Map.Entry<String,Integer> hash1, Map.Entry<String,Integer> hash2){
if(hash1.getKey().split(",")[1].equals(hash2.getKey().split(",")[1]))
return hash2.getValue().compareTo(hash1.getValue());
return hash1.getKey().split(",")[1].compareTo(hash2.getKey().split(",")[1]);
}
});
return list1;
}
- 解析数据库中的所有数据返回排序后的精简数据,方便后续返回给前端数据
/**
* 预存入动态柱状图所需第二个参数
* @throws JsonProcessingException
*/
@GetMapping("/json22")
@ResponseBody
public void getSecondJson2() throws JsonProcessingException {
List<List<String>> json2 = new ArrayList<>();
List<String> jsonson2 = new ArrayList<>();
HashMap<String,Integer> hashMap = new HashMap<String,Integer>();
List<NameAndYear> keyandyear = new ArrayList<>();
keyandyear = trendService.getYear();
List<String> keywords = new ArrayList<>();
List<String> publicationYear = new ArrayList<>();
for (int i = 0;i<keyandyear.size();i++){
String key = keyandyear.get(i).getKeywords();
if (key == null) continue;
else key = key.toLowerCase();
String year = keyandyear.get(i).getPublicationYear();
keywords.add(key);
publicationYear.add(year);
}
String a = new String();
String numm = new String();
String str = new String();
for (int q = 0; q < keywords.size(); q++) {
numm = publicationYear.get(q);
a = keywords.get(q);
if(a==null){
continue;
}
str = a.replace("\"", "").replace("(","").replace(")","");
String[] chars = new String[2000];
chars = str.split(",");
String strr = new String();
for (int j = 0; j < chars.length; j++) {
strr = chars[j]+","+numm;
if(hashMap.containsKey(strr)){
hashMap.put(strr,hashMap.get(strr)+1);
}
else {
hashMap.put(strr,1);
}
}
}
jsonson2.add("Income");
jsonson2.add("Life Expectancy");
jsonson2.add("Population");
jsonson2.add("Country");
jsonson2.add("Year");
json2.add(jsonson2);
List<HashMap.Entry<String, Integer>> count = getSortedList(hashMap);
List<HashMap.Entry<String, Integer>> count2 = new ArrayList<>();
count = getSortedList2(hashMap);//
int size = count.size();
HashMap<String ,Integer> yearnumhash = new HashMap<>();
for(int l = 0;l<size;l++){
String year = count.get(l).getKey().replace("\"", "").replace("(","").replace(")","").split(",")[1];
if(yearnumhash.containsKey(year)){
yearnumhash.put(year,yearnumhash.get(year)+1);
}else {
yearnumhash.put(year,1);
}
if(yearnumhash.get(year)<21){
count2.add(count.get(l));
}else {
continue;
}
}
for(int y = 0;y<count2.size();y++){
List<String> jsonson = new ArrayList<>();
String name = count2.get(y).getKey().replace("\"", "").replace("(","").replace(")","").split(",")[0];
String year = count2.get(y).getKey().replace("\"", "").replace("(","").replace(")","").split(",")[1];
jsonson.add(String.valueOf(count2.get(y).getValue()));
jsonson.add("");
jsonson.add("");
jsonson.add(name);
jsonson.add(year);
json2.add(jsonson);
}
ObjectMapper mapper3 = new ObjectMapper();
String jsonlist = mapper3.writeValueAsString(json2).toString();
trendService.insertTrend2(jsonlist);
}
七、心路历程和收获
cyl(221801128):
在对json
文件的处理与数据库的存储时碰到了一些困难,后来通过查询文档及CSDN教程学习;
在绘制词云图以及动态柱状图的对图表结构不太明白,后来通过研究源码明白各段数据的意义,最后在后端获取需要的json
文件;
对前端布局设计还不是很熟练,还需要多加练习。
收获:学会了springboot、mybatis
框架的应用,AJAX
获取后端数据,前后端数据的交互以及远程数据库的连接。
cjx(221801113):
本次结对作业收获还是很大的,在一开始看到作业的时候还是有丶小慌的,因为之前都是编写前端部分,后端都没有接触过,还好这次作业完成的比较顺利,这次学习了使用springboot+mybatis
,还学习了服务器部署,顺便学习了下怎么用java
进行爬取数据,感觉收获还是特别大的,期间也踩了挺多坑的,尤其在项目刚开始在使用git-desktop
的时候代码出现直接被覆盖,数据丢失的情况,幸亏之前有备份,后来每次要进行git
提交时都备份了代码,在学习框架创建项目的时候也遇到许多问题,好在室友、百度都能为我解答,在进行编码的时候还是很顺利的,爬取部分花费了一些时间,一开始选择的页面虽然可以直接下载json
文件,但文件里每篇论文都没有摘要,后面选择的页面(虽然没有关键词,但无伤大雅)在进行请求时忘记设置请求头,一直接收不到数据。这次作业感觉很好锻炼了我学习新知识,解决未知问题的能力,感觉还是很不错的。
八、评价结对队友
cyl(221801128)对cjx(221801113)的评价:这是我和cjx同学的第二次合作,jx同学是一个编码能力很强,对设计要求很严格的同学,对我起到十分大的约束作用。在我编码碰到难题时,jx同学也总是热心解答,帮我度过山重水复的难关,从而顺利完成这一次的结对编程。
cjx(221801113)对cyl(221801128)的评价:这次合作还是非常愉快的,yl同学虽然之前对这方面很陌生,但学习能力很强,交代的任务能很好的完成。而且在完成自己的任务后,也帮我解决了许多小问题,希望下次还能和他进行合作。