点击直接跳转
技术实现:Angular.js+Express+Mysql
前言:
在本次作业中,我选择了项目一,想把爬虫项目完整实现,同时也申请了项目二的材料,在课余自行尝试。在项目一中,一开始自己并没有使用任何前端框架,但学习了老师给出的示例中运用的angular.js框架,发现angular.js具有指令、插值表达式、双向数据绑定等核心特性,使得View和Model间的数据传输非常便利,简化了代码的编写,进行了清晰的分层,提高了代码的可维护性。最终达到用户与应用程序交互时动态更新页面的效果,更好地响应用户操作的业务逻辑管理。
学习angular.js指令和操作参考网站——菜鸟教程:
https://www.runoob.com/angularjs/angularjs-intro.html
在示例的基础功能上,主要添加了用表单输入查任意词频率变化情况、基于分词实现中文全文检索、对查询结果按照主题词打分进行排序等功能。
(1)先在数据库中创建两张表:
一个是用于记录已注册用户的注册名、密码和注册时间,用于添加新用户的信息和检验用户再次登录时的用户和密码是否匹配
CREATE TABLE `crawl`.`user` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`username` VARCHAR(45) NOT NULL,
`password` VARCHAR(45) NOT NULL,
`registertime` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `username_UNIQUE` (`username`))ENGINE=InnoDB DEFAULT CHARSET=utf8;
另一个是记录用户的注册、登录和查询等操作,记入数据库中的日志,并且实时显示在终端
CREATE TABLE `crawl`.`user_action` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`username` VARCHAR(45) NOT NULL,
`request_time` VARCHAR(45) NOT NULL,
`request_method` VARCHAR(20) NOT NULL,
`request_url` VARCHAR(300) NOT NULL,
`status` int(4),
`remote_addr` VARCHAR(100) NOT NULL,
PRIMARY KEY (`id`))ENGINE=InnoDB DEFAULT CHARSET=utf8;
(2)建立好连接数据库的配置文件/conf/mysqlConf.js
module.exports = {
mysql: {
host: 'localhost',
user: 'root',
password: 'root',
database:'crawl',
// 最大连接数,默认为10
connectionLimit: 10
}
};
功能实现——对用户有适当提示:
首先在根元素上定义 AngularJS 应用“login”,指定AngularJS应用程序管理的边界,使得在ng-app内部的指令起作用
<html ng-app="login">
登录窗口:
通过 ng-model 指令把输入域的用户名和密码以属性名 “username” 和 “password” 绑定到当前作用域中,以便 $scope 对象在控制器和视图之间传递数据;当按钮事件被触发时,调用 check_pwd() 函数。
<form id="login-form" method="post" role="form" style="display: block;">
<div class="form-group">
<input ng-model="username" tabindex="1" class="form-control" placeholder="Username" value=""/>
</div>
<div class="form-group">
<input type="password" ng-model="password" tabindex="2" class="form-control" placeholder="Password">
</div>
<div class="form-group">
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<button id="login-submit" tabindex="4" class="form-control btn btn-login" ng-click="check_pwd()">LOG IN</button>
</div>
</div>
</div>
</form>
警告窗口:
定义了 $scope 对象属性 msg,用来传递报错信息,展示在前端页面。如果 msg 未定义或者值为“ok”,表达式结果为 false ,将会移除 HTML 元素,不会展示出来。
<div class="alert alert-warning" ng-if="msg && msg!='ok'">
<a href="#" class="close" data-dismiss="alert">×</a>
<strong>警告!</strong>{{msg}}
</div>
先定义控制器“loginCtrl” :
var app = angular.module('login', []);
app.controller('loginCtrl', function ($scope, $http, $timeout) {…});
通过 $scope 对象的 username 和 password 属性获取绑定的用户名和密码,生成json对象传给路由。 如果返回结果是“ok”,则修改 window 对象的 location.href 属性,将当前页面跳转至新闻查询和echarts图操作前端页面 news.html ;否则通过预先定义在前端页面定义的 &scope 对象属性 msg 显示警告。
$scope.check_pwd = function () {
var data = JSON.stringify({
username: $scope.username,
password: $scope.password
});
$http.post("/users/login", data)
.then(
function (res) {
if(res.data.msg=='ok') {
window.location.href='/news.html';
}else{
$scope.msg=res.data.msg;
}
},
function (err) {
$scope.msg = err.data;
});
};
将用户名传入数据库获取对应的密码
var userDAO = require('../dao/userDAO');
router.post('/login', function(req, res) {
var username = req.body.username;
var password = req.body.password;
userDAO.getByUsername(username, function (user) {
if(user.length==0){
res.json({msg:'用户不存在!请检查后输入'});
}else {
if(password===user[0].password){
req.session['username'] = username;
res.cookie('username', username);
res.json({msg: 'ok'});
}else{
res.json({msg:'用户名或密码错误!请检查后输入'});
}
}
});
});
(注册操作的实现与登录操作思路相同,这里不作详述)
功能实现——对用户有适当提示:
因为进入主页先显示登录界面,因此这里设置元素显示方式 display 为 none
<form id="register-form" method="post" role="form" style="display: none;">
<div class="form-group">
<input ng-model="add_username" tabindex="1" class="form-control" placeholder="Username" value=""/>
</div>
<div class="form-group">
<input type="password" ng-model="add_password" tabindex="2" class="form-control" placeholder="Password">
</div>
<div class="form-group">
<input type="password" ng-model="confirm_password" tabindex="2" class="form-control" placeholder="Confirm Password">
</div>
<div class="form-group">
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<button tabindex="4" class="form-control btn btn-register" ng-click="doAdd()">Register Now</button>
</div>
</div>
</div>
</form>
先检验两次输入密码是否一致,如果不一致,直接显示警告
$scope.doAdd = function () {
if($scope.add_password!==$scope.confirm_password){
$scope.msg = '两次密码不一致!';
}
else {
var data = JSON.stringify({
username: $scope.add_username,
password: $scope.add_password
});
$http.post("/users/register", data)
.then(function (res) {
if(res.data.msg=='成功注册!请登录') {
$scope.msg=res.data.msg;
$timeout(function () {
window.location.href='index.html';
},2000);
} else {
$scope.msg = res.data.msg;
}
}, function (err) {
$scope.msg = err.data;
});
}
先检查用户是否已存在,如果已存在,则无需再添加用户信息,显示警告
router.post('/register', function (req, res) {
var add_user = req.body;
userDAO.getByUsername(add_user.username, function (user) {
if (user.length != 0) {
res.json({msg: '用户已存在!'});
}else {
userDAO.add(add_user, function (success) {
res.json({msg: '成功注册!请登录'});
})
}
});
});
这里借助 Express 框架记录日志的中间件 morgan ,而且在 app.js 文件中已经默认引入了该中间件 var logger = require('morgan');
(使用 app.use(logger('dev'))
, 可以将请求信息打印在控制台)
参照 morgan官方说明文档,获取所要存储的相关信息,直接将用户操作记入 mysql 中
//设置session信息
var logger = require('morgan');//借助中间件保存的信息
app.use(session({
secret: 'sessiontest',//与cookieParser中的一致
resave: true,
saveUninitialized: false, // 是否保存未初始化的会话
cookie : {
maxAge : 1000 * 60 * 60, // 设置 session 的有效时间,单位毫秒
},
}));
app.use(logger(function (tokens, req, res) {
var request_time = new Date();
var request_method = tokens.method(req, res);
var request_url = tokens.url(req, res);
var status = tokens.status(req, res);
var remote_addr = tokens['remote-addr'](req, res);
if(req.session){
var username = req.session['username']||'notlogin';
}else {
var username = 'notlogin';
}
if(username!='notlogin'){
logDAO.userlog([username,request_time,request_method,request_url,status,remote_addr], function (success) {
console.log('成功保存!');
})
}
}, ));
在登录后,我们便进入到了查询和echarts图操作界面
前端代码(news.html)
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">News</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li ><a ng-click="showSearch()">检索</a></li>
<li ><a ng-click="showSearch2()">分词查询</a></li>
<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="showsearchline()">折线图</a></li>
<li><a ng-click="wordcloud()">词云</a></li>
</ul>
</li>
<li>
<a href="#" class="dropdown-toggle" data-toggle="dropdown">账号管理<span class="caret"></span></a>
<ul class="dropdown-menu">
<li class="dropdown-header">账号</li>
<li><a ng-click="logout()">退出登录</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
(比如“新冠 AND 肺炎”或者“新冠 OR 肺炎”)
(1)当点击“检索”按钮—>调用 show_search() 函数
angular代码(javascripts/new.js)
在示例基础上添加新表单的时候,出现了由于覆盖导致显示不出来的问题,琢磨了很久,发现在此处很巧妙地实现了“检索”和“图片”不同时出现的效果:
注意在此文件中要依据不同实现效果修改不同元素 ng-show 或 ng-hide 指令表达式的指令,这里不再一一展示。
在 showSearch() 函数中,将 isShow 和 isShow2 都设为 true,ng-show 指令在表达式为 true 时显示指定的 HTML 元素,否则隐藏指定的 HTML 元素,ng-hide 相反。于是显示 isShow 表达式所在区域。
$scope.showSearch = function () {
$scope.isShow = true;
$scope.isShow2 = true;
$scope.isisshowresult = false;
$scope.isShow22 = false;
$scope.isShowtext = false;
// 再次回到查询页面时,表单里要保证都空的
$scope.title1=undefined;
$scope.title2=undefined;
$scope.selectTitle='AND';
$scope.content1=undefined;
$scope.content2=undefined;
$scope.selectContent='AND';
$scope.sorttime=undefined;
};
(2)—>显示输入框(search.html)
这里通过 ng-include 语句在 news.html 引入查询页面
<div ng-show="isShow" ng-init="isShowtext=false" style="width: 1300px;position:relative; top:70px;left: 80px">
<!--查询页面-->
<div ng-include="'search.html'"></div>
</div>
点击提交后调用search()函数
<form class="form-horizontal" role="form">
<div class="row" style="margin-bottom: 10px;">
<label class="col-lg-2 control-label">标题关键字</label>
<div class="col-lg-3">
<input type="text" class="form-control" placeholder="标题关键字" ng-model="$parent.title1">
</div>
<div class="col-lg-1">
<select class="form-control" autocomplete="off" ng-model="$parent.selectTitle">
<option selected="selected">AND</option>
<option>OR</option>
</select>
</div>
<div class="col-lg-3">
<input type="text" class="form-control" placeholder="标题关键字" ng-model="$parent.title2">
</div>
</div>
<div class="row" style="margin-bottom: 10px;">
<label class="col-lg-2 control-label">内容关键字</label>
<div class="col-lg-3">
<input type="text" class="form-control" placeholder="内容关键字" ng-model="$parent.content1">
</div>
<div class="col-lg-1">
<select class="form-control" autocomplete="off" ng-model="$parent.selectContent">
<option selected="selected">AND</option>
<option>OR</option>
</select>
</div>
<div class="col-lg-3">
<input type="text" class="form-control" placeholder="内容关键字" ng-model="$parent.content2">
</div>
</div>
<div class="form-group">
<div class="col-md-offset-9">
<button type="submit" class="btn btn-default" ng-click="search()">查询</button>
</div>
</div>
</form>
(3)—>调用 search() 函数(javascripts/news.js)
获取用户传入的文本内容,先检查参数是否存在问题,再通过GET请求传给路由。如果获取到了数据,则显示表格查询结果并进行分页;也有可能因为没有获取到用户登录信息,返回登录页面。
$scope.search = function () {
var title1 = $scope.title1;
var title2 = $scope.title2;
var selectTitle = $scope.selectTitle;
var content1 = $scope.content1;
var content2 = $scope.content2;
var selectContent = $scope.selectContent;
var sorttime = $scope.sorttime;
// 检查用户传的参数是否有问题
//用户有可能这样输入:___ and/or 新冠(直接把查询词输在了第二个位置)
if(typeof title1=="undefined" && typeof title2!="undefined" && title2.length>0){
title1 = title2;
}
if(typeof content1=="undefined" && typeof content2!="undefined" && content2.length>0){
content1 = content2;
}
// 用户可能一个查询词都不输入,默认就是查找全部数据
var myurl = `/news/search?t1=${title1}&ts=${selectTitle}&t2=${title2}&c1=${content1}&cs=${selectContent}&c2=${content2}&stime=${sorttime}`;
$http.get(myurl).then(
function (res) {
if(res.data.message=='data'){
$scope.isisshowresult = true;
$scope.initPageSort(res.data.result)
}else {
window.location.href=res.data.result;
}
},function (err) {
$scope.msg = err.data;
});
};
(4)—>传给路由(routes/news.js)
在后端查询前,先检验用户是否是登录状态,若不是则返回登录界面
router.get('/search', function(request, response) {
console.log(request.session['username']);
//sql字符串和参数
if (request.session['username']===undefined) {
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});
})
}
});
在连接池文件 newsDao.js 中对关键词和连接词执行拼路由操作,将查询结果按照新闻发表时间排序
search :function(searchparam, callback) {
// 组合查询条件
var sql = 'select * from fetches ';
if(searchparam["t2"]!="undefined"){
sql +=(`where title like '%${searchparam["t1"]}%' ${searchparam['ts']} title like '%${searchparam["t2"]}%'`);
}else if(searchparam["t1"]!="undefined"){
sql +=(`where title like '%${searchparam["t1"]}%' `);
};
if(searchparam["t1"]=="undefined"&&searchparam["t2"]=="undefined"&&searchparam["c1"]!="undefined"){
sql+='where ';
}else if(searchparam["t1"]!="undefined"&&searchparam["c1"]!="undefined"){
sql+='and ';
}
if(searchparam["c2"]!="undefined"){
sql +=(`content like '%${searchparam["c1"]}%' ${searchparam['cs']} content like '%${searchparam["c2"]}%' `);
}else if(searchparam["c1"]!="undefined"){
sql +=(`content like '%${searchparam["c1"]}%' `);
}
if(searchparam['stime']!="undefined"){
if(searchparam['stime']=="1"){
sql+='ORDER BY publish_date ASC ';
}else {
sql+='ORDER BY publish_date DESC ';
}
}
sql+=';';
console.log(sql);
pool.getConnection(function(err, conn) {
if (err) {
callback(err, null, null);
} else {
conn.query(sql, function(qerr, vals, fields) {
conn.release(); //释放连接
callback(qerr, vals, fields); //事件驱动回调
});
}
});
}
结果展示
以折线图图为例,实现页面上用表单来输入查任意词频率变化情况
(1)点击折线图—>调用 showsearchline() 函数
$scope.showsearchline = function (){
$scope.isShowtext = true;
$scope.isShow = true;
$scope.isShow2 = false;
$scope.isShow22 = false;
//再次回到查询页面时,表单里要保证都空的
$scope.searchline=undefined;
}
(2)—>展示搜索框,前端代码实现(news.html)
<div ng-show="isShowtext" style="width: 1300px;position:relative; top:70px;left: 80px">
<form>
查询词: <input type="text" ng-model="searchline">
<button type="submit" ng-click="line()">查询</button>
</form>
</div>
(3)—>点击搜索框,调用 line() 函数(javascripts/news.js)
$scope.line = function () {
var line_keyword = $scope.searchline;
var myurl = `/news/line?keyword=${line_keyword}`;
$scope.isShow = false;
$scope.isShow2 = false;
$scope.isShow22 = false;
//$scope.isShowtext = false;
$http.get(myurl).then(
function (res) {
if(res.data.message=='url'){
window.location.href=res.data.result;
}else {
var myChart = echarts.init(document.getElementById("main1"));
option = {
title: {
text: `“${line_keyword}”该词在新闻中的出现次数随时间变化图`
},
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);
}
}
});
};
(4)—>传给路由(routes/news.js)
router.get('/line', function(request, response) {
//sql字符串和参数
console.log(request.session['username']);
//sql字符串和参数
if (request.session['username']===undefined) {
// response.redirect('/index.html')
response.json({message:'url',result:'/index.html'});
}else {
//var keyword = '疫情'; //也可以改进,接受前端提交传入的搜索词
var keyword = request.query.keyword;
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依赖太多,在运行过程中常常报错,这里使用基于 IKAnalyzer 字典分词器的 node.js 实现——node-analyzer
npm install node-analyzer -save
//对content分词
var Segmenter = require('node-analyzer');
var segmenter = new Segmenter();
var words = segmenter.analyze(content).split(' ');
(文件位于 javascripts/stopwords.js )
这里要先引入停用词的概念:
停用词(Stop Words)是指在信息检索中,为节省存储空间和提高搜索效率,在处理自然语言文本会自动过滤掉某些字或词。大致分为两类:
所以通常会把这些词从问题中移去,从而提高搜索性能。
分词处理文档存储在 javasripts/stopwords.js 中
先创建停用词集:从github下载中文常用停用词表(ChineseStopWords.txt),读取文档将每一行停用词存储到集合中
var fs = require("fs");
var stop_words = new Set();
fs.readFile('./ChineseStopWords.txt','utf-8', function(err, data) {
if(err){
console.log(err);
}else {
var all_words= data.split('\n');
for(var i = 0; i < all_words.length; i++) {
stop_words.add(all_words[i]);
}
}
})
在数据库中创建新表以存储分词结果
CREATE TABLE Splitwords (
id_fetches int,
word varchar(50) DEFAULT NULL
);
分词处理:对爬虫内容,先用正则表达式去掉一些无用的字符与高频但无意义的词;进行分词后,逐个处理词条判断是否存在于停用词表当中,将有效词条存储到数据库的 Splitwords 表中。
const regex = /[\t\s\r\n\d\w]|[\+\-\(\),\.。,!?《》@、【】"'::%-\/“”]/g;
var fetchSql = "select id_fetches,content from fetches;";
newsDAO.query_noparam(fetchSql, function (err, result, fields) {
result.forEach(function (item){
var segmenter = new Segmenter();
var newcontent = item["content"].replace(regex,'');
if(newcontent.length !== 0){
var words = segmenter.analyze(newcontent).split(' ');
var id_fetch = item["id_fetches"];
words.forEach(function(word){
if(!stop_words.has(word)&&word.length>1){
var insert_word_Sql = 'INSERT INTO Splitwords2(id_fetches,word) VALUES(?,?);';
var insert_word_Params = [id_fetch, word];
newsDAO.query(insert_word_Sql, insert_word_Params, function(err){
if(err)console.log(err);
});
}
});
}
});
});
已经存储好了分词结果,前端的搭建以及前后端的连接与实现布尔查询操作类似,这里主要介绍网站的后端查询:
对读入进来的关键词,同样先进行正则表达式的处理
因为 mysql 中没有 intersect 并集操作,这里折腾了很久,最后找到达到同样效果的数据库操作语句:select * from fetches where id_fetches in(select id_fetches from(select id_fetches from Splitwords where word like '${word1}' UNION ALL select id_fetches from Splitwords where word like '${word2}' UNION ALL ……)a GROUP BY id_fetches HAVING COUNT(*) = n);
var segmenter = new Segmenter();
var sql ='select * from fetches ';
if(searchparam["t"]!="undefined"){
if(searchparam["t"].length<=3){
sql+=(`where id_fetches in (select id_fetches from Splitwords where word like '${searchparam["t"]}')`);
}else{
var newcontent = searchparam["t"].replace(regex,'');
var words = segmenter.analyze(newcontent).split(' ');
var n=1;
//默认第一个分词词语是有效词,像“的”这样的无效词一般出现在词语之间
sql+=(`where id_fetches in (select id_fetches from(select id_fetches from Splitwords where word like '${words[0]}'`);
for(var i=1;i<words.length;i++){
if(!stop_words.has(words[i])&&words[i].length>1){
sql+=(` UNION ALL select id_fetches from Splitwords where word like '${words[i]}'`);
n++;
}
}
sql+=`)a GROUP BY id_fetches HAVING COUNT(*) = ${n})`;
}
}
if(searchparam['stime']!="undefined"){
if(searchparam['stime']=="1"){
sql+='ORDER BY publish_date ASC ';
}else {
sql+='ORDER BY publish_date DESC ';
}
}
sql+=';';
实现效果在开始便展示了~
依据Elastic Search的相关性文档打分机制:TF-IDF
它使用了被搜索词条的频率和它有多常见来影响得分,从两个方面理解:
总而言之,一个词条在一篇文章中出现次数越多, 同时在所有文档中出现次数越少, 越能够代表该文章,越能与其它文章区分开来。
TF是词频(term frequency),表示词条在所在文档中出现的频率:TF=(某词在文档中出现的次数/文档的总词量)
这样可以防止结果偏向过长的文档,同一个词语在长文档里通常会具有比短文档更高的词频。
IDF是逆文档频率(inverse document frequency),表示词条在所有文档中出现的频率:IDF=loge(语料库中文档总数/包含该词的文档数)
比如一些专业名词的IDF值应该高,而一个极端的情况,如果一个词在所有的文本中都出现,那么它的IDF值应该为0。
计算公式:TF-IDF=TFxIDF,TFIDF值越大表示该特征词对这个文本的重要性越大
我们先遍历Splitwords表中的词条,对每个词条计算权重,再存储在带权重的新表WeightSearch中
CREATE TABLE WeightSearch (
id_fetches int,
word varchar(50) DEFAULT NULL,
weight float
);
先整理我们需要的数据库操作语句:
获取所有的数据以遍历select id_fetches,word from Splitwords;
计算TF:
select count(*) as num from Splitwords where word='${word}' and id_fetches=${id};
select count(*) as num from Splitwords where id_fetches=${id};
计算IDF:
select count(distinct id_fetches) as num from Splitwords;
select count(distinct id_fetches) as num from Splitwords where word='${word}';
将结果插入数据表中INSERT INTO WeightSearch VALUES (id,word,weight)
完整代码如下,存储在 javascripts/rank.js 文件中
注意:
代码层级嵌套在回调函数中是因为函数外部无法获取内部的值;
返回数据类型是[RowDataPacket {num:‘1055’}],用res[0].num来获取数值。
var newsDAO = require('../../dao/newsDAO');
var fetchGetSql = 'select id_fetches,word from Splitwords;';
newsDAO.query_noparam(fetchGetSql, function(err, result){
var tn;
var getTotalNum = 'select count(distinct id_fetches) as num from Splitwords;';
newsDAO.query_noparam(getTotalNum, function(err,res){
tn = res[0].num;//文档总数
result.forEach(function(item){
var id=item.id_fetches;
var word=item.word;
var tf1;//该文档中word出现的次数
var gettf1=`select count(*) as num from Splitwords where word='${word}' and id_fetches=${id};`
newsDAO.query_noparam(gettf1, function(err,res){
tf1=res[0].num;
var tf2;//该文档中的词条数目
var gettf2=`select count(*) as num from Splitwords where id_fetches=${id};`
newsDAO.query_noparam(gettf2, function(err,res){
tf2=res[0].num;
var tf=tf1/tf2;
var idf2;//包含词条t的文档数
var getidf2=`select count(distinct id_fetches) as num from Splitwords where word='${word}';`
newsDAO.query_noparam(getidf2, function(err,res){
idf2=res[0].num;
var weight=tf*Math.log(tn/idf2);
if(weight!=undefined){
//写入带权重的数据表
console.log(weight);
var insert_word_Sql = 'INSERT INTO WeightSearch(id_fetches,word,weight) VALUES(?,?,?);';
var insert_word_Params = [id,word,weight];
newsDAO.query(insert_word_Sql, insert_word_Params, function(err){
if(err)console.log(err);
});
}
});
});
});
});
});
});
在数据库表中查询存储结果
修改分词查询后端的查询语句(位于 dao/newsDao.js 文件中)
var sql ='select * from fetches ';
if(searchparam["t"]!="undefined"){
sql+=(`, WeightSearch where WeightSearch.id_fetches = fetches.id_fetches and word like "${searchparam["t"]}" order by weight desc`);
}
sql+=';';
为了给用户更好的使用体验,当然要给网页添加样式,这里用到外部引入的方式,实现内容与样式分离。
以给登录界面添加样式为例:
在 index.html 文件中使用 < link > 标签链接到外部样式表
<link rel="stylesheet" type="text/css" href="stylesheets/index.css">
在 index.css 文件中设置样式
body {
padding-top: 90px;
background-image:url("../images/bg.jpg");
background-repeat:no-repeat;
background-size:100% 100%;
background-attachment:fixed;
}
h2{
color: #286288be;
text-align:center;
}
.imgBox{
position: absolute;
top: 0%;
left: 46%;
}
.panel-login {
border-color: #ccc;
-webkit-box-shadow: 0px 2px 3px 0px rgba(0,0,0,0.2);
-moz-box-shadow: 0px 2px 3px 0px rgba(0,0,0,0.2);
box-shadow: 0px 2px 3px 0px rgba(0,0,0,0.2);
}
.panel-login>.panel-heading {
color: #00415d;
background-color: #fff;
border-color: #fff;
text-align:center;
}
.panel-login>.panel-heading a{
text-decoration: none;
color: #666;
font-weight: bold;
font-size: 15px;
-webkit-transition: all 0.1s linear;
-moz-transition: all 0.1s linear;
transition: all 0.1s linear;
}
.panel-login>.panel-heading a.active{
color: #029f5b;
font-size: 18px;
}
.panel-login>.panel-heading hr{
margin-top: 10px;
margin-bottom: 0px;
clear: both;
border: 0;
height: 1px;
background-image: -webkit-linear-gradient(left,rgba(0, 0, 0, 0),rgba(0, 0, 0, 0.15),rgba(0, 0, 0, 0));
background-image: -moz-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.15),rgba(0,0,0,0));
background-image: -ms-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.15),rgba(0,0,0,0));
background-image: -o-linear-gradient(left,rgba(0,0,0,0),rgba(0,0,0,0.15),rgba(0,0,0,0));
}
.panel-login input[type="text"],.panel-login input[type="email"],.panel-login input[type="password"] {
height: 45px;
border: 1px solid #ddd;
font-size: 16px;
-webkit-transition: all 0.1s linear;
-moz-transition: all 0.1s linear;
transition: all 0.1s linear;
}
.panel-login input:hover,
.panel-login input:focus {
outline:none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
border-color: #ccc;
}
.btn-login {
background-color: #59B2E0;
outline: none;
color: #fff;
font-size: 14px;
height: auto;
font-weight: normal;
padding: 14px 0;
text-transform: uppercase;
border-color: #59B2E6;
}
.btn-login:hover,
.btn-login:focus {
color: #fff;
background-color: #53A3CD;
border-color: #53A3CD;
}
.forgot-password {
text-decoration: underline;
color: #888;
}
.forgot-password:hover,
.forgot-password:focus {
text-decoration: underline;
color: #666;
}
.btn-register {
background-color: #1CB94E;
outline: none;
color: #fff;
font-size: 14px;
height: auto;
font-weight: normal;
padding: 14px 0;
text-transform: uppercase;
border-color: #1CB94A;
}
.btn-register:hover,
.btn-register:focus {
color: #fff;
background-color: #1CA347;
border-color: #1CA347;
}
最终实现效果: