Web编程期末项目:“全球体育快报”——体育新闻门户网站

1. 项目总体要求

1.1 网站后端

       核心需求:选取3~5个有代表性的新闻网站建立爬虫,针对不同网站的新闻页面进行分析,爬取出编码、标题、作者、时间、关键词、摘要、内容、来源等结构化信息,存储在数据库中。

       技术要求:采用Node.JS实现网络爬虫。

1.2 网站前端

       核心需求:建立网站提供对爬取内容的分项全文搜索,给出所查关键词的时间热度分析。

       技术要求:采用HTML+JS实现前端。 

1.3 注意事项

       本项目的实现过程主要基于Web编程的实验课课件内容,在此基础上利用理论课课件内容进行衔接与修饰。本文在涉及到直接使用实验课课件内容处可能讲述的较为简略,不再赘述,因此建议在阅读本文之前先熟悉一下所有实验课课件。

2. 网站后端实现

2.1 目标网站分析

       我选择的目标网站是懂球帝(https://dongqiudi.com)和虎扑体育(https://www.hupu.com),它们是体育新闻领域中的两个代表性网站,具有用户基数大、内容更新快的特点,适合作为爬虫爬取对象。

       目标网站分析主要分为两部分:先进行种子页面分析,解析出新闻链接;再进行新闻页面分析,解析出编码、标题、作者、时间、关键词、内容、来源等结构化数据。

       目标网站分析主要指页面元素检查,可以通过在网页点击右键选择“检查”选项或按“F12”键查看源码。

2.1.1 懂球帝

2.1.1.1 种子页面分析

       由于网站首页的可爬取新闻链接数量过少,我选择可爬取新闻链接数量更多的动态分区(https://dongqiudi.com/articles)作为种子页面。

       通过检查种子页面的元素可知,所有的新闻链接都位于a标签中,所以设计新闻链接的爬取格式如下。

var seedURL_format = "$('a')";

       此处的a标签并没有显示href属性,但在后续使用爬虫尝试对其进行爬取时,可以成功获取位于href属性中的具体的新闻链接,对于这个问题目前还未找到一个合理的解释,读者可以将自己的思考结果发在评论区。新闻链接的格式如下。

        通过查看多个新闻页面可知,新闻链接都具有“/”加上7位数字加上“.html”的形式,因此可以设计并利用如下的正则表达式对爬取的新闻链接进行筛选。

var url_reg = /\/(\d{7}).html/;
2.1.1.2 新闻页面分析

       在新闻页面中,可以通过检查元素确定编码方式并依次找到标题、作者、时间、内容、关键词对应的标签特征,例如class属性定义的元素类名,从而设计相对应的元素读取格式。

编码:

       可知新闻页面采用的是utf-8编码。

 标题:

        可知标题位于class属性为“news-title”的<h1>标签中,设计如下的标题爬取格式。

var title_format = "$('.news-title').text()";

作者、时间:

         可知作者和时间都位于class属性为“tips”的<p>标签中,其中,作者位于<p>标签中的<span>标签中,时间具有“yyyy-MM-dd HH:mm”的形式,设计如下的作者及时间爬取格式和时间筛选正则表达式。

var author_format = "$('.tips span').text()";
var date_format = "$('.tips').text()";
var regExp = /\d{4}-\d{2}-\d{2} \d{2}:\d{2}/;

内容:

         可知内容位于class属性为“con”的<div>标签中,设计如下的内容爬取格式。

var content_format = "$('.con').text()";

关键词:

         这里选取页面中的相关标签作为关键词,因为相关标签这一栏提供了与新闻最为相关的球员名和球队名,可以辅助用户快速判断是否是自己感兴趣的新闻。可知所有关键词均位于一个描述列表中,具体位于class属性为“name”的<p>标签中,设计如下的关键词爬取格式。

var keywords_format = "$('.tag-item .name').text()";

2.1.2 虎扑体育

2.1.1.1 种子页面分析

       我选择网站首页(https://www.hupu.com)作为种子页面。

       通过检查种子页面的元素可知,所有的新闻链接都位于a标签中,所以设计新闻链接的爬取格式如下。

var seedURL_format = "$('a')";

        此处的a标签显示了href属性,所以可以获取位于href属性中的具体的新闻链接。通过查看a标签的href属性可知,新闻链接都具有“/”加上8位数字加上“.html”的形式,因此可以设计并利用如下的正则表达式对爬取的新闻链接进行筛选。

var url_reg = /\/(\d{8}).html/;
2.1.1.2 新闻页面分析

        在新闻页面中,可以通过检查元素确定编码方式并依次找到标题、作者、时间、内容、关键词对应的标签特征,例如class属性定义的元素类名,从而设计相对应的元素读取格式。

编码:

       可知新闻页面采用的是utf-8编码。

 标题:

        可知标题位于class属性为“index_name__M5qqs”的<h1>标签中,设计如下的标题爬取格式。

var title_format = "$('.index_name__M5qqs').text()";

作者:

         可知作者位于class属性为“post-user_post-user-comp-info-top-name__N3D4w”的<a>标签中,设计如下的作者爬取格式。

var author_format = "$('.post-user_post-user-comp-info-top-name__N3D4w').text()";

时间:

         可知时间位于class属性为“post-user_post-user-comp-info-top-time__k9K2U”的<span>标签中,且时间具有“yyyy-MM-dd HH:mm:ss”的形式,设计如下的时间爬取格式和时间筛选正则表达式。

var date_format = "$('.post-user_post-user-comp-info-top-time__k9K2U').text()";
var regExp = /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/;

内容:

         可知内容位于class属性为“thread-content-detail”的<div>标签中,设计如下的内容爬取格式。

var content_format = "$('.thread-content-detail').text()";

关键词:

        这里选取新闻发表的分区名称作为关键词,对用户来说可以起到初步筛选作用。可知关键词位于class属性为“post-user_post-user-comp-info-bottom-link__BMF8U”的<a>标签中,设计如下的关键词爬取格式。

var keywords_format = "$('.post-user_post-user-comp-info-bottom-link__BMF8U').text()";

2.2 网络爬虫编写

2.2.1 MySQL数据库

       我下载的是MySQL Community Server 8.0.33的免安装版,同时还下载了官方可视化工具MySQL Workbench 8.0 CE。

       新建一个crawl数据库后,再创建三张表,本项目实际使用了fetches_1、fetches_2这两张表,表fetches_1用于存储懂球帝爬取内容,表fetches_2用于存储虎扑体育爬取内容。

 

2.2.2 Node.JS网络爬虫

        对于懂球帝和虎扑体育,编写爬虫的思路大致相同:每次读取种子页面,分析出种子页面中的新闻链接后,爬取所有新闻链接的内容;通过分析新闻页面,解析出编码、标题、作者、时间、关键词、内容等结构化内容;将非重复的结构化内容保存至MySQL数据库(来自两个目标网站的数据分别单独保存在一张表中)。我在六天内每12小时定时爬取,共爬取了11次,最终共爬取到了约1300条记录。

        在爬虫编写过程中,主要用到了request、cheerio、iconv-lite、node-schedule这四个模块,需要提前自行下载并引入。特别注意如果要使用自己新建的表,需要在代码相应位置修改表名。

2.2.2.1 懂球帝
var myRequest = require('request');
var myCheerio = require('cheerio');
var myIconv = require('iconv-lite');
var mysql = require('./mysql.js');
var schedule = require('node-schedule');

var source_name = "懂球帝";
var myEncoding = "utf-8";
var seedURL = "https://dongqiudi.com/articles/";

// 将要爬取数据的格式(通过查看页面源代码获得)
var seedURL_format = "$('a')";
var title_format = "$('.news-title').text()";
var content_format = "$('.con').text()";
var keywords_format = "$('.tag-item .name').text()";
var date_format = "$('.tips').text()";
var author_format = "$('.tips span').text()";
var url_reg = /\/(\d{7}).html/;
var regExp = /\d{4}-\d{2}-\d{2} \d{2}:\d{2}/;

// 防止网站屏蔽我的爬虫
var headers = {
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65 Safari/537.36'
}

// request模块异步fetch url
function request(url, callback) {
    var options = {
        url: url,
        encoding: null,
        //proxy: 'http://x.x.x.x:8080',
        headers: headers,
        timeout: 10000 //
    }
    myRequest(options, callback)
};

// 处理种子页面
function seedget() {
    request(seedURL, function(err, res, body) { // 读取种子页面
        // 用iconv转换编码,获取种子页面html
        var html = myIconv.decode(body, myEncoding);
        // 用cheerio解析种子页面html
        var $ = myCheerio.load(html, { decodeEntities: true });
        // 获取种子页面里所有的新闻页面a链接
        var seedurl_news;
        try {
            seedurl_news = eval(seedURL_format);
        } catch (e) { console.log('url列表所处的html块识别出错:' + e) };
        seedurl_news.each(function(i, e) {
            var myURL = "";
            try {
                // 获取新闻页面的url
                var href = "";
                href = $(e).attr("href");
                if (href == undefined) {
                    return;
                }
                if (href && (href.toLowerCase().indexOf("http://") >= 0 || href.toLowerCase().indexOf("https://") >= 0)) {
                    myURL = href;
                } // 以"http://"开头的url 
                else if (href.startsWith("//")) {
                    myURL = "http:" + href;
                } // 以"//"开头的url
                else {
                    var lastIndex = seedURL.lastIndexOf("/");
                    myURL = seedURL.substr(0, seedURL.lastIndexOf("/", lastIndex - 1)) + href;
                } // 其他形式的url
            } catch (e) { console.log('识别种子页面中的新闻链接出错:' + e) }
            // 检验获取的url是否符合新闻页面url的正则表达式
            if (!url_reg.test(myURL)) return;
            // 在数据库中查询url是否已经存在
            var fetch_url_Sql = 'select url from fetches_1 where url=?';
            var fetch_url_Sql_Params = [myURL];
            mysql.query(fetch_url_Sql, fetch_url_Sql_Params, function(qerr, vals, fields) {
                if (vals.length > 0) { // 若存在,无需存入数据库
                    console.log('URL duplicate!')
                } else newsGet(myURL); // 若不存在,读取新闻页面的具体信息
            });
        });
    });
};

// 处理新闻页面
function newsGet(myURL) {
    request(myURL, function(err, res, body) { // 读取新闻页面
        // 用iconv转换编码,获取新闻页面html
        var html_news = myIconv.decode(body, myEncoding);
        // 用cheerio解析新闻页面html
        var $ = myCheerio.load(html_news, { decodeEntities: true });
        myhtml = html_news;       
        console.log("转码读取成功:" + myURL);
        // 动态执行format字符串,构建json对象准备写入数据库
        var fetch = {};
        fetch.title = "";
        fetch.content = "";
        fetch.keywords = "";
        fetch.author = "";
        fetch.publish_date = "";
        fetch.url = myURL;
        fetch.source_name = source_name;
        fetch.source_encoding = myEncoding;
        fetch.crawltime = new Date();
        // 获取标题  
        if (title_format == "") {
            fetch.title = "";
        }
        else {
            fetch.title = eval(title_format);
        }
        // 获取内容
        if (content_format == "") {
            fetch.content = "";
        }
        else { // 去除作者信息
            fetch.content = eval(content_format).replace("\r\n" + fetch.author, "").trim();
        }
        // 获取关键词
        if (keywords_format == "") { // 若没有关键词,以source_name填充
            fetch.keywords = source_name;
        }
        else {
            // fetch.keywords = eval(keywords_format);
            // 上面爬取的关键词之间无空格隔开,下面的爬取方法可以将关键词以空格隔开
            const nameElements = $('.relate-tag-list .name');
            const names = nameElements.map((index, element) => {
                return $(element).text();
            }).get();
            fetch.keywords = names.join(' ');
        }
        // 获取作者
        if (!eval(author_format)) {
            fetch.author = source_name;
        }
        else {
            fetch.author = eval(author_format);
        }
        if (fetch.author.startsWith(" 懂球号作者: ")) { // 如果是懂球号作者,去除其前缀
            fetch.author = fetch.author.substring(8);
        }
        // 获取刊登日期
        if (!eval(date_format)) {
            fetch.publish_date = eval(date_format);
        }
        if(fetch.publish_date) {
            fetch.publish_date = regExp.exec(fetch.publish_date)[0];
        }
        else {
            fetch.publish_date = new Date().toLocaleDateString().split('/').join('-');
        }
        fetch.publish_date = fetch.publish_date.replace("年", "-");
        fetch.publish_date = fetch.publish_date.replace("月", "-");
        fetch.publish_date = fetch.publish_date.replace("日", "");
        fetch.publish_date = new Date(fetch.publish_date).toLocaleDateString().split('/').join('-');
        // 将新闻页面信息写入数据库
        if (fetch.title != "") {
            var fetchAddSql = 'INSERT INTO fetches_1(url,source_name,source_encoding,title,' +
                'keywords,author,publish_date,crawltime,content) VALUES(?,?,?,?,?,?,?,?,?)';
            var fetchAddSql_Params = [fetch.url, fetch.source_name, fetch.source_encoding,
            fetch.title, fetch.keywords, fetch.author, fetch.publish_date,
            fetch.crawltime.toLocaleDateString().split('/').join('-'), fetch.content];
            mysql.query(fetchAddSql, fetchAddSql_Params, function(qerr, vals, fields) {
                if (qerr) {
                    console.log(qerr);
                }
            });
        }
    });
}

// 定时爬虫
var rule = new schedule.RecurrenceRule();
var times = [9, 21]; // 每天2次自动执行(9点和21点)
var times2 = 0; // 每次整点执行
rule.hour = times;
rule.minute = times2;

schedule.scheduleJob(rule, function() {
    seedget();
});
2.2.2.2 虎扑体育
var myRequest = require('request');
var myCheerio = require('cheerio');
var myIconv = require('iconv-lite');
var mysql = require('./mysql.js');
var schedule = require('node-schedule');

var source_name = "虎扑体育";
var myEncoding = "utf-8";
var seedURL = "https://www.hupu.com/";

// 将要爬取数据的格式(通过检页面源代码获得)
var seedURL_format = "$('a')";
var title_format = "$('.index_name__M5qqs').text()";
var content_format = "$('.thread-content-detail').text()";
var keywords_format = "$('.post-user_post-user-comp-info-bottom-link__BMF8U').text()";
var date_format = "$('.post-user_post-user-comp-info-top-time__k9K2U').text()";
var author_format = "$('.post-user_post-user-comp-info-top-name__N3D4w').text()";
var url_reg = /\/(\d{8}).html/;
var regExp = /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/;

// 防止网站屏蔽我的爬虫
var headers = {
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65 Safari/537.36'
}

// request模块异步fetch url
function request(url, callback) {
    var options = {
        url: url,
        encoding: null,
        //proxy: 'http://x.x.x.x:8080',
        headers: headers,
        timeout: 10000 //
    }
    myRequest(options, callback)
};

// 处理种子页面
function seedget() {
    request(seedURL, function(err, res, body) { // 读取种子页面
        // 用iconv转换编码,获取种子页面html
        var html = myIconv.decode(body, myEncoding);
        // 用cheerio解析种子页面html
        var $ = myCheerio.load(html, { decodeEntities: true });
        // 获取种子页面里所有的新闻页面a链接
        var seedurl_news;
        try {
            seedurl_news = eval(seedURL_format);
        } catch (e) { console.log('url列表所处的html块识别出错:' + e) };
        seedurl_news.each(function(i, e) {
            var myURL = "";
            try {
                // 获取新闻页面的url
                var href = "";
                href = $(e).attr("href");
                if (href == undefined) {
                    return;
                }
                if (href && (href.toLowerCase().indexOf("http://") >= 0 || href.toLowerCase().indexOf("https://") >= 0)) {
                    myURL = href;
                } // 以"http://"开头的url 
                else if (href.startsWith("//")) {
                    myURL = "http:" + href;
                } // 以"//"开头的url
                else {
                    var lastIndex = seedURL.lastIndexOf("/");
                    myURL = seedURL.substr(0, seedURL.lastIndexOf("/", lastIndex - 1)) + href;
                } // 其他形式的url
            } catch (e) { console.log('识别种子页面中的新闻链接出错:' + e) }
            // 检验获取的url是否符合新闻页面url的正则表达式
            if (!url_reg.test(myURL)) return;
            // 在数据库中查询url是否已经存在
            var fetch_url_Sql = 'select url from fetches_2 where url=?';
            var fetch_url_Sql_Params = [myURL];
            mysql.query(fetch_url_Sql, fetch_url_Sql_Params, function(qerr, vals, fields) {
                if (vals.length > 0) { // 若存在,无需存入数据库
                    console.log('URL duplicate!')
                } else newsGet(myURL); // 若不存在,读取新闻页面的具体信息
            });
        });
    });
};

// 处理新闻页面
function newsGet(myURL) {
    request(myURL, function(err, res, body) { // 读取新闻页面
        // 用iconv转换编码,获取新闻页面html
        var html_news = myIconv.decode(body, myEncoding);
        // 用cheerio解析新闻页面html
        var $ = myCheerio.load(html_news, { decodeEntities: true });
        myhtml = html_news;       
        console.log("转码读取成功:" + myURL);
        // 动态执行format字符串,构建json对象准备写入数据库
        var fetch = {};
        fetch.title = "";
        fetch.content = "";
        fetch.keywords = "";
        fetch.author = "";
        fetch.publish_date = "";
        fetch.url = myURL;
        fetch.source_name = source_name;
        fetch.source_encoding = myEncoding;
        fetch.crawltime = new Date();
        // 获取标题  
        if (title_format == "") {
            fetch.title = "";
        }
        else {
            fetch.title = eval(title_format);
        }
        // 获取内容
        if (content_format == "") {
            fetch.content = "";
        }
        else { // 去除作者信息
            fetch.content = eval(content_format).replace("\r\n" + fetch.author, "").trim();
        }
        // 获取关键词
        if (keywords_format == "") { // 若没有关键词,以source_name填充
            fetch.keywords = source_name;
        }
        else {
            fetch.keywords = eval(keywords_format);
            fetch.keywords = fetch.keywords.trim().split(/\s+/)[0]; //保留分区名称 
        }
        // 获取作者
        if (!eval(author_format)) {
            fetch.author = source_name;
        }
        else {
            fetch.author = eval(author_format);
        }
        // 获取刊登日期
        if (!eval(date_format)) {
            fetch.publish_date = eval(date_format);
        }
        if(fetch.publish_date) {
            fetch.publish_date = regExp.exec(fetch.publish_date)[0];
        }
        else {
            fetch.publish_date = new Date().toLocaleDateString().split('/').join('-');
        }
        fetch.publish_date = fetch.publish_date.replace("年", "-");
        fetch.publish_date = fetch.publish_date.replace("月", "-");
        fetch.publish_date = fetch.publish_date.replace("日", "");
        fetch.publish_date = new Date(fetch.publish_date).toLocaleDateString().split('/').join('-');
        // 将新闻页面信息写入数据库
        if (fetch.title != "") {
            var fetchAddSql = 'INSERT INTO fetches_2(url,source_name,source_encoding,title,' +
                'keywords,author,publish_date,crawltime,content) VALUES(?,?,?,?,?,?,?,?,?)';
            var fetchAddSql_Params = [fetch.url, fetch.source_name, fetch.source_encoding,
            fetch.title, fetch.keywords, fetch.author, fetch.publish_date,
            fetch.crawltime.toLocaleDateString().split('/').join('-'), fetch.content];
            mysql.query(fetchAddSql, fetchAddSql_Params, function(qerr, vals, fields) {
                if (qerr) {
                    console.log(qerr);
                }
            });
        }
    });
}

// 定时爬虫
var rule = new schedule.RecurrenceRule();
var times = [9, 21]; // 每天2次自动执行(9点和21点)
var times2 = 0; // 每次整点执行
rule.hour = times;
rule.minute = times2;

schedule.scheduleJob(rule, function() {
    seedget();
});

3. 网站前端实现

3.1 网站页面设计

       网站页面由网站标题、搜索框、导航栏、搜索结果四部分组成。

3.1.1 网站标题

       我利用PPT将网站标题制作成艺术字并转为图片形式放入网页。

<div class="title-container">
    <img id="title" src="images/title.png" alt="Title">
</div>

       css文件中设置网站标题的位置和大小。

.title-container {
  display: flex;
  justify-content: center;
  margin-top: 30px;
}

#title {
  width: 500px;
}

3.1.2 搜索框

       我实现了对多个查询条件的复合查询功能。靠左的三个块为输入框:前两个输入框为文本输入框,都用于输入标题关键字,搜索时会根据标题关键字筛选标题符合的记录;第三个输入框为提供选项菜单的表单控件,用于输入时间范围,搜索时会根据时间范围筛选时间符合的记录。

<div class="search-box">
    <form>
        <input type="text" id="input-box1" name="title_text1" autocomplete="on" placeholder="标题关键词1">
        <input type="text" id="input-box2" name="title_text2" autocomplete="on" placeholder="标题关键词2">
        <select id="select-date" name="date-range">
            <option value="">全部</option>
            <option value="2023-07-18">07-18</option>
            <option value="2023-07-17">07-17</option>
            <option value="2023-07-16">07-16</option>
            <option value="2023-07-15">07-15</option>
            <option value="2023-07-14">07-14</option>
            <option value="2023-07-13">07-13</option>
        </select>
    </form>
</div>

       css文件中设置输入框的位置、大小和背景。

#input-box1 {
  /* 位置 */
  position: absolute;
  left: 32%;
  top: 30%;
  margin: 0 auto;
  overflow: hidden;
  /* 大小 */
  width: 140px;
  height: 45px;
  border: none;
  /* 背景 */
  background-color: white;
  box-shadow: none;
  color: black;
  font-size: 16px;
  line-height: 34px;
}

#input-box2 {
  /* 位置 */
  position: absolute;
  left: 42%;
  top: 30%;
  margin: 0 auto;
  overflow: hidden;
  /* 大小 */
  width: 140px;
  height: 45px;
  border: none;
  /* 背景 */
  background-color: white;
  box-shadow: none;
  color: black;
  font-size: 16px;
  line-height: 34px;
}

