web编程期末作业

web编程期末项目

实验要求

基于第一个项目爬虫爬取的数据,完成数据展示网站。

基本要求

1、用户可注册登录网站,非注册用户不可登录查看数据
2、用户注册、登录、查询等操作记入数据库中的日志
3、爬虫数据查询结果列表支持分页和排序
4、用Echarts或者D3实现3个以上的数据分析图表展示在网站中
5、实现一个管理端界面,可以查看(查看用户的操作记录)和管理(停用启用)注册用户。

扩展要求

1、实现对爬虫数据中文分词的查询
2、实现查询结果按照主题词打分的排序
3、用Elastic Search+Kibana展示爬虫的数据结果

项目实现

1. 数据库准备

在第一次实验的基础上,需要在数据库中额外增加如下的表:

user

用于记录所有用户信息,记录已注册用户的姓名、密码和注册时间,用于实现注册后存储用户的信息、检验用户是否存在、检验用户名与密码是否匹配、管理员查看、检验登录等操作。此外,为了实现第五个基本要求,又在该数据库上增加了一个属性,用于控制用户当前处于停用还是启用状态。

对应的构造数据库语句如下:

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,  
`state` int(4),  
PRIMARY KEY (`id`),  
UNIQUE KEY `username_UNIQUE` (`username`))ENGINE=InnoDB DEFAULT CHARSET=utf8;
user_action

用于记录用户的注册、登录和查询等操作,记入数据库中的日志,用于管理员查询功能的实现。

对应的构造数据库语句如下:

CREATE TABLE `crawl`.`user_action` (  
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,  
`username` VARCHAR(45) NOT NULL,  
`request_time` VARCHAR(45) NOT NULL,  
`request_url` VARCHAR(300) NOT NULL,  
`request_op` int(4),     
PRIMARY KEY (`id`))ENGINE=InnoDB DEFAULT CHARSET=utf8;
stage

用于记录用户所处状态对应列表,用于停用、启用用户。

对应的构造数据库语句如下:

CREATE TABLE `crawl`.`stage` (  
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,  
`stat` VARCHAR(255),  
PRIMARY KEY (`id`))ENGINE=InnoDB DEFAULT CHARSET=utf8;
数据库连接

对应的数据库的连接语句如下:

var db = mysql.createConnection({
    host: '1.116.1.85',
    port: 3306,
    user: 'root',
    password: 'root',
    database: 'crawl'
});

2. 第一次实验后的优化

第一次实验做完后感觉前端实在太难看了,在第二次实验中对前端进行了一次优化,使用了layUI和semantic UI作为前端框架进行美化。使用jquery可以快速访问每个页面dom对象以及处理页面的动态加载,利用ajax向后端每个restful接口传送相应的参数以及数据。在第一次记实验优化后的基础上再继续实现第二次实验要求的功能。

主页面效果:
在这里插入图片描述

3. 用户可注册登录网站,非注册用户不可登录查看数据

登录

对于登录功能,注意在判断了数据库访问没有问题之后,需要判断用户名和密码均存在输入且输入正确之后,再判断输入的用户名与密码是否匹配,只有在两者匹配的条件下才返回登录成功。

前端页面:

$(document).on('click','#submit',function (data) {
        var username = $('#username').val();
        var password = $('#password').val();
        var check=$('#check').val();
        if(username===''){
            layer.alert("请输入用户名", {icon: 3});
        }else if(password===''){
            layer.alert("请输入密码", {icon: 3});
        }else {
            $.ajax({
                url:"/mysql/login",
                type:"post",
                async: false,
                data:{
                    "username":username,
                    "password":password
                },
                dataType:"json",
                statusCode:{
                    403:function(){
                        layer.alert('用户名不存在', {icon: 2});
                    },
                    405:function () {
                        layer.alert('密码输入错误', {icon: 2});
                    },
                    409:function () {
                        layer.alert('用户被停用', {icon: 2});
                    },
                    200:function () {
                        log(username,1,location.href);
                        layer.alert('登录成功', {icon: 1}, function () {
                            location.href='./查询.html';
                        });
                    }
                }
            })
        }
    })

