web final project
作业要求
基本要求:
1、用户可注册登录网站,非注册用户不可登录查看数据
2、用户注册、登录、查询等操作记入数据库中的日志
3、实现查询词支持布尔表达式 (比如“新冠 AND 肺炎”或者“新冠 OR 肺炎”)
4、爬虫数据查询结果列表支持分页和排序
5、用Echarts或者D3实现3个以上的数据分析图表展示在网站中
扩展要求(非必须):
1、实现对爬虫数据中文分词的查询
2、实现查询结果按照主题词打分的排序
3、用Elastic Search+Kibana展示爬虫的结果
基本要求完成过程
用户登陆与记录用户操作的日志(任务一与任务二)
首先,我们需要为用户在数据库中创建用户的信息表,和用户的操作表。
-创建用户信息数据表
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;
接下来,我们需要配置mysql,使得nodejs能够连接到我们的数据库。
我们在mysqlConf.js文件下加入以下内容。
module.exports = {
mysql: {
host: 'localhost',
user: 'root',
password: 'root',
database:'crawl',
// 最大连接数,默认为10
connectionLimit: 10
}
};
这样,nodejs就可以与本地mysql连接了。
接下来,我们来实现登陆页面。
登陆页面中应该有登陆和注册两种选项,并且用户能够对用户的输入有所反馈。
我们在public/index.html和routes/users.js下写入登陆页面的内容。它的内在逻辑是:
1.当用户选择登录时,检查用户输入的账户密码是否与数据库中的一致,如果一致,则进入应用页面,否则提示错误信息。
2. 当用户选择注册时,
a. 检查输入的两次密码是否一致,如果不一致则提示用户输入密码不一致;如果一致则提示用户注册成功并且将用户的注册信息加入数据库。
b. 如果用户的密码或者用户名为空,则提示用户名或为不能为空;.
c. 如果用户名已经存在则提示用户用户名已经存在。
最后登陆页面结果如图所示。
登陆失败情况
接下来,我们看看数据库的状态
显示一个注册成功的用户。
这里是用户lm123的部分操作记录。
所以任务一与任务二我们都完成了。
查询词的实现以及分页结果的展示。(任务三和四)
当用户进入应用之后,news.html文件会向用户展示界面, 如下图所示。
用户可以选择检索选项进行布尔查询。
我们将代码实现写在public/search.html文件里面。查询界面如图所示。
用户分标题关键字和内容关键字进行查询,也可以自行选择AND或者OR进行查询。
当用户输入查询字段之后,public/javascripts/news.js文件代码会处理该字段。这个文件的实现逻辑是:
1.检查用户传的参数是否有问题。检查用户是否输入了一些无意义字符。
2. 如果用户一个查询词都不输入,那么默认就是查找全部数据。
接下来,根据用户的输入,我们利用sql在数据库中查询,这里我们主要用到了newsDAO.search函数,它位于dao/newsDAO.js中。它根据用户的输入内容在mysql对新闻进行查询并且返回查询结果。
得到查询的结果之后进行分页展示。分页展示的实现逻辑是
3. 首先我们定义每页显示的数据量,这样一个页面所显示的数据量就不会大于这个数。
4. 接下来打印当前选中页。最多显示分页数5,如果大于5页则开始分页转换。
5. 设置当前选中页样式并且设置上下页的转换。
这样,我们的分页操作就完成了。
查询如图所示。
查询结果如下图所示。
至此,我们已经完成了作业三分次查询和作业四分页展示。
用Echarts或者D3实现3个以上的数据分析图表展示在网站中(任务五)
四个图标的实现在public/javascript/news.js中实现。
柱状图展示新闻发布数随时间的变化。结果如图所示
饼状图显示作者发不新闻数量。
折线图显示某一个词的热度变化。
词云显示所有新闻的jieba词云
至此,我们五个任务都已经完成了。下面是扩展任务。
扩展任务
1. 实现对爬虫数据中文分词的查询
下面的分词代码写在javascript/split.js 里面
(1)分词工具下载使用
我们可以利用现有的分词工具包对用户提交的文本进行分词。
首先,我们需要安装粉刺工具,这里我们选用node-analyzer。
安装:npm install node-analyzer -save
(2)去除停用此
在每个查询语句之中,停用词都是没有用的,所以我们需要去除停用词。
下面的代码是读取停用词的代码,它读取StopWords.txt文件里面的停用词,将其存入stop_words
var fs = require("fs");
var stop_words = new Set();
fs.readFile('./StopWords.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);
});
}
});
}
});
});
最后是分词查询。
对于已经存储好了分词结果,前端的搭建以及前后端的连接与实现布尔查询操作类似,这里主要介绍网站的后端查询:
- 对读入进来的关键词,同样先进行正则表达式的处理
- 若用户提交的字符串没有超过三个中文字,直接到数据库索引词汇;否则先进行分词操作,再对每个词条进行判断,这里把在停用词表中出现的以及长度小于等于一的词条当作无效词,然后在 Splitwords 表中获取每个分词的id字段,最后取 id 字段交集查询 fetches 表中完整的新闻爬取信息。
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+=';';
这样子我们就完成了分词查询了!
部分的分词结果如下图所示
2. 实现查询结果按照主题词打分的排序
首先,我们创建一个新表WeightSearch用于存储每个词条计算权重。
CREATE TABLE WeightSearch (
id_fetches int,
word varchar(50) DEFAULT NULL,
weight float
);
我们采用Elastic Search的相关性文档打分机制:TF-IDF打分。它使用了被搜索词条的频率和它有多常见来影响得分,一个词条在某篇文档中出现的次数越多,该文档就越相关;一个词条如果在不同的文档中出现的次数越多,它就越不相关。所以,一个词条在一篇文章中出现次数越多, 同时在所有文档中出现次数越少, 越能够代表该文章,越能与其它文章区分开来。
计算公式:TF-IDF=TFxIDF,TFIDF值越大表示该特征词对这个文本的重要性越大。
打分的实现方式(所有代码写在javascripts/rank.js里面):
- 获取所有的数据遍历splitwords
select id_fetches,word from Splitwords;
- 计算对应id文档中word出现次数:
select count(*) as num from Splitwords where word='${word}' and id_fetches=${id};
- 计算对应id文档中词条总数目
select count(*) as num from Splitwords where id_fetches=${id};
- 文档总数(获取一次即可)
select count(distinct id_fetches) as num from Splitwords;
- 计算包含该word的文档数
select count(distinct id_fetches) as num from Splitwords where word='${word}';
- 最后将结果插入数据表中
INSERT INTO WeightSearch VALUES (id,word,weight)
所有的打分代码如下所示
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);
});
}
});
});
});
});
});
});
部分的权重存储结果如下图所示
最后的查询结果展示
这样我们的扩展任务二就完成了。