Web编程期末大作业
作业要求:
使用到的技术栈要求:
一、项目架构和数据库schema
本次项目采用前后端分离架构,采用Node.JS作为后端,前端使用Express+Vue+Element UI框架,后台用户数据库使用Mysql,对爬虫数据的搜索使用elasticsearch,可视化功能使用echarts加kibana实现。
本次实验完成了所有的基本要求和扩展要求。
代码框架:
启动server文件夹内为后端代码,其余为vue前端代码。
1、后端框架:
期中app.js为运行express、nodejs的主文件,services.js为实现功能的主要文件,router.js为路由文件、db.js配置连接数据库和elasticsearch的文件。
2、前端框架:
前端实现只要在src文件夹下,主要由登录注册界面和查询界面的vue组件,控制前端路由的router文件和运行前端的main.js文件。
3、数据库schema
数据库中有三个表,分别为fetches、ophistorys和users,分别为爬虫爬取到的新闻数据表、用户操作记录表和用户信息表。
其中fetch表和期中是用的一样。
ophistorys表含有以下字段
username | operation | submission_date | |
---|---|---|---|
Text | Text | Text | Date |
users表含有以下字段
id | username | password | avatar | info | verify_key | status | |
---|---|---|---|---|---|---|---|
Primary KEY | Text | Text | Text | Text | Text | Text | Minint |
期中id为主键,avator为标志位,verify_key为注册时的邮箱验证码(还未实现),status为用户状态,用于启动和禁用用户。
二、实现用户登录和注册的功能
用户只有注册账号登录系统之后才能得到新闻爬虫网站中的数据。
1、用户注册
用户注册时需要提交用户名称,用户密码和用户邮箱,前端接收到用户数据后传给后端由后端写入数据库,并将用户名、注册邮箱、注册操作和操作时间写入操作日志数据库中。
前端代码实现:
<script>
/* import axios from 'axios'
import qs from 'qs' */
export default{
data:function(){
return {
username: '',
password: '',
email: '',
rules: {
username_val: [
{required: true, message: '账号不可为空', trigger: 'blur'}
],
password_val: [
{required: true, message: '密码不可为空', trigger: 'blur'}
],
email_val:[
{required: true,message:'邮箱不可为空',trigger:'blur'}
]
},
}
},
methods:{
doRegiste:function(){
let params={
username:this.username,
password:this.password,
email:this.email,
methodName:'userRegiste',
msg:''
};
console.log(params);
this.$axios.post(this.HOST+'/api/register'
, params).then(result=>{
console.log(result.data)
this.msg = result.data.msg
}).catch(resp =>{
console.log(resp);
});
}
}
}
</script>
拓展功能:密码采用加密方式存储在数据库中
防止因为数据库数据泄漏导致用户密码泄漏,所以用户密码在数据库中采用加密存储,使用md5+一个自己定义的密钥进行存储。
在用户登陆时使用同样的加密方法同从数据库中获得的密码进行对比,相同则登录成功,不同则登录失败。用户注册时会将用户输入的密码加密后存储在数据库中。
加密模块为crypto.js文件
const crypto= require('crypto')
// 秘钥
const SECRET_KEY = 'WJio_8776#' //字符串自己设定的
// md5
function md5(content){
let md5 =crypto.createHash('md5') //
return md5.update(content).digest('hex') //把输出变成16进制
}
var genPassword=function(password){
const str =`password=${password}&key=${SECRET_KEY}`
return md5(str)
}
exports.genPassword = genPassword;
2、用户登录
用户登录操作需要用户的用户名和密码,前端接收用户名和密码发送到后台,后台验证用户名和密码是否相对应且用户的状态为启用状态,再将用户名作为参数跳转到数据搜索界面,使用登录拦截方法实现只有用户在登录过后才能访问搜索数据,如果直接访问搜索页面会被重定向到登录界面。
同时会将用户名、邮箱、登陆操作和操作时间写入操作日志数据库中。
Vue中实现登录拦截的方法:修改router.js文件,在需要登录拦截的路由中加入
meta: {
requireAuth: true
}
接着设置导航守卫函数,并绑定钩子函数
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth) { // 判断该路由是否需要登录权限
if (sessionStorage.getItem("token") == 'true') { // 判断本地是否存在token
next()
} else {
// 未登录,跳转到登陆页面
next({
path: '/login'
})
}
} else {
if(sessionStorage.getItem("token") == 'true'){
next('/search_home');
}else{
next();
}
}
});
记得在login页面中将sessionStorage的token设置为true
sessionStorage.setItem("token", 'true');
this.$router.push('/search_home');
三、爬虫数据搜索和展示功能
1、使用elasticsearch查询爬虫数据
通过爬虫得到的数据会存储在MySQL数据库中,而我们的查询功能需要使用elasticsearch提供的非结构化存储和快速的分词和按照查询结果打分的功能,故我们需要先将MySQL中的爬虫数据导入到elasticsearch中,具体导入过程需要使用logstash和mysql的驱动jar包,具体导入过程可参考:
自动将 MySQL 中的数据转入到 Elasticsearch 中 (logstash)
导入成功之后可以在elasticseach中看到我们从mysql中导入的数据
但这时我们的数据还不支持分词和按匹配程度打分,我们需要为我们在elasticsearch中索引添加映射让特定的字段支持分词,所以我们需要重新创建一个索引crawler_new,设置其mapping,让keywords、content和title字段支持分词,即为其设置 “analyzer”: “ik_smart” 属性(我们这里采用ik分词器,其使用可以参考ElasticSearch中文分词器-IK分词器的使用),新创建索引的mapping如下所示:
{
"crawler_new": {
"mappings": {
"properties": {
"@timestamp": {
"type": "date"
},
"@version": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"author": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"content": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
},
"analyzer": "ik_smart"
},
"crawltime": {
"type": "date"
},
"createtime": {
"type": "date"
},
"id_fetches": {
"type": "long"
},
"keywords": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
},
"analyzer": "ik_smart"
},
"publish_date": {
"type": "date"
},
"source_encoding": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"source_name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
},
"analyzer": "ik_smart"
},
"url": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
}
之后再将我们之前导入的crawler索引中数据导入crawler_inex索引即可。
导入之后的数据:
此时用我们的crawler_new索引中的数据,使用elasticsearch中的全文索引,使用elasticsearch自带的tf-idf得分对所搜索的结果进行打分可以实现查询结果按照主题词打分的排序。
2、搜索功能实现
前端的搜索功能支持按照title搜索或者按照content搜索内容,搜索得到的结果会以表格形式展示,搜索结果的表格支持分页功能,用户可以自定义每页的数据条数,可以对搜索得到的结果按照匹配度得分、数据发布时间和数据爬取时间进行排序,由于新闻内容字段可能过长会影响页面美观,我们之对其显示部分内容。
前端界面如下
分页功能和对查询结果的匹配度得分的排序或者时间的排序功能都由element-ui的接口提供。
前端代码实现:
<template>
<div class="title">
<span>总数量:{{total}}个</span>
<!-- <el-button icon="el-icon-setting" class="fl">操作</el-button>-->
<el-select v-model="value5" multiple placeholder="查询选项" class="fl">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
<div class="demo-input-suffix fl">
<el-form>
<el-form-item>
<el-input size="small"
placeholder="请输入要查询的内容"
prefix-icon="el-icon-search"
v-model="input21">
</el-input>
</el-form-item>
<el-form-item>
<el-button style="" @click="doSearch()" style="display:inline" icon="el-icon-search" size="small">查询</el-button>
</el-form-item>
</el-form>
</div>
<div class="container_table">
<el-table
:data="tableData.slice((currentPage-1)*pagesize,currentPage*pagesize)"
stripe
style="width: 100%"
:default-sort = "{prop: 'date', order: 'descending'}"
>
<el-table-column
type="selection"
width="55">
</el-table-column>
<el-table-column
prop="id"
label="id"
sortable
width="50">
</el-table-column>
<el-table-column
prop="score"
label="score"
sortable
width="100">
</el-table-column>
<el-table-column
prop="url"
label="url">
</el-table-column>
<el-table-column
prop="source_name"
label="source_name">
</el-table-column>
<el-table-column
prop="title"
label="title">
</el-table-column>
<el-table-column
prop="keywords"
label="keywords">
</el-table-column>
<el-table-column
prop="author"
label="author">
</el-table-column>
<el-table-column
label="content"
width="300"
:show-overflow-tooltip='true'>
<template slot-scope="scope">
<span>{{scope.row.content}}</span>
</template>
</el-table-column>
<el-table-column
prop="publish_date"
sortable
label="publish_date">
</el-table-column>
<el-table-column
prop="crawltime"
sortable
label="crawltime">
</el-table-column>
<el-table-column
prop="createtime"
sortable
label="createtime">
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<!--<el-button type="warning" icon="el-icon-star-off" circle></el-button>-->
<el-button type="primary" icon="el-icon-star-on" v-if="istag" @click='switchChange'></el-button>
<el-button type="primary" icon="el-icon-star-off" v-else="!istag" @click='switchChange'></el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="size_change"
@current-change="current_change"
:current-page="currentPage"
:page-sizes="[2, 4, 6, 8, 10]"
:page-size="pagesize"
layout="total, sizes, prev, pager, next, jumper"
:total="tableData.length">
</el-pagination>
</div>
</div>
</template>
<script>
export default {
name:'list11',
data() {
return {
total:6,//默认数据总数
pagesize:2,//每页的数据条数
currentPage:1,//默认开始页面
istag: true,
input:"",
input21: '',
options: [{
value: 'key_word',
label: 'key_word'
},{
value: 'content',
label: 'content'
}],
value5: [],
tableData: []
};
} ,
methods: {
doSearch:function(){
let params={
select_word:this.value5[0],
search_word:this.input21,
msg:'',
methodname:'UserSearch'
};
console.log(params);
this.$axios.post(this.HOST+'/api/search',params).then(result=>{
var res_len=result.data.length;
this.total=res_len;
this.tableData=[];
for(var i=0;i<res_len;i++){
var res_params={
id:result.data[i]._id,
score:result.data[i]._score,
url:result.data[i]._source.url,
source_name:result.data[i]._source.source_name,
title:result.data[i]._source.title,
keywords:result.data[i]._source.keywords,
author:result.data[i]._source.author,
content:result.data[i]._source.content,
publish_date:result.data[i]._source.publish_date,
createtime:result.data[i]._source.createtime,
crawtime:result.data[i]._source.crawltime,
};
console.log(res_params)
this.tableData.push(res_params);
}
this.msg = '查询成功';
}).catch(resp=>{
console.log(resp);
});
},
tableRowClassName({row, rowIndex}) {
if (rowIndex === 0) {
return 'th';
}
return '';
},
switchChange(){
this.istag = !this.istag ;
},
current_change:function(currentPage){
this.currentPage = currentPage;
},
size_change:function(currentSize){
this.pagesize=currentSize;
}
},
created:function(){
this.total=this.tableData.length;
},
};
</script>
后端代码实现:
exports.search=(req,res)=>{
let key_word=req.body.select_word;
let words=req.body.search_word;
if(key_word=="key_word"){
var search={
index: 'crawler_new',
body:{
query:{
match:{
keywords:words,
}
}
}
};
db.getES(search,function(query){
return res.json(query);
})
}
if(key_word=="content"){
var search={
index: 'crawler_new',
body:{
query:{
match:{
content:words,
}
}
}
};
db.getES(search,function(query){
return res.json(query);
})
}
}
这里使用elasticsearch的query搜索,得到对所搜索字段的tf-idf得分来作为该关键词的得分排序
四、图表显示
项目要求中还要求对所获得的数据实现可视化展示功能,图表展示功能分为两个部分一个是使用echars的网页图表展示功能,一个是使用elastricsearch和kibnana的图表展示功能。
1、使用echarts进行图表展示
安装echarts
npm install echarts --save
首先在我们的vue项目中引入echars,并将echars设置为全局变量,这样我们可以在其他文件中也可以使用echars
import echarts from 'echarts'
Vue.prototype.$echarts = echarts
使用echarts组件实现热度分析功能,仅提供对关键词的所搜,当然也可以改成对内容和标题的搜索,都可以改的,我们对搜索得到的文章进行统计得到按出版时间计数的个数,并以图表形式表现出来(大于等于三张不同的图表)。
这里实现了四种图形的展示,分别是直方图、散点图、折线图和饼状图
前端代码:
<script>
export default {
name: 'Echarts',
data(){
return{
input21:'',
chartsData:[],
total:'',
}
},
methods:{
myEcharts(){
// 基于准备好的dom,初始化echarts实例
var myChart = this.$echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
var xdata=[];
var ydata=[];
for(var i=0;i<this.chartsData.length;i++){
xdata.push(this.chartsData[i].publish_date);
ydata.push(this.chartsData[i].doc_count);
}
console.log(xdata)
console.log(ydata)
var option = {
title: {
text: 'Echarts 热度分析 直方图'
},
tooltip: {},
legend: {
data:['publish_date']
},
xAxis: {
data: xdata
},
yAxis: {},
series: [{
name: 'publish_date',
type: 'bar',
data: ydata // data: [5, 20, 36, 10, 10, 20]
}]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
},
doSearch(){
let params={
keywords:this.input21,
msg:'',
methodname:'ChartsSearch'
};
this.$axios.post(this.HOST+'/api/echarts',params).then(result=>{
var res_len=result.data.length;
var temp_list=result.data;
this.total=res_len;
this.chartsData=[];
for(var i=0;i<res_len;i++){
var res_params={
publish_date:temp_list[i].publish_date,
doc_count:temp_list[i].count
};
console.log(res_params)
this.chartsData.push(res_params);
}
this.msg = '查询成功';
this.myEcharts();
this.myEcharts_line();
this.myEcharts_scatter();
this.myEcharts_pie();
}).catch(resp=>{
console.log(resp);
});
}
},
}
后端查询代码:
exports.echarts_display=(req,res)=>{
let keywords=req.body.keywords;
console.log(keywords)
var fetchSql="SELECT publish_date,COUNT(*) as count FROM fetches WHERE keywords LIKE '%"+keywords+"%'"+" GROUP BY publish_date";
db.query(fetchSql,function(err,result,fields){
res.writeHead(200,{
"Content-Type":"application/json"
});
res.write(JSON.stringify(result));
res.end();
});
}
2、使用elastric+kibnana进行图表展示
首先我们需要让kibana连接上elasticsearch,可以参考
Kibana使用教程,连接我们的爬虫索引之后,
就可以进入discover界面对我们的爬虫数据进行可视化探索,可以看到按照publish_date展示的柱图表
如:我们查看对关键词为新冠的新闻的发布时间的可视化展示:
通过设置过滤条件可以查看更多的要求的可视化图形。
五、用户后台管理功能
对后台管理功能的实现,由用户数据库的一个字段status实现,status为0代表该用户被禁用,status为1表示该用户已启用,在用户登录,注册,搜索的时候操作都会被加入ophistorys表。
查看所有操作的网页路由为 ***/admin_home/basetable***进入页面会加载所有的操作数据,且每一项操作数据旁都有启用用户和封禁用户两个按钮来实现对某一用户的封禁和启用。
前端实现在admin_view.vue文件内
具体代码不在这里贴了,只贴启用和禁用两个函数
handleBan(index, row) {
this.$confirm('此操作将禁止该用户登录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.form = this.tableData[index]
this.currentIndex = index
let username=this.form.username
let params={
username:username
}
this.$axios.post(this.HOST+'/api/admin/ban',params).then(result=>{
console.log(result.data)
}).catch(resp =>{
console.log(resp);
});
this.$message({
type: 'success',
message: '禁止成功!'
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消禁止'
})
})
},
handleAllow(index, row) {
this.form=this.tableData[index]
this.currentIndex = index
let username=this.form.username
let params={
username:username
}
this.$axios.post(this.HOST+'/api/admin/allow',params).then(result=>{
console.log(result.data)
this.$message({
type: 'success',
message: result.data.msg
})
}).catch(resp =>{
console.log(resp);
});
},
用户一开始注册时status为1,禁用成功后用户的status变为0
启用前账户的status为0,
用户无法登录
启用后
账户status为1,
用户可以登录
禁用、启用功能后端代码:
/**
* 停用功能
*/
exports.admin_ban=(req,res)=>{
let username=req.body.username;
console.log(username)
var fetch_sql='update users set status=? where username=?';
var params=[0,username]
db.query(fetch_sql,params,function(query,vals,fields){
if(query!=null){
return res.json({status:0,msg:'停用失败'});
}else{
return res.json({status:1,msg:'停用成功'});
}
})
}
/**
* 启用功能
*/
exports.admin_allow=(req,res)=>{
let username=req.body.username;
var fetch_sql='update users set status=? where username=?';
var params=[1,username]
db.query(fetch_sql,params,function(query,vals,fields){
if(query!=null){
return res.json({status:0,msg:'启用失败'});
}else{
return res.json({status:1,msg:'启用成功'});
}
})
}
演示 视频
web编程期末演示视频
六、总结
本次项目代码地址:https://github.com/lh123cha/web_program_final_project
本次实验并没有使用老师所给的示例代码,而是自己从头开始重写代码并对自己期中大作业时的代码进行修改,并结合vue和node js实现前后端分离的框架,正好实现了期中大作业所没有实现的预期。
通过本此实验,我学习了前端开发的一些基本知识,了解并练习了前后端分离开发的一些基本技巧与规范,熟悉了JavaScript语言,又学了一门新的语言,让自己感觉收获颇多,学习了前端vue框架和express框架以及后端的nodejs框架,又多掌握了一项技能,通过这门课的学习让我收获颇多。