该部分对应路由代码:

router.post('/login', function (req, res, next) {
    var username = req.param('username');
    var password = req.param('password');
    var sql = "SELECT * FROM user where username= '"+username+"';";
    console.log(sql);
    db.query(sql, function (err, data) {
        if (err) {
            console.log("数据库访问出错\n", err);
        } else {
            console.log("OK");
        }
        if (data.length < 1) {
            res.writeHead(403, {
                "Content-Type": "application/json"
            });
            console.log(data);
            res.write(JSON.stringify(data));
            res.end();
        }else {
            if(data[0].password===password){
                if(data[0].state===1){
                    res.writeHead(409, {
                        "Content-Type": "application/json"
                    });
                    console.log(data);
                    res.write(JSON.stringify(data));
                    res.end();
                }else {
                    res.writeHead(200, {
                        "Content-Type": "application/json"
                    });
                    console.log(data);
                    res.write(JSON.stringify(data));
                    res.end();
                }
            }else {
                res.writeHead(405, {
                    "Content-Type": "application/json"
                });
                console.log(data);
                res.write(JSON.stringify(data));
                res.end();
            }
        }
    });
});

成功登陆效果:
在这里插入图片描述

注册

对于注册功能,需要注意在判断了数据库访问没有问题之后,需要判断用户名和密码均存在输入且输入正确之后,再判断输入的用户名没有已注册过且两次输入密码一致,只有在用户名不存在且两次密码一致的条件下才返回注册成功。

前端页面:

 $(document).on('click','#submit',function (data) {
        var username = $('#username').val();
        var password = $('#password').val();
        var check=$('#check').val();
        if(username===''){
            layer.alert("请输入用户名", {icon: 3});
        }else if(password===''){
            layer.alert("请输入密码", {icon: 3});
        }else if(check===''){
            layer.alert("请输入确认密码", {icon: 3});
        }else if(password!==check){
            layer.alert("两次密码不一致", {icon: 5});
        }else {
            $.ajax({
                url:"/mysql/register",
                type:"post",
                async: false,
                data:{
                    "username":username,
                    "password":password
                },
                dataType:"json",
                statusCode:{
                    403:function(){
                        layer.alert('用户名已被占用', {icon: 2});
                    },
                    200:function () {
                        log(username,2,location.href);
                        layer.alert('注册成功', {icon: 1}, function () {
                            location.href='./登录.html';
                        });
                    }
                }
            })
        }
    })

该部分对应路由代码:

router.post('/register', function (req, res, next) {
    var username = req.param('username');
    var password = req.param('password');
    var sql = "SELECT * FROM user where username= '"+username+"';";
    console.log(sql);
    db.query(sql, function (err, data) {
        if (err) {
            console.log("数据库访问出错", err);
        } else {
            console.log("OK");
        }
        if (data.length > 0) {
            res.writeHead(403, {
                "Content-Type": "application/json"
            });
            console.log(data);
            res.write(JSON.stringify(data));
            res.end();
        }else {
            res.writeHead(200, {
                "Content-Type": "application/json"
            });
            db.commit("insert into user values (NULL,\""+username+"\",\""+password+"\",now(),0);");
            res.end();
        }
    });
});

注册用户名被占用效果:
在这里插入图片描述

实验扩展:密码加密

由于我们并不希望存在数据库中的用户密码是直接可读的,那么万一数据库被攻击了,用户的密码信息就会全部泄漏,所以需要对用户密码进行密码加密处理。

这里参考了文章: Nodejs 密码加密存储,实现用户密码加密。

加密主要流程是,首先我们得到明文的hash值,进行计算获取MD5明文hash值,再随机生成加盐值并插入,并通过MD5插入加盐值得到的hash,从而得到最终的密文。在验证过程中,通过得到用户的密码哈希值和对应盐值,将盐值混入用户输入的密码,并且使用同样的哈希函数进行加密,再比较上一步的结果和数据库储存的哈希值是否相同,如果相同那么密码正确,反之密码错误。

