Node.js爬虫一站到底系列九进阶篇:开天辟地——丰富网页

本节实现的目标

点击直接跳转

基本要求:
  1. 用户可注册登录网站,非注册用户不可登录查看数据
  2. 用户注册、登录、查询等操作记入数据库中的日志
  3. 实现查询词支持布尔表达式
  4. 爬虫数据查询结果列表支持分页和排序(期中作业已实现)
  5. 用Echarts或者D3实现3个以上的数据分析图表展示在网站中
扩展要求:
  1. 实现对爬虫数据中文分词的查询
  2. 实现查询结果按照主题词打分的排序
  3. 添加网页样式

技术实现: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
    }
};
项目概览

在这里插入图片描述

  1. 用户可注册登录网站,非注册用户不可登录查看数据
实现登录操作

功能实现——对用户有适当提示:

  • 提示用户名或密码输错
  • 用户不存在
(1)前端页面(index.html):

首先在根元素上定义 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">&times;</a>
	<strong>警告!</strong>{{msg}}
</div>
(2)angular代码(位于同一文档):

先定义控制器“loginCtrl” :

  • 初始化视图中输入的数据,作为存储数据的容器;
  • 同时通过 $scope 对象把函数行为暴露给视图;
  • 监视模型的变化,做出相应的逻辑处理。
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;
        });

};
(3)路由代码(users.js):

将用户名传入数据库获取对应的密码

  • 如果返回的用户名长度为零,说明用户不存在;
  • 如果获取到密码,检查和用户输入的密码是否匹配:若匹配则发送 json 响应,msg 赋值为“ok”,并且把用户名保存到服务器内存中,以便记录用户登录操作日志和检查用户对页面进行操作时的登录状态;否则 msg 为错误提示。
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:'用户名或密码错误!请检查后输入'});
      }
    }
  });
});
实现注册操作

(注册操作的实现与登录操作思路相同,这里不作详述)

功能实现——对用户有适当提示:

  • 两次密码不一致
  • 用户已存在
  • 注册成功跳转登陆页面
(1)前端页面(index.html):

因为进入主页先显示登录界面,因此这里设置元素显示方式 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>
(2) angular代码(位于同一文档):

先检验两次输入密码是否一致,如果不一致,直接显示警告

$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;
        });
}
(3)路由代码(users.js):

先检查用户是否已存在,如果已存在,则无需再添加用户信息,显示警告

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: '成功注册!请登录'});
      })
    }
  });
});
  1. 用户注册、登录、查询等操作记入数据库中的日志

这里借助 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>
  1. 实现查询词支持布尔表达式

(比如“新冠 AND 肺炎”或者“新冠 OR 肺炎”)

(1)当点击“检索”按钮—>调用 show_search() 函数
angular代码(javascripts/new.js)

在示例基础上添加新表单的时候,出现了由于覆盖导致显示不出来的问题,琢磨了很久,发现在此处很巧妙地实现了“检索”和“图片”不同时出现的效果:

  • 图片:ng-hide=“isShow”
  • 表单:ng-show=“isShow”
    但如果想要再加入第二个表单,要注意两个表单要不同时出现,但是表单又要和图片绘制区域也不同时出现,否则会被覆盖,因此就无法绑定,这里进行了分离:
  • 图片:ng-hide=“isShow”
  • 表单:ng-show=“isShow2”

注意在此文件中要依据不同实现效果修改不同元素 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. 用Echarts或者D3实现3个以上的数据分析图表展示在网站中

以折线图图为例,实现页面上用表单来输入查任意词频率变化情况
(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();
        });
    }
});

结果展示:
虽然爬虫爬取的结果较少,依然可以看到疫情该词的出现频率在六月出现了第二个峰值,联系实际,在疫情得到有效控制的情形下,北京继连续五十六天无本地报告新增确诊病例,但在六月中旬出现一批有新发地活动史的新增患者,因此媒体有关疫情的报道逐渐增加,疫情动态又一次引发公众关注。如果数据量充足,也许我们可以通过词频图,预测下一波疫情高潮的到来。
在这里插入图片描述
其他图表的展示

词云图在这里插入图片描述

出现了类似“的”这样的出现频率很高但无实际意义的词语,因此计算词频的时候先判断词语长度是否大于一以及是否是停用词(在后面分词的时候作详细介绍)。
在这里插入图片描述

饼图

在这里插入图片描述

柱状图

在这里插入图片描述

  1. 实现对爬虫数据中文分词的查询
分词查询的意义

用一个直观的方式,先看结果
没有分词前
在这里插入图片描述
使用分词查询
在这里插入图片描述
可以看到在使用未分词的查询时,“北京的疫情”被当作了一整个词条,即使数据库中有内容相匹配的词条也无法被展示出来,而使用分词技术,该词条被分解为“北京”和“疫情”,“的”被作为无效词去除,因此搜索到了文档库中同时包含这两个关键词的内容,使得内容搜索的范围更广了,也准确匹配到了目标内容。

(1)分词工具下载使用

我们可以利用现有的分词工具包对用户提交的文本进行分词。
因为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(' ');
  • 弊端:
    由于英文是以词为单位的,词和词之间是靠空格隔开,因此计算机可以很简单通过空格来划分单词,但是中文是以字为单位,句子中所有的字连起来才能描述一个意思,因此会给分词带来一定难度。尤其是出现新名词的时候往往会出现错误的划分,比如在这里“新冠”原本是一个词语,却被划分为了“新”和“冠”。
(2)分词处理

(文件位于 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);
                    });
                }                    
            });
        }
    });
});

存储结果
在这里插入图片描述

(3)分词查询

已经存储好了分词结果,前端的搭建以及前后端的连接与实现布尔查询操作类似,这里主要介绍网站的后端查询:
对读入进来的关键词,同样先进行正则表达式的处理

  • 若用户提交的字符串没有超过三个中文字,直接到数据库索引词汇;
  • 否则先进行分词操作,再对每个词条进行判断,这里把在停用词表中出现的以及长度小于等于一的词条当作无效词,然后在 Splitwords 表中获取每个分词的id字段,最后取 id 字段交集查询 fetches 表中完整的新闻爬取信息。

因为 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+=';';

实现效果在开始便展示了~

  1. 实现查询结果按照主题词打分的排序
(1)打分机制

依据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 
);
(2)打分实现

先整理我们需要的数据库操作语句:
获取所有的数据以遍历select id_fetches,word from Splitwords;
计算TF:

  • 对应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};

计算IDF:

  • 文档总数(获取一次即可):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)

完整代码如下,存储在 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+=';';

搜索结果展示
在这里插入图片描述
在这里插入图片描述

  1. 添加网页样式

为了给用户更好的使用体验,当然要给网页添加样式,这里用到外部引入的方式,实现内容与样式分离。
以给登录界面添加样式为例:
在 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;
}

最终实现效果:


end

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值