#select-date {
  /* 位置 */
  position: absolute;
  left: 52%;
  top: 30%;
  margin: 0 auto;
  overflow: hidden;
  /* 大小 */
  width: 135px;
  height: 47px;
  border: none;
  /* 背景 */
  background-color: white;
  box-shadow: none;
  color: black;
  font-size: 16px;
  line-height: 34px;
}

       靠右的两个块为功能按钮:第一个是“查询”按钮,点击后会根据输入框中的内容在数据库中查询,默认是从懂球帝对应的表中查询;第二个是“重置”按钮,点击后会将前两个输入框中的内容全部清空,将第三个输入框中的内容重置为默认。

<div>
    <input class="form-submit" type="button" value="查询">
    <button class="reset" type="button" onclick="reset()">重置</button>
</div>

<script>
    $(document).ready(function() {
        $(".form-submit").click(function() {
            searchDqd();
        });
    });
    function reset() {
        document.getElementById("input-box1").value = "";
        document.getElementById("input-box2").value = "";
        document.getElementById("select-date").value = "";
    }
</script>

       css文件中设置功能按钮的位置、大小和边框,以及用户将光标悬停在按钮上时触发的特殊样式。

.form-submit {
  /* 位置 */
  position: absolute;
  top: 30%;
  left: 62%;
  margin: 0;
  padding: 0;
  /* 大小 */
  width: 55px;
  height: 45px;
  border: none;
  font-size: 16px;
  /* 边框 */
  border-radius: 2px;
  background: lightblue;
  line-height: 26px;
  cursor: pointer;
}