bcrypt是一个第三方密码加密库,它自带加盐和慢哈希功能,通过引用该库可以实现密码加密。示例代码如下:

var bcrypt  = require('bcrypt');
var password = "helloworld";

bcrypt.hash(password, 10, function(err, encryptPassword) {
    console.log("bcrypt password:"+encryptPassword);
    bcrypt.compare(password, encryptPassword, function(err, res) {
    if (res === true)
        console.log("The password is true");
    })
});

4. 用户注册、登录、查询等操作记入数据库中的日志

记录日志

构建user_action数据库用于存储相应的操作,对于用户的行为来说,分为注册、登录、查询三类操作,以一个变量op来记录,三种操作对应于op取值为1、2、3。在用户每次在页面上操作后,会在路由处执行"insert into crawl.user_action values (null,'"+username+"',now(),'"+url+"',"+op+");"语句,记录当前操作的执行用户、执行时间以及执行操作名,用于描述用户执行的相应操作。

该部分对应路由代码:

router.post('/log', function (req, res, next) {
    var username = req.param('username');
    var op = req.param('op');
    var url = req.param('url');
    var sql = "insert into crawl.user_action values (null,'"+username+"',now(),'"+url+"',"+op+");";
    console.log(sql);
    db.commit(sql);
    res.writeHead(200, {
        "Content-Type": "application/json"
    });
    res.end();
});

数据库存储效果:
在这里插入图片描述

5. 爬虫数据查询结果列表支持分页和排序

这里对于前端表格的制作,第一次实验我用的是bootstrap,后来我在bootstrap-table和layui表格中比较了一下选择了layui.table,因为个人体验下来layui的文档是真的做的非常好,各种功能的实例做的也是好且非常容易上手,样式也更好看,所以最后选择了layui来实现表格展示以及分页和排序。

分页

在layui中,limit 表示了页面显示数据条目数,pag表示了当前页码,countItem为数据总数。layui table 分页实现思路为,table开启分页之后会默认传递page limit 两个参数,在后台获取String sql = "select * from table limit ((page - 1)*limit),limit;"然后获取需要显示数据的总条目数通过count(*) 查询,通过"select * from table" 获取全部数据条目数。

排序

该功能在第一次实验中已经实现,这里在第一次实验的基础上实现了进一步的优化。在之前的排序基础上,用户可以手动选择实现倒序还是正序排序,也可以选择进行排序的属性。需要排序的属性直接在sort值上加一个ture/false即可。

对应前端代码:

layui.use('element',function () {
        var element=layui.element;
    });

    layui.use('table', function(){
        var table = layui.table;

        //第一个实例
        table.render({
            elem: '#demo'
            ,size: 'lg'
            ,height:  'full-270'
            ,url: 'http://localhost:3000/mysql/search' //数据接口
            ,cols: [[ //表头
                {field: 'index', type: 'numbers'}
                ,{field: 'id_fetches', type: 'id_fetches',width:80,hide:true}
                ,{field: 'url', title: '链接', width:200}
                ,{field: 'source_name', title: '来源', width:200}
                ,{field: 'source_encoding', title: 'source_encoding', width:80,hide:true}
                ,{field: 'title', title: '标题', width:500}
                ,{field: 'keywords', title: '关键词', width: 200}
                ,{field: 'author', title: 'author', width: 80,hide:true}
                ,{field: 'publish_date', title: 'publish_date', width: 80,hide:true}
                ,{field: 'crawltime', title: 'crawltime', width: 80,hide:true}
                ,{field: 'content', title: 'content', width: 80,hide:true}
                ,{field: 'createtime', title: '发布时间', width: 200,sort:true}
            ]]
            ,page: true
            ,limits: [3,5,10]  //一页选择显示3,5或10条数据
            ,limit: 10  //一页显示10条数据
            ,parseData: function(res){ //将原始数据解析成 table 组件所规定的数据,res为从url中get到的数据
                var result;
                console.log(this);
                console.log(JSON.stringify(res));
                if(this.page.curr){
                    result = res.data.slice(this.limit*(this.page.curr-1),this.limit*this.page.curr);
                }
                else{
                    result=res.data.slice(0,this.limit);
                }
                return {
                    "code": res.code, //解析接口状态
                    "msg": res.msg, //解析提示文本
                    "count": res.count, //解析数据长度
                    "data": result //解析数据列表
                };
            }
        });
        $(document).on('click','#submit',function (data) {
            var bt=$('#bt').val();
            var gjc=$('#gjc').val();
            var zz=$('#zz').val();
            var rdfx=$('#rdfx').val();
            table.reload('demo',{
                where:{
                    bt:bt,
                    gjc:gjc,
                    zz:zz,
                    rdfx:rdfx
                },
                page:{
                    curr:1
                }
            })
        })
    });

