·
本项目在期中项目(指路链接: Web编程期中实践——Nodejs新闻爬虫及查询网站)的基础上修改完成。
·
项目要求
数据库设计
该项目我们在crawl数据库中设计了3个表:fetches、user和user_action。
(1)fetches表同期中项目的设计一样,只不过我们在此处修改优化了当时的爬虫代码,利用正则表达式规范爬取的数据,比如对author一栏,确保获得并存储进数据库的作者名足够“干净”,有利于后续在网页中的数据展示。
(2)user表包含基本用户信息(姓名、密码、注册时间),此外还设置power和state两项来记录 用户身份(管理员or普通用户)和 状态(正常or冻结)。
同时,我们人为向数据库中插入一条记录,创建管理员admin。
insert into crawl.user(username, password, power, state) values('admin', 12345, 'super', 'normal');
(3)user_action表存放所有用户在该网站上的所有操作记录(如请求时间、用户所在IP地址、请求内容、响应状态等)。
·
基础配置
如图,我们在express搭建框架生成的search_site文件中进行一些基础的配置(express框架搭建见我们期中项目博客)。
在mysqlConf.js中配置mysql连接参数;
在app.js中设置路由路径、导入angular框架、设置session参数等,以及设置将用户的操作记录存入数据库。
var usersRouter = require('./routes/users');
var newsRouter = require('./routes/news');
......
·
用户注册、登录及登出
我们在index.html上实现了一个简洁明了的登录和注册窗口,能根据用户的操作进行相应提示。
- 注册:
a.用户已存在;
b.两次输入密码不一致;
c.注册成功后自动跳转登录界面。 - 登录:
a. 用户名或密码错误;
b. 用户被冻结,无法登陆;
c. 用户不存在,请先注册。
同时,我们也不忘设计对未登录用户,当其处于非登录页面时,不允许其查看网页,并强制其跳转登录(此处的设计仍不够完善,通过用户触发操作函数时判断是否有登录用户来达到该目的,存在耦合、冗余的问题,之后有待改进)。
登出选项我们则设置在网站各页面的导航栏中,一旦退出,则清除session。
·
新闻查询
由于我们将新闻查询和图表展示的内容置于同一个界面news.html,结构稍显耦合、复杂,故通过ng-show、ng-hide两个参数来调控页面的展示布局。
这里isShow作用于所有图表,isShow2控制折线图的查询框。通过二者合取的结果来控制新闻查询窗口的显示与否。我们默认初始时显示新闻查询窗口。
<div ng-show="isShow && isShow2" style="width: 1150px;position:relative; top:70px;left: 80px">
<!-- 查询页面-->
<div ng-include="'search.html'"></div>
</div>
对于新闻查询,我们实现了多条件组合查询,可同时对标题、内容、作者和来源搜索,并且对标题和内容关键字还配备布尔查询AND和OR选项。
结果页面简洁明了,直接展示总结果记录数;对标题赋予超链接,用户可直接点击跳转到新闻页面;同时还实现新闻按时间升序、降序排列;以及分页展示结果。
查询页路由 routes/news.js:
router.get('/search', function(request, response) {
if (request.session['username']===undefined) {
// response.redirect('/index.html')
response.json({message:'url',result:'/index.html'});
}else {
var param = request.query;
newsDAO.search(param,function (err, result, fields) {
response.json({message:'data',result:result});
})
}
});
查询结果展示 public/search.html:
<!--isisshowresult:控制显示查询结果-->
<div ng-show="isisshowresult" style="padding:0 1em;">
<p><b>共 {{datasize}} 条记录</b></p>
<table class="table table-striped">
<thead>
<tr>
<td>序号</td>
<td>标题</td>
<td>作者</td>
<td>关键词</td>
<td>来源</td>
<td>发布时间</td>
</tr>
</thead>
<tbody>
<tr ng-repeat="(key, item) in items">
<td>{{index+key}}</td>
<td><a href={{item.url}}>{{item.title}}</a></td>
<td>{{item.author}}</td>
<td>{{item.keywords}}</td>
<td>{{item.source_name}}</td>
<td>{{item.publish_date}}</td>
</tr>
</tbody>
</table>
</div>
·
图表展示
以下我们一共实现了4个图表,通过导航栏中下拉菜单来选择目标图像,并用 isShow 和 isShow2 标签来控制同在 news.html 页面的各图表的显示。
·
柱状图
展示了 新闻发布数 随时间的变化情况。
·
饼图
由于作者数目过多,这里仅展示了新闻发布数在10篇以上的作者分布的饼图。之前提到的对爬虫进行优化以得到“干净”的作者名能方便此处饼图的展示。(虽然也可以在展示饼图时对不干净的作者名进行处理,但当用户数目以及页面访问次数增多时,会带来不必要的开销,因此我们追求在初始时就将干净的数据存入数据库。)
·
折线图
这里对折线图的实现要稍复杂,需要用户现在搜索框中输入想要查询的词语,再处理显示对应词语在不同时间的新闻中出现的次数图。
public/new.html:
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">图片<span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a ng-click="histogram()">柱状图</a></li>
<li><a ng-click="pie()">饼状图</a></li>
<li><a ng-click="word_freq()">折线图</a></li>
<li><a ng-click="wordcloud()">词云</a></li>
</ul>
</li>
<!-- 所有的图片都绘制在main1位置-->
<span ng-hide="isShow" id="main1" style="width: 1000px;height:600px;position:fixed; top:70px;left:80px"></span>
<!-- 折线图搜索框显示 -->
<div ng-hide="isShow2" id="main1" style="width: 1000px;height:600px;position:fixed; top:70px;left:80px">
<div class="row" style="margin-bottom: 10px">
<div class="col-lg-2">
<input type="text" class="form-control" placeholder="查询词语" ng-model="$parent.word_search">
</div>
<div class="col-lg-2">
<button type="submit" class="btn btn-default" ng-click="wordSearch()">查询</button>
</div>
</div>
</div>
public/javascripts/news.js:
$scope.word_freq = function () {
$scope.isShow = true; // 调用wordSearch()后再显示
$scope.isShow2 = false;
};
$scope.wordSearch = function () {
$scope.isShow = false;
var word_search = $scope.word_search;
var word_url = `/news/wordSearch?word_search=${word_search}`;
$http.get(word_url).then(
function (res) {
if (res.data.message == 'data') {
var myChart = echarts.init(document.getElementById("main1"));
option = {
title: {
text: '"' + word_search + '"该词在新闻中的出现次数随时间变化图',
x: 'center'
},
xAxis: {
type: 'category',
data: Object.keys(res.data.result)
},
yAxis: {
type: 'value'
},
series: [{
data: Object.values(res.data.result),
type: 'line',
itemStyle: { normal: { label: { show: true } } }
}],
};
if (option && typeof option === "object") {
myChart.setOption(option, true);
}
}
}, function (err) {
$scope.msg = err.data;
});
};
routes/news.js:
router.get('/wordSearch', function(request, response) {
if (request.session['username']===undefined) {
response.json({message:'url',result:'/index.html'});
}else {
var keyword = request.query["word_search"]; // 前端提交传入的搜索词
var fetchSql = "select content,publish_date from fetches where content like'%" + keyword + "%' order by publish_date;";
newsDAO.query_noparam(fetchSql, function (err, result, fields) {
response.writeHead(200, {
"Content-Type": "application/json",
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": 0
});
response.write(JSON.stringify({message:'data',result:myfreqchangeModule.freqchange(result, keyword)}));
response.end();
});
}
});
·
词云
在绘制词云时,我们调用了 nodejieba 库,通过 extract() 方法抽选出每篇新闻中排名前20的词语,合并作为词云的数据集。
由于nodejieba没有去除停用词的方法,人工去除较为麻烦,故用 extract 能较好地避免展示的数据中高频词多为停用词的情况。
·
用户管理
鉴于对用户相关的操作较多,我们单独新建 userSQLMap.js 文件来记录所需SQL命令,解耦合便于操作。
var userSqlMap = {
add: 'insert into user(username, password, power, state) values(?, ?, \'normal\', \'normal\')', //注册时用,默认非管理员
getByUsername: 'select username, password from user where username = ?', //登陆时用
getPower: 'select power from user where username = ?', // 查询该用户是否是管理员
getAllUsers: 'select * from user', // 查询所有用户
freezeUser: 'update user set state = \'freeze\' where username = ?', // 冻结用户
unfreezeUser: 'update user set state = \'normal\' where username = ?', // 解冻用户
powerUser: 'update user set power = \'super\' where username = ?', // 设置用户为管理员
unpowerUser: 'update user set power = \'normal\' where username = ?', // 取消用户管理员权限
getHistory: 'select * from user_action where username = ?', // 获取用户历史记录
getState: 'select state from user where username = ?', // 查询用户状态
};
module.exports = userSqlMap;
·
同样,我们通过点击导航栏“账号管理”下拉菜单中的“管理”选项可以跳转管理界面。但此处仅对 管理员 身份用户有效,若为普通用户则点击无用,会跳转会新闻查询界面。
// 管理用户
router.get('/manage', function (req, res, next) {
var user = req.session;
if (user['username'] === undefined) {
response.json({ message: 'url', result: '/index.html' });
} else {
userDAO.getPower(user.username, function (powerflag) {
// 返回的powerflag为:[ RowDataPacket { power: 1 } ]
if (powerflag[0].power != 'super') {
// res.json({ msg: '您并非管理员,暂无该权限' });
res.json({ msg: 'url', result: '/news.html' });
} else {
res.json({ result: '/manage.html' });
}
})
}
});
·
我们利用 ng-show 和 ng-hide 设置标签来美化页面布局。通过单击 查询用户 按钮来展示所有用户,以及他们的身份(管理员与否)、账户状态(冻结or正常)。
还可通过输入用户名,来调整该用户的权限和状态信息。
·
此外,我们也支持通过输入用户名来查询其相应历史记录。鉴于用户的记录数量一般较大,我们在此处也依旧设置了分页显示结果,每页最大记录数为10。
·
鉴于该项目代码较多,博客中指贴出小部分做示例,完整代码详见:GitHub——Web Programming