.form-submit:hover {
  background: rgb(0, 160, 255);
}

.reset {
  /* 位置 */
  position: absolute;
  top: 30%;
  left: 66%;
  margin: 0;
  padding: 0;
  /* 大小 */
  width: 55px;
  height: 45px;
  border: none;
  font-size: 16px;
  /* 边框 */
  border-radius: 2px;
  background: lightgray;
  line-height: 26px;
  cursor: pointer;
}

.reset:hover {
  background: gray;
}

3.1.3 导航栏

       导航栏包含三大功能模块:查看来源为懂球帝的相关新闻,查看来源为虎扑体育的相关新闻,以及查看搜索关键字的热度分析图。点击一个功能模块,会触发相应功能,并将结果展示在导航栏右侧区域内。

<div class="nav">
    <ul id="nav-bar">
        <li onclick="searchDqd()">懂球帝<br><img id="dqd-logo" src="images/dqd-logo.jpg" alt="懂球帝"></li>
        <li onclick="searchHupu()">虎扑体育<br><img id="hupu-logo" src="images/hupu-logo.png" alt="虎扑体育"></li>
        <li onclick="heatAnalyze()">热度分析<br><img id="heat-analyze" src="images/heat-analyze.jpg" alt="热度分析"></li>
    </ul>
</div>

       如果选择查看来源为懂球帝的相关新闻,首先根据标题关键词和时间范围,从表fetches_1请求得到符合要求的记录的url、title、keywords、author、publish_date字段。对于每条记录,我需要处理这五个字段,得到最终显示在搜索结果中的标题、关键词、作者和刊登日期四部分。对于标题部分,将title字段放入一个a标签中,并将url字段设为a标签的href属性,这样就可以点击标题直接跳转到对应的新闻页面。对于关键词部分,因为keywords字段可能包含很过关键词不利于网页展示效果,所以只取前五个关键词。作者部分直接显示author字段的值。对于刊登日期部分,如果直接显示publish_date字段的值,会发现末尾有如T16:00:00.000Z的格式,即为UTC时间,这样的时间格式不符合我们的读取习惯,而且过长会影响网页展示效果,所以需要先转换日期格式。