后端查询功能、热度分析的函数和第一次一致,没有改动,这里不再赘述。

效果:
在这里插入图片描述

实验扩展:交集(OR)查询

如果对于查询不加以限制,那么实现的实际上是一个并集(and)查询,即返回的是所有只要有一条满足用户需求的信息,而在实际情况中用户应当想要的是一个交集查询,对于每一条用户的输入都是对于查询的限制。

为了实现上述要求,增添了一个tag标识符,当在查询语句中已经有一句查询时(例如查询title中包含“中”),那么如果是在此基础上还要求取并集(例如查询keywords中包含“文化”),那么需要在select查询语句中增加一个and,所以解决办法就是使用标识符判断是否已经存在一句查询了,如果有的话需要增加and,如果没有的话将标识符修改为ture,说明当前查询中已经存在了一句查询了。

这样的好处是可以支持用户对于既有标题限制又有关键词限制的查询,同样可以查询到结果,而非像之前一样搜索只能限制在某一个属性中进行查询了。

交集查询实例,查询标题和关键词都包含北京的新闻:

在这里插入图片描述

6. 用Echarts或者D3实现3个以上的数据分析图表展示在网站中

柱形图

前端代码,使用echarts实现:

layui.config({
        version: 1,
        base: './javascripts/'
    }).use(['layer','element', 'echarts'], function() {});
    var element = layui.element,
        $ = layui.jquery,
        echarts = layui.echarts;


    function generateChart(data) {
        var myChart = echarts.init(document.getElementById('EchartZhu'));

        myChart.clear();
        var optionchart = {
            title: {
                text: '每日发布的新闻数'
            },
            tooltip: {},
            legend: {
                data: ['数量/条']
            },
            xAxis: {
                data: data.map(function (obj) {
                    return obj.publish_date.substr(6,4)
                })
            },
            yAxis: {
                type: 'value'
            },
            series: [{
                name: '数量/条',
                type: 'bar', //柱状
                data: data.map(function (obj) {
                    return obj.number
                }),
                itemStyle: {
                    normal: { //柱子颜色
                        color: '#009688'
                    }
                },
            }]
        };

        myChart.setOption(optionchart, true);
    }
    $.ajax({
        url:"/mysql/zhu",
        type:"get",
        async: false,
        data:{
        },
        dataType:"json",
        statusCode:{
            200:function (msg) {
                console.log(msg);
                console.log(msg.map(function (obj) {
                    return obj.publish_date.substr(6,4);
                }));
                generateChart(msg);
            }
        }
    });

这里为了实现将爬取的新闻按照新闻发布时间统计形成对应的柱状图,需要实现的对应查询语句为:select publish_date, count(*) as number from crawl.fetches group by publish_date;

对应路由代码:

router.get('/zhu',function(req,res,next){
    var sql="select publish_date, count(*) as number from crawl.fetches group by publish_date;";
    console.log(sql);
    db.query(sql,function(err,data){
        if(err){
            console.log("数据库访问出错",err);
        }else{
            console.log("OK");
        }
        res.writeHead(200, {
            "Content-Type": "application/json"
        });
        console.log(JSON.stringify(data));
        res.end(JSON.stringify(data));
    });
});

最终柱状图效果:

在这里插入图片描述

饼状图

这里为了实现将爬取的新闻按照新闻发布来源统计形成对应的饼状图,需要实现的对应查询语句为:select source_name as name , count(*) as value from crawl.fetches where source_name != '' group by source_name;

同理柱形图,实现饼状图效果:
在这里插入图片描述

词云图

这里为了实现将爬取的新闻按照新闻关键词统计形成对应的词云图,需要实现的对应查询语句为:"select * from (SELECT" + substring_index( substring_index( crawl.fetches.keywords, ',', b.help_topic_id + 1 ), ',',- 1 ) name" +"FROM crawl.fetches" +"JOIN mysql.help_topic b ON b.help_topic_id < ( length( crawl.fetches.keywords ) - length( REPLACE ( crawl.fetches.keywords, ',', '' ) ) + 1 )) as fbn" + "where fbn.name != '' and fbn.name not like '% %';"

同理柱形图,实现词云图效果:
在这里插入图片描述

实验扩展:结合查询结果的折线图

此外,出了生成静态图片之外,还希望和之前的查询功能结合,实现一个用户输入查询即会动态生成图片的功能。

对应的前端为:

    layui.config({
        version: 1,
        base: './javascripts/'
    }).use(['layer','element', 'echarts'], function() {});
    var element = layui.element,
        $ = layui.jquery,
        echarts = layui.echarts;


    function generateChart(data) {
        var myChart = echarts.init(document.getElementById('EchartZhu'));

        myChart.clear();
        var optionchart = {
            title: {
                text: '关键词出现的数目'
            },
            tooltip: {},
            legend: {
                data: ['数量/条']
            },
            xAxis: {
                data: data.map(function (obj) {
                    return obj.publish_date.substr(6,4)
                })
            },
            yAxis: {
                type: 'value'
            },
            series: [{
                name: '数量/条',
                type: 'line', //柱状
                data: data.map(function (obj) {
                    return obj.number
                }),
                itemStyle: {
                    normal: { //柱子颜色
                        color: '#009688'
                    }
                },
            }]
        };

        myChart.setOption(optionchart, true);
    }
    $(document).on('click','#submit',function (data) {
        var name=$('#username').val();
        $.ajax({
            url:"/mysql/line",
            type:"get",
            async: true,
            data:{
                name: name
            },
            dataType:"json",
            statusCode:{
                200:function (msg) {
                    console.log(msg);
                    console.log(msg.map(function (obj) {
                        return obj.publish_date.substr(6,4);
                    }));
                    generateChart(msg);
                }
            }
        });
    });

这里为了实现将搜索后的新闻按照新闻发布时间统计形成对应的柱状图,需要实现的对应查询语句为:select publish_date, count(*) as number from crawl.fetches where keywords like '%"+name+"%' group by publish_date;

对应路由代码:

router.get('/line',function(req,res,next){
    var name = req.param('name');
    var sql="select publish_date, count(*) as number from crawl.fetches where keywords like '%"+name+"%' group by publish_date;";
    console.log(sql);
    db.query(sql,function(err,data){
        if(err){
            console.log("数据库访问出错",err);
        }else{
            console.log("OK");
        }
        res.writeHead(200, {
            "Content-Type": "application/json"
        });
        console.log(JSON.stringify(data));
        res.end(JSON.stringify(data));
    });
});

在这里插入图片描述
这实际上也是第一次实验热度分析的图表化展示功能的实现。

7. 实现一个管理端界面,可以查看(查看用户的操作记录)和管理(停用启用)注册用户

查看、停用与启用

由于管理员需要停用和启用某用户的注册功能,所以需要对于用户增加一个状态属性,用于确定该账号目前是处于停用还是启用状态,如果是停用状态,那么在登录界面时会出现报错信息。并且新创建一个数据库表,用于存放每个用户对应的是停用还是启用状态,即stage表。