<script>
    function searchDqd() {
        $(".nav").empty().append('<ul id="nav-bar">' + 
            '<li class="active" onclick="searchDqd()">懂球帝<br><img id="dqd-logo" src="images/dqd-logo.jpg" alt="懂球帝"></li>' + 
            '<li onclick="searchHupu()">虎扑体育<br><img id="hupu-logo" src="images/hupu-logo.png" alt="虎扑体育"></li>' + 
            '<li onclick="heatAnalyze()">热度分析<br><img id="heat-analyze" src="images/heat-analyze.jpg" alt="热度分析"></li>' + 
            '</ul>');
        $.get('/process_get?tablename=fetches_1&type=normal&title1=' + $("#input-box1:text").val() + '&title2=' + $("#input-box2:text").val() + '&date=' + $("#select-date").val(), function(data) {
            $("#record").empty();
            $("#record").append('<thead><tr><th>标题</th>' + 
                '<th>关键词</th><th>作者</th><th onclick="sortTable()">刊登日期</th></tr></thead>');
            for (let list of data) {
                let table = '<tbody><tr><td>';
                let index = 0;
                let url = "";
                Object.values(list).forEach(element => {
                    if (index == 0) {
                        url = element;
                    }
                    else {
                        if (index == 1) { // 将网页url加在标题上
                            table += ('<a href="' + list.url + '" target="_blank">' + element + '</a>' + '</td><td>');
                        }
                        else if (index == 2) { // 只显示前五个关键词
                            keywords = element.split(" ");
                            if (keywords.length > 5) {
                                tmp = "";
                                for (let i = 0; i < 5; i++) {
                                    tmp += keywords[i] + " ";
                                }
                                element = tmp.trim();
                            }
                            table += (element + '</td><td>');
                        }
                        else if (index == 4) { // 转换日期格式
                            let date = new Date(element);
                            let year = date.getFullYear();
                            let month = (date.getMonth() + 1).toString().padStart(2, '0');
                            let day = date.getDate().toString().padStart(2, '0');
                            element = year + '-' + month + '-' + day;
                            table += (element + '</td>');
                        }
                        else {
                            table += (element + '</td><td>');
                        }
                    }
                    index++;
                });
                $("#record").append(table + '</tr></tbody>');
            } 
        });
    }
</script>

       如果选择查看来源为虎扑体育的相关新闻,操作流程基本相同,不同的是从表fetches_2中请求记录。此外,由于此时关键词只有一个,所以无需对关键词进行筛选。

<script>
function searchHupu() {
        $(".nav").empty().append('<ul id="nav-bar">' + 
            '<li onclick="searchDqd()">懂球帝<br><img id="dqd-logo" src="images/dqd-logo.jpg" alt="懂球帝"></li>' + 
            '<li class="active" onclick="searchHupu()">虎扑体育<br><img id="hupu-logo" src="images/hupu-logo.png" alt="虎扑体育"></li>' + 
            '<li onclick="heatAnalyze()">热度分析<br><img id="heat-analyze" src="images/heat-analyze.jpg" alt="热度分析"></li>' + 
            '</ul>');
        $.get('/process_get?tablename=fetches_2&type=normal&title1=' + $("#input-box1:text").val() + '&title2=' + $("#input-box2:text").val() + '&date=' + $("#select-date").val(), function(data) {
            $("#record").empty();
            $("#record").append('<thead><tr><th>标题</th>' + 
                '<th>关键词</th><th>作者</th><th onclick="sortTable()">刊登日期</th></tr></thead>');
            for (let list of data) {
                let table = '<tbody><tr><td>';
                let index = 0;
                let url = "";
                Object.values(list).forEach(element => {
                    if (index == 0) {
                        url = element;
                    }
                    else {
                        if (index == 1) { // 将网页url加在标题上
                            table += ('<a href="' + list.url + '" target="_blank">' + element + '</a>' + '</td><td>');
                        }
                        else if (index == 4) { // 转换日期格式
                            let date = new Date(element);
                            let year = date.getFullYear();
                            let month = (date.getMonth() + 1).toString().padStart(2, '0');
                            let day = date.getDate().toString().padStart(2, '0');
                            element = year + '-' + month + '-' + day;
                            table += (element + '</td>');
                        }
                        else {
                            table += (element + '</td><td>');
                        }
                    }
                    index++;
                });
                $("#record").append(table + '</tr></tbody>');
            } 
        });
    }
</script>

       我实现了对搜索结果按刊登日期字段进行排序,没有选择其他字段是因为按其他字段排序的意义都不是很大。第一次点击时将升序排列,再次点击时将将降序排列。

<script>
    function sortTable() {
        var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
        table = document.getElementById("record");
        switching = true;
        // 设置升序排列
        dir = "asc";
        while (switching) {
            // 设置循环结束标记
            switching = false;
            rows = table.rows;
            for (i = 1; i < (rows.length - 1); i++) {
                // 设置元素是否调换位置
                shouldSwitch = false;
                x = rows[i].getElementsByTagName("TD")[3];
                y = rows[i + 1].getElementsByTagName("TD")[3];
                if (dir == "asc") {
                    if (x.innerHTML.toLowerCase() > y.innerHTML.toLowerCase()) {
                        shouldSwitch = true;
                        break;
                    }
                } else if (dir == "desc") {
                    if (x.innerHTML.toLowerCase() < y.innerHTML.toLowerCase()) {
                        shouldSwitch = true;
                        break;
                    }
                }
            }
            if (shouldSwitch) {
                // 如果元素调换位置设置为true,则进行对调操作
                rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
                switching = true;
                // 每次对调完成时,将switchcount增加1
                switchcount ++;
            } else { // 如果完成所有元素的排序且 direction 设置为 "asc",这时就将 direction 设置为 "desc" 并再次执行循环
                if (switchcount == 0 && dir == "asc") {
                    dir = "desc";
                    switching = true;
                }
            }
        }
    }
</script>

       如果选择查看搜索关键字的热度分析图,首先根据标题关键词,从表fetches_1和fetches_2请求得到符合要求的记录的createtime字段。对这些记录按照createtime字段分类统计数量。再利用Echarts绘图,我选择绘制的是一张折线图与饼图的组合图,折线代表总新闻数量随时间的变化情况,饼图表示每一时间点上,来源为懂球帝和虎扑体育的新闻各自所占的比例。通过查看这样的组合图,我们能够详细具体地观察所查关键字的热度变化情况。

<header>
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts@4.7.0/dist/echarts.min.js"></script>
</header>
<script type="text/javascript">
    function heatAnalyze() {
        $(".nav").empty().append('<ul id="nav-bar">' + 
            '<li onclick="searchDqd()">懂球帝<br><img id="dqd-logo" src="images/dqd-logo.jpg" alt="懂球帝"></li>' + 
            '<li onclick="searchHupu()">虎扑体育<br><img id="hupu-logo" src="images/hupu-logo.png" alt="虎扑体育"></li>' + 
            '<li class="active" onclick="heatAnalyze()">热度分析<br><img id="heat-analyze" src="images/heat-analyze.jpg" alt="热度分析"></li>' + 
            '</ul>');
        $(".block").empty().append('<div id="heat-graph" style="margin-left:0px;margin-top:0px;height:100%;"></div>');

        var myChart = echarts.init(document.getElementById("heat-graph"));
        option = null;
        function fetchData(cb) {
            /* 对懂球帝新闻进行统计 */
            var data_list1 = new Array(10).fill(0);
            $.get('/process_get?tablename=fetches_1&type=heat&title1=' + $("#input-box1:text").val() + '&title2=' + $("#input-box2:text").val(), function(data1) {
                for (let list of data1) {
                    Object.values(list).forEach(element => {
                        // 转换日期时间格式
                        var date = new Date(element);
                        var year = date.getFullYear();
                        var month = (date.getMonth() + 1).toString().padStart(2, '0');
                        var day = date.getDate().toString().padStart(2, '0');
                        var hours = date.getHours().toString().padStart(2, '0');
                        var minutes = date.getMinutes().toString().padStart(2, '0');
                        var seconds = date.getSeconds().toString().padStart(2, '0');
                        element = year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds;

                        if (element.startsWith("2023-07-13 21")) {
                            data_list1[0] += 1;
                        }
                        else if (element.startsWith("2023-07-14 09")) {
                            data_list1[1] += 1;
                        }
                        else if (element.startsWith("2023-07-14 21")) {
                            data_list1[2] += 1;
                        }
                        else if (element.startsWith("2023-07-15 09")) {
                            data_list1[3] += 1;
                        }
                        else if (element.startsWith("2023-07-15 21")) {
                            data_list1[4] += 1;
                        }
                        else if (element.startsWith("2023-07-16 09")) {
                            data_list1[5] += 1;
                        }
                        else if (element.startsWith("2023-07-16 21")) {
                            data_list1[6] += 1;
                        }
                        else if (element.startsWith("2023-07-17 09")) {
                            data_list1[7] += 1;
                        }
                        else if (element.startsWith("2023-07-17 21")) {
                            data_list1[8] += 1;
                        }
                        else if (element.startsWith("2023-07-18 09")) {
                            data_list1[9] += 1;
                        }
                    });
                }
            });

            /* 对虎扑体育新闻进行统计 */
            var data_list2 = new Array(10).fill(0);
            $.get('/process_get?tablename=fetches_2&type=heat&title1=' + $("#input-box1:text").val() + '&title2=' + $("#input-box2:text").val(), function(data2) {
                for (let list of data2) {
                    Object.values(list).forEach(element => {
                        // 转换日期时间格式
                        var date = new Date(element);
                        var year = date.getFullYear();
                        var month = (date.getMonth() + 1).toString().padStart(2, '0');
                        var day = date.getDate().toString().padStart(2, '0');
                        var hours = date.getHours().toString().padStart(2, '0');
                        var minutes = date.getMinutes().toString().padStart(2, '0');
                        var seconds = date.getSeconds().toString().padStart(2, '0');
                        element = year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds;

                        if (element.startsWith("2023-07-13 21")) {
                            data_list2[0] += 1;
                        }
                        else if (element.startsWith("2023-07-14 09")) {
                            data_list2[1] += 1;
                        }
                        else if (element.startsWith("2023-07-14 21")) {
                            data_list2[2] += 1;
                        }
                        else if (element.startsWith("2023-07-15 09")) {
                            data_list2[3] += 1;
                        }
                        else if (element.startsWith("2023-07-15 21")) {
                            data_list2[4] += 1;
                        }
                        else if (element.startsWith("2023-07-16 09")) {
                            data_list2[5] += 1;
                        }
                        else if (element.startsWith("2023-07-16 21")) {
                            data_list2[6] += 1;
                        }
                        else if (element.startsWith("2023-07-17 09")) {
                            data_list2[7] += 1;
                        }
                        else if (element.startsWith("2023-07-17 21")) {
                            data_list2[8] += 1;
                        }
                        else if (element.startsWith("2023-07-18 09")) {
                            data_list2[9] += 1;
                        }
                    });
                }
            });

            // 通过 setTimeout 模拟异步加载
            setTimeout(function () {
                cb({
                    categories: ["07-13\n21:00", "07-14\n09:00", "07-14\n21:00", "07-15\n09:00", "07-15\n21:00",
                    "07-16\n09:00", "07-16\n21:00", "07-17\n09:00", "07-17\n21:00", "07-18\n09:00"],
                    data: data_list1.map(function(v, i) {
                        return v + data_list2[i];
                    }),
                    list1 : data_list1,
                    list2 : data_list2
                });
            }, 2000);
        }

        var option = {
            title: {
                text: '词条热度分析图',
                textStyle: {
                    fontWeight:'bold',
                    fontSize: 24,
                },
                x: 'center',
                top: '10'
            },
            tooltip: {},
            legend: [{
                data:['新闻数量'],
                textStyle: {
                    fontWeight:'bold',
                    fontSize: 18,
                },
                itemHeight: 18,
                top: '60',
                right: '40'
            },
            {
                data:['懂球帝', '虎扑体育'],
                textStyle: {
                    fontWeight:'bold',
                    fontSize: 18,
                },
                itemHeight: 18,
                top: '90',
                right: '40',
                orient: 'vertical'
            }],
            xAxis: {
                data: []
            },
            yAxis: {},
            series: []
        };

        myChart.showLoading();

        fetchData(function (data) {
            myChart.hideLoading();
            myChart.setOption({
                xAxis: {
                    data: data.categories
                },
                series: getSeriesData(data.categories, data.data, data.list1, data.list2)
            });
        });;
        if (option && typeof option === "object") {
            myChart.setOption(option, true);
        }

        function getSeriesData(categories, lineData, data_list1, data_list2) {
            var seriesData = [];

            seriesData.push(
                {
                    name: '新闻数量',
                    type: 'line',
                    data: lineData,
                    tooltip: {
                        formatter: '时间 : {b}<br/>{a} : {c} 条',
                    }
                }
            );

            for (var i = 0; i < categories.length; i++) {
                seriesData.push({
                    name: categories[i],
                    type: 'pie',
                    radius: '15%',
                    center: [(i * 8 + 14) + '%', '78%'],
                    label: {
                        normal: {
                            formatter: '{c}',
                            position: 'inside'
                        }
                    },
                    data: [
                        { name: '懂球帝', value: data_list1[i] },
                        { name: '虎扑体育', value: data_list2[i] },
                    ]
                });
            }

            return seriesData;
        }
    }