在管理端界面上,包含了所有用户的各类信息,有用户名、密码、注册时间、状态、请求时间、请求地址以及请求操作,管理员可以选择停用或者启用某个用户的服务。

用户被停用效果:

在这里插入图片描述

实验扩展:加入管理员的查询功能

在管理员界面,和之前的查询界面类似,为了方便管理员进行操作,在管理员界面也实现了一个查询功能,管理员可以更方便地通过用户名(没有选择id是因为id号没有在前端展示,对于管理员来说没有使用用户名查询方便)查询到某指定用户所有的操作记录,并可以停用或者再次启用该用户的服务。

前端页面:

    layui.use('table', function(){
        var table = layui.table;

        //第一个实例
        table.render({
            elem: '#demo'
            ,size: 'lg'
            ,height:  'full-270'
            ,url: 'http://localhost:3000/mysql/action' //数据接口
            ,cols: [[ //表头
                {field: 'index', type: 'numbers'}
                ,{field: 'username', title: '用户名', width:80}
                ,{field: 'password', title: '密码', width:80}
                ,{field: 'registertime', title: '注册时间', width:250}
                ,{field: 'stat', title: '状态', width:200}
                ,{field: 'request_time', title: '请求时间', width:250}
                ,{field: 'request_url', title: '请求地址', width:200}
                ,{field: 'name', title: '请求操作', width:80}
                ,{field: '管理', title: '管理', toolbar: '#barDemo'}
            ]]
            ,page: true
            ,limits: [3,5,10]  //一页选择显示3,5或10条数据
            ,limit: 10  //一页显示10条数据
            ,parseData: function(res){ //将原始数据解析成 table 组件所规定的数据,res为从url中get到的数据
                var result;
                console.log(this);
                console.log(JSON.stringify(res));
                if(this.page.curr){
                    result = res.data.slice(this.limit*(this.page.curr-1),this.limit*this.page.curr);
                }
                else{
                    result=res.data.slice(0,this.limit);
                }
                return {
                    "code": res.code, //解析接口状态
                    "msg": res.msg, //解析提示文本
                    "count": res.count, //解析数据长度
                    "data": result //解析数据列表
                };
            }
        });
        $(document).on('click','#submit',function (data) {
            var username=$('#username').val();
            table.reload('demo',{
                where:{
                    username:username,
                },
                page:{
                    curr:1
                }
            })
        });
        table.on('tool(test)',function (obj) {
            var tr=obj.data;
            console.log(tr);
            var event = obj.event;
            if(event==='off'){
                $.ajax({
                    url:"/mysql/off",
                    type:"put",
                    async: false,
                    data:{
                        "username":tr.username
                    },
                    dataType:"json",
                    // success:function (msg) {
                    //     console.log(msg);
                    //     alert("注册成功");
                    //     // window.location.href="index.html";
                    // },
                    // error:function (msg) {
                    //     console.log(msg);
                    // },
                    statusCode:{
                        200:function () {
                            layer.alert('修改成功', {icon: 1});
                        }
                    }
                })
            }else if(event==='on'){
                $.ajax({
                    url:"/mysql/on",
                    type:"put",
                    async: false,
                    data:{
                        "username":tr.username
                    },
                    dataType:"json",
                    statusCode:{
                        403:function(){
                            layer.alert('修改失败', {icon: 2});
                        },
                        200:function () {
                            layer.alert('修改成功', {icon: 1});
                        }
                    }
                })
            }
        })
    });

该部分对应的路由代码:

router.put('/off', function (req, res, next) {
    var username = req.param('username');
    var sql = "update user set state = 1 where username = '"+username+"';";
    console.log(sql);
    db.commit(sql);
    res.writeHead(200, {
        "Content-Type": "application/json"
    });
    res.end();
});

router.put('/on', function (req, res, next) {
    var username = req.param('username');
    var sql = "update user set state = 0 where username = '"+username+"';";
    console.log(sql);
    db.commit(sql);
    res.writeHead(200, {
        "Content-Type": "application/json"
    });
    res.end();
});

效果:

在这里插入图片描述

8. 扩展功能:实现对爬虫数据中文分词的查询

nodejieba库安装

nodeJieba是"结巴"中文分词的 Node.js 版本实现, 由CppJieba提供底层分词算法实现, 是兼具高性能和易用性两者的 Node.js 中文分词组件。其特点是词典载入方式灵活,无需配置词典路径也可使用,需要定制自己的词典路径时也可灵活定制;底层算法实现是C++,性能高效;支持多种分词算法;支持动态补充词库。

本次实验在一开始下载nodejieba库时卡住了很长的时间,windows下总是显示无法安装 nodejieba,安装依赖总是报错,试验了好多次,终于结合网上资料找到一个解决办法:

  1. 安装 python2.7版本
npm install --python=python2.7
npm config set python python2.7
  1. 安装 node-pre-gyp
npm install --global --production windows-build-tools
npm install -g node-gyp
  1. 安装nodejieba
npm install nodejieba
分词与查询

nodejieba库安装成功后,分词就比较容易实现了。使用node结巴的cut方法可以实现中文分词,分词语句如下:

var strings = nodejieba.cut(bt);

分词之后,分词结果存储于数组中,注意这里是取的所有分词结果下的交集而非并集,所以要通过循环来把所有的分词单词写入一个查询sql语句的条件中中,中间通过and实现连接,对应的mysql查询语句如下:

ELECT * FROM fetches where  title like '%%'  and  title like '%%'  …… and keywords like '%%' and author like '%%'  order by createtime ;
实验扩展:导入停用词表

实现了分词之后,考虑一个更实际的问题,当用户的查询语句带有停用词时,当前的查询结果可能不够全面。例如,用户查询“石家庄的房价”这一语句时,分词功能会将这一短语分为“石家庄”、“的”、“房价”三个单词做交集查询,但对于用户查询来说,“的”这个词的查询是没有任何意义的,而实际查询中标题内没有“的”的新闻就被去除了,这显然不合理。用户关心的是“石家庄”和“房价”的信息,像“的”这样的停用词应当提前被移除。

通常意义上,停用词(Stop Words)大致可分为如下两类:一是使用十分广泛,甚至是过于频繁的一些单词。比如英文的“i”、“is”、“a”、“the”,中文的“我”、“的”之类词几乎在每个文档上均会出现,查询这样的词搜索引擎就无法保证能够给出真正相关的搜索结果,难于缩小搜索范围提高搜索结果的准确性,同时还会降低搜索的效率;二是文本中出现频率很高,但实际意义又不大的词。这一类主要包括了语气助词、副词、介词、连词等,通常自身并无明确意义,只有将其放入一个完整的句子中才有一定作用的词语。如常见的“的”、“在”、“和”、“接着”之类。

这里采用了最常用的哈工大停用词表,对于这一类没有意义的词进行了移除,不会出现在查询结果中。

最终分词功能实现后查询结果如下,可以看到,用户连续输入查询语句时,系统会自动对查询语句进行分解,去除停用词后对于分词结果进行交集查询处理:

在这里插入图片描述
由上图可见,之前搜索”上海的楼市“将会没有搜索结果,现改为分词搜索之后,会先进行分词,将查询语句分为“上海”、“的”、“楼市”,然后去除停用词“的”,改为搜索标题含有“上海”和“楼市”的交集查询。

实验扩展:使用倒排索引表

使用nodejieba分词实际上是以文档的ID为关键字,表中记录文档中每个字的位置信息,查找时扫描表中每个文档中字的信息直到找出所有包含查询关键字的文档。