</script>

       css文件中设置导航栏各功能模块的位置、大小和图标,当前被选中的功能模块会显示绿色背景。

.nav {
  position: relative;
  margin-top: 0px;
  margin-left: 0px;
  height: 100%;
}

.nav ul {
  list-style-type: none;
  margin: 0;
  padding: 0;
  width: 120px;
  background-color: rgba(241,241,241,0.8);
  position: fixed;
  height: 100%;
  overflow: auto;
}

.nav li {
  display: block;
  color: #000;
  font-size: 18px;
  padding: 8px 16px;
  text-decoration: none;
  text-align: center;
}

.nav li.active {
  background-color: rgba(76,175,80,0.8);
  color: white;
}

.nav li:hover:not(.active) {
  background-color: rgba(85,85,85,0.8);
  color: white;
}

.nav li img {
  width: 80px;
  padding: 8px 0px;
  border-radius: 30px;
  display: flex;
  justify-content: center;
}

3.1.4 搜索结果

       我固定了搜索结果的展示区域,当选择导航栏的前两个功能模块时,展示的是以表格形式呈现的新闻条目,当选择导航栏的第三个功能模块时,会先将表格去除,然后在相同位置展示利用Echarts绘制的热度分析图。

<div class="block" style="margin-left:120px;height:432px;overflow:auto;">
    <table id="record"></table>
</div>

       css文件中设置以表格形式呈现的搜索结果的位置、大小和背景,以及字号和颜色。已访问的新闻链接会由蓝色变为紫色,方便标注用户的浏览历史记录。

a:link {
  color: blue; /* 未访问的链接颜色 */
}

a:visited {
  color: blueviolet; /* 已访问的链接颜色 */
}

a:hover {
  color: darkblue; /* 鼠标移动到链接的颜色 */
  text-decoration: underline;
}

.block {
  background-color: rgba(255, 255, 255, 0.8);
}

#record {
  position: relative;
  margin-top: 0px;
  margin-left: 0px;
  width: 100%;
  height: 100%;
  border: 1px solid #000000;
  border-collapse: collapse;
}

#record tr th {
  position: sticky;
  top: 0px;
  background-color: rgb(81, 130, 187);
  color: #ffffff;
  border-bottom-width: 0;
  height: 28px;
  font-size: 14px;
}

#record td {
  color: #000000;
  font-size: 13px;
}

#record th, #record td {
  padding: 5px 10px;
  font-weight: bold;
  border: 1px solid #000000;
}

3.2 网站框架构建

       我主要利用express构建网站框架,具体的代码文件组织结构如下。3.1节主要介绍了search.html和style.css文件。

        其中./routes文件夹下包含两个路由文件:index.js和user.js,index.js文件需要根据3.1.3节中提到的数据请求语句进行修改。首先根据传入的type参数,判断是来自热度分析的请求还是来自正常关键字搜索的请求,编写不同的sql语句。对于来自热度分析的请求,利用like操作符根据一个(或多个)标题关键字进行模糊查询,只需要选择符合要求的记录的createtime字段。对于来自正常关键字搜索的请求,利用like操作符根据一个(或多个)标题关键字和时间范围进行模糊查询,需要选择符合要求的记录的url、title、keywords、author、publish_date字段。

var express = require('express');
var router = express.Router();
var mysql = require('../mysql.js');

/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});

router.get('/process_get', function(request, response) {
    // sql字符串和参数
    if (request.query.type === "heat") { // 热度分析的请求格式
        var fetchSql = "select createtime " + 
        "from " + request.query.tablename + " where title like '%" + request.query.title1 + "%' and title like '%"  + request.query.title2 + "%'";
    }
    else { // 正常关键字搜索的请求格式
        var fetchSql = "select url,title,keywords,author,publish_date " + 
        "from " + request.query.tablename + " where title like '%" + request.query.title1 + "%' and title like '%"  + request.query.title2 + "%' and publish_date like '%"  + request.query.date + "%'";
    }
    mysql.query(fetchSql, function(err, result, fields) {
        response.writeHead(200, {
            "Content-Type": "application/json"
        });
        response.write(JSON.stringify(result));
        response.end();
    });
});

module.exports = router;

3.3 网站效果展示

Web编程期末项目——网站演示

4. 不足与展望

4.1 增加爬虫目标网站数量

       除了懂球帝和虎扑体育,我一开始还选择了腾讯体育(https://sports.qq.com)和央视体育(https://sports.cctv.com)作为目标网站,但从腾讯体育爬取的新闻相关数据均为乱码,切换编码方式也无法解决问题,而从央视体育一次性只能爬取较少新闻,所以我放弃了爬取这两个网站。未来可以对这两个网站继续进行研究,设计有效的爬虫。

4.2 对搜索结果进行分页显示

       由于目前爬取的新闻数量不多,所以我没有实现分页显示功能。当数据库中的记录数量很大时,符合搜索要求的记录数量也会较多,这时分页显示就会很有必要。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值