这种组织方法建立比较方便且易于维护;因为索引是基于文档建立的,若是有新的文档加入,直接为该文档建立一个新的索引块,挂接在原来索引文件的后面。若是有文档删除,则直接找到该文档号文档对应的索引信息,将其直接删除。但是在查询的时候需对所有的文档进行扫描以确保没有遗漏,这样就使得检索时间大大延长,检索效率低下。所以一般在实际查询时,尽管正排表的工作原理非常的简单,但是由于其检索效率太低,实用性价值不大。

考虑使用倒排索引来实现,倒排表以字或词为关键字进行索引,表中关键字所对应的记录表项记录了出现这个字或词的所有文档,一个表项就是一个字表段,它记录该文档的ID和字符在该文档中出现的位置情况。

由于每个字或词对应的文档数量在动态变化,所以倒排表的建立和维护都较为复杂,但是在查询的时候由于可以一次得到查询关键字所对应的所有文档,所以效率高于正排表。在全文检索中,检索的快速响应是一个最为关键的性能,而索引建立由于在后台进行,尽管效率相对低一些,但不会影响整个搜索引擎的效率。正排索引是从文档到关键字的映射(已知文档求关键字),倒排索引是从关键字到文档的映射(已知关键字求文档)。

这一部分只做了一个小的Demo,因为整个倒排索引表构建过程太过庞大了,这里主要描述一下其核心思想,即把可能进行查询的关键词提前存储在对应的数据库中,从而当用户需要查询这些关键词时,直接从倒排索引表中返回结果,提高查询的效率。

9. 扩展功能:实现查询结果按照主题词打分的排序

TF-IDF

TF-IDF是一种用于信息检索与文本挖掘的常用加权技术。

TF-IDF是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。其主要思想是:如果某个单词在一篇文章中出现的频率TF高,并且在其他文章中很少出现,则认为此词或者短语具有很好的类别区分能力,适合用来分类。

TF是词频,词频(TF)表示词条在文本中出现的频率。这个数字通常会被归一化(一般是词频除以文章总词数), 以防止它偏向长的文件。

IDF是逆向文件频率,逆向文件频率 (IDF) 表示某一特定词语的IDF,可以由总文件数目除以包含该词语的文件的数目,再将得到的商取对数得到。如果包含词条t的文档越少, IDF越大,则说明词条具有很好的类别区分能力。

TF-IDF实际上是TF * IDF。某一特定文件内的高词语频率,以及该词语在整个文件集合中的低文件频率,可以产生出高权重的TF-IDF。因此,TF-IDF倾向于过滤掉常见的词语,保留重要的词语。

数据库实现
1. 获取所有的数据

select id_fetches,word from words;

2. 计算TF

对应文档中单词出现频数A:select count(*) as num from words where word='${word}' and id_fetches=${id};

对应文档中单词总数B:select count(*) as num from words where id_fetches=${id};

TF值为单词频数与文档总长度之比,即为A / B

3. 计算IDF

文档总数C:select count(distinct id_fetches) as num from Splitwords;

包含查询单词的文档数D:select count(distinct id_fetches) as num from Splitwords where word='${word}';

TF值为包含该单词的文档数与总文档数之比,即为log(C / (D+1))

4. 计算IDF

由公式TF-IDF = TF * IDF,可以得出最后的TF-IDF值为 (A / B) * (log(C / (D+1)))

并将计算结果插入数据表中INSERT INTO Weight VALUES (id,word,weight)

结果展示

由于这里是对于查询标题单独做了一个关键词处理,所以单独做了一个页面来实现这一附加功能的展示。
在这里插入图片描述
查询结果可以看到,在只查看单个词“北京”的情况下,与“北京”联系最紧密的新闻标题将会被先显示出来。

项目总结

第二次实验由于做完前老师没有给任何相应代码了,所以全部内容都是自己一点点结合网上资料来实现的,相对来说难度比第一次大很多。在本学期使用nodejs实现了自己搭建一个网络,对于nodejs的语言特性有了一定的体会,也学习了搭建网络、网络前后端的实现过程,收获颇丰,最终也算顺利完成了期末项目。

最后感谢老师和助教在我学习过程中的指导!

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值