新闻爬虫及爬取结果的查询网站(基于Node.js)

新闻爬虫及爬取结果的查询网站(使用Node.js)

!!!----------
网站博文限于审核,删减内容过多,实验报告部分还请老师、助教查看作业压缩包中pdf报告文件
!!!----------

!!!----------
具体代码也在作业压缩包中:
myapp文件夹:内含所实现的网站的源码
爬虫源码文件夹:爬虫源码
!!!----------


总任务

一、爬虫

爬取新闻网站数据
这里完成了搜狐、网易与澎湃

二、搜索网站

  1. 搜索
  2. 时间热度分析
  3. 时间热度与来源分析

新闻网页爬虫部分

这一部分的图片请见pdf文件

概述

工具包
使用node.js中的三个工具包

  1. request 用于请求网页获取所需数据
  2. chreeio 用于解析得到的网页
  3. iconv-lite 用于转换编码

代码的主要逻辑

  1. 请求新闻种子页面,该页面是新闻网站的主页,布满了供爬取的锚点链接,根据这个页面获取到具体新闻网页的链接。
  2. 遍历第一步获取到的全部新闻网站链接,请求具体新闻网页,解析页面获取需要的信息并存入数据库中。

需要详细了解jQuery的一些语法来解析网页,可以参考这里

搜狐新闻

分析种子页面

右键查看源代码,寻找种子url所在位置

观察整个网页,不难发现一个特点:种子url所含的a标签都有data-param这一属性,属性的具体值皆不同,可以利用这一特点筛选标签获取种子url。根据jQuery属性选择器的语法,可以使用$(’[data-param]’)来获取,然后对于每一个这样的a标签获取href属性的值。
于是…先试着将种子url放入数组中

request(seedURL, function(err, res, body) { 
    var html = myIconv.decode(body, myEncoding);
    var $ = myCheerio.load(html, { decodeEntities: true });
    var seedurl_news;
    url = new Array();
    var seedurl_news;
    try {
        seedurl_news = eval($('[data-param]'));
    } catch (e) { console.log('url列表所处的html块识别出错:' + e) };

    seedurl_news.each(function(i, e){
        url.push($(e).attr('href'));
    });
    console.log(url);
});

然而…结果
在这里插入图片描述
看来还是要清洗一下
如下,改动一点,经排查发现我们需要的url必须要以’http://www.sohu.com/a’开头
这样得到的url就是正确的了

  seedurl_news.each(function(i, e){

      var myURL = ""
        try {
        var href = ""
        href = $(e).attr('href');
        if (href == undefined) return;
    } catch (e) { console.log('error: ' + e) }
        if (href.startsWith('http://www.sohu.com/a')){
            myURL = href;
            // url.push(myURL);
    }else if (href.startsWith('//www.sohu.com/a')){ 
            myURL = 'http:' + href;
            // url.push(myURL);
    }else return;
    }
分析新闻页面

首先明确我们要获取到这样几个字段:
标题、关键词、摘要、时间、作者(或者是编辑)
来源、来源链接、原始来源、原始来源链接
内容
编码都是utf-8就不再新建这个字段了
进入一个具体的网页
我们要找的信息有很多已经显示在前几行里面了
至于内容可以右击使用“检查”来寻找

于是…

var title_format = "$('title').text()";
var keywords_format = " $('meta[name=\"keywords\"]').eq(0).attr(\"content\")";
var desc_format = " $('meta[name=\"description\"]').eq(0).attr(\"content\")";
var date_format = "$('meta[itemprop=\"datePublished\"]').eq(0).attr(\"content\")"
var author_format = "$('p[data-role=\"editor-name\"]').text()"
var original_format = "$('span[data-role=\"original-link\"]').children('a').text()"
var original_url_format = "$('span[data-role=\"original-link\"]').children('a').attr('href')"
var content_format = "$('.article').text()";

排错:

  1. 有可能某个字段爬到空值或者undefined还需要再判断一下,比如
 if ((desc_format == "")||(eval(desc_format) === undefined)) item.desc = item.title; //没有摘要就用标题
        else item.desc = eval(desc_format).replace("\r\n", ""); //摘要 

        if ((date_format == "")||(regExp.exec(eval(date_format))==null)) item.publish_date = (new Date()).toFormat("YYYY-MM-DD") //没有日期 
        else{
        item.publish_date = regExp.exec(eval(date_format))[0];
        item.publish_date = item.publish_date.replace('年', '-')
        item.publish_date = item.publish_date.replace('月', '-')
        item.publish_date = item.publish_date.replace('日', '')
        item.publish_date = new Date(item.publish_date).toFormat("YYYY-MM-DD");
        }
  1. 这里的body如果是undefine就会报错。
var html_news = myIconv.decode(body, myEncoding); //用iconv转换编码

在这里插入图片描述
所以上面要加一行判断

if(body === undefined) return;
  1. 还有一个点确实是有些奇怪,我们已经对是否重复爬取链接做了判断,但依然会抛出违反唯一键的异常,而且据我所知这个是否抛出异常还因电脑不同而异(运行老师给的样例代码就是有的正常,有的抛出异常),虽然不影响程序正常运行,但是有报错还是觉得别扭,后来发现可以使用insert ignore into…来运行(参考这里),如果数据库中没有这条数据就插入,如果有就跳过,这样就不会有报错了,而且相关字段也满足唯一键约束,数据库的模式依然正常。

网易新闻

分析种子页面

网易主页的新闻链接排布很整齐,$(’[ne-if]’).find(‘a’).attr(‘href’)来获取

分析新闻页面

与爬取搜狐新闻时同理

网易新闻页是真的规范,注释已经写得很清楚哪里是正文了
但有一些字段也确实是找不到了,于是

var title_format = "$('.post_title').text()";
var keywords_format = " $('meta[name=\"keywords\"]').eq(0).attr(\"content\")";
var desc_format = " $('meta[name=\"description\"]').eq(0).attr(\"content\")";
var date_format = "$('meta[property=\"article:published_time\"]').eq(0).attr(\"content\")"
var original_format = "$('div[class=\"post_wemedia_name\"]').children('a').text()"
var content_format = "$('div[class=\"post_body\"]').text()";

网易新闻爬虫结束

澎湃新闻

分析种子页面

稍稍有点乱
采用获取全部a标签再判断其href是否以"newsDetail"开头来获取全部新闻链接,当然还要加上’https://www.thepaper.cn/'才能得到一个正确的链接。

分析新闻页面

分析基本同理可得
于是

var title_format = "$('.news_title').text()";
var keywords_format = " $('meta[name=\"Keywords\"]').eq(0).attr(\"content\")";
var desc_format = " $('meta[name=\"Description\"]').eq(0).attr(\"content\")";
var date_format = "$('div.news_about').children('p').text()"
var content_format = "$('.news_txt').text()";

澎湃新闻爬虫结束

网站搭建:基于Express框架与MySQL数据库

数据库设计

新建news表

物理模型
在这里插入图片描述
建表sql语句

CREATE TABLE `news`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `keywords` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `summary` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `date` date NULL DEFAULT NULL,
  `author` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `source` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `source_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `original` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `original_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `s_url`(`source_url`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2813 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

主要的设计就是将id设为自增的主键,source_url设为唯一键,可以为null的字段默认为null。

排错

  1. node连接数据库时遇到过一个报错,ER_NOT_SUPPORTED_AUTH_MODE,但我密码明明是正确的…参考这里这里进行调整,最后终于成功连上了。

  2. 爬虫的时候还遇到一个问题,放到这里说。所爬取文字里有一些类似emoji表情的东西,数据库不认,无法插入数据库中,本来想着用正则表达式将这些奇怪的字符replace掉,但也没找到一个合适的表达式,有一种表达式把所有中文都匹配没了…于是参考这里改了数据库的配置,这样就不报错了。
    贴上之前的报错信息如下:
    在这里插入图片描述

Express框架:后端

Express 是一个简洁而灵活的 node.js Web应用框架, 提供了一系列强大特性帮助你创建各种 Web 应用,和丰富的 HTTP 工具。
使用 Express 可以快速地搭建一个完整功能的网站。Express 框架核心特性:1. 可以设置中间件来响应 HTTP 请求。2. 定义了路由表用于执行不同的 HTTP 请求动作。3. 可以通过向模板传递参数来动态渲染 HTML 页面。

这是来自菜鸟教程的一段简介,但就只讲了一页…
于是我又学习了一些资料
Express中文网
b站上某资源 挑着看了一点,讲的还挺好

搭建脚手架
npm install express-generator -g
express -e myApp
cd myApp
npm install
npm install mysql --save
npm install pug --save

然后参考这里调整为调试模式,也就是每次保存都会自动重启运行

npm run dev

打开本地5000端口发现程序正常运行啦

添加路由

其实后端的任务主要就在于添加路由了,修改routes文件夹中的index.js文件

任务1 搜索
  1. 用get方法添加路由’\search’,渲染页面’search.pug’,显示一个表单,表单中有三个字段,标题、内容与日期,这三个字段可以都填,也可有不填的字段,如果填了多个字段就是复合查询,表单提交后传入后端路由’\search_handler’。
  2. 用post方法添加路由’\search_handler’,然后根据前端传入的值访问数据库进行查询,为了美观,处理一下日期的显示再将结果渲染到前端’news_list.pug’页面。
//搜索界面
router.get('/search',(req,res)=>{
  res.render('search.pug', { title: 'Search News' });
});
//搜索处理
router.post('/search_handler',(req,res)=>{
  var body = req.body;
  var fetchSql = "select source_url,source,title,author,date " +
      "from news where title like '%" + body.title + "%' and " + 
      "content like '%" + body.content +"%'";
  if(body.date!=""){
    var fetchSql = fetchSql + " and date = '" + body.date + "'";
  }
  mysql.query(fetchSql, function(err, result, fields) {
    var regExp = /((\d{4}|\d{2})(\-|\/|\.)\d{1,2}\3\d{1,2})/;
    for(let i=0;i<result.length;i++){
      result[i].date = regExp.exec(JSON.stringify(result[i].date))[0];
    }
  res.render('news_list.pug', { title: 'News List', news_list: result});
   });
});
任务2 时间热度分析
  1. 用get方法添加路由’\time’,渲染页面’time.pug’,显示一个表单,表单中有两个字段,标题、内容,这两个字段可以都填,也可有不填的字段,如果填了多个字段就是复合查询,表单提交后传入后端路由’\time_handler’。
  2. 用post方法添加路由’\time_handler’,根据表单传入的值使用groupby语句对数据库进行查询,sql语句构造如下
var fetchSql = "select date,count(*) from " + 
    "news where title like '%" + body.title + 
    "%' and content like '%" + body.content + "%' group by date order by date asc";

sql数据库会返回一个类似这样的对象:

//[{"date":"2021-04-26T16:00:00.000Z","count(*)":3},{"date":"2021-04-27T16:00:00.000Z","count(*)":4},{"date":"2021-04-28T16:00:00.000Z","count(*)":7}]

为了前端展示数据,在这里我必须将数据进行处理,也就是要索引到对象里的值,然后令人头大的事情就来了…

count(*)那一列无论如何也无法直接引用到,试了比如result[0]['count(*)']等等多种表示形式都不行,但用这样的方式索引date那一列就可以

sql语句里面换列名,还是索引不到,甚至date换了列名,用其新列名也索引不到date了

这个确实太奇怪了,最后想了一个其它的办法终于拿到了这个对象里的数据,总之就是把这些键值对对象里的值都拿出来放到数组里(好奇为什么这样双重循环就拿到了对象中的value…),再操作数组整理数据。然后得到两个数组传到前端’time_handler.ejs’文件。

执行sql以及其回调:

  mysql.query(fetchSql, function(err, result, fields) {
      var regExp = /((\d{4}|\d{2})(\-|\/|\.)\d{1,2}\3\d{1,2})/;
      var date_list = new Array();
      var count = new Array();
      var vals = new Array();
      for (let i = 0; i < result.length; i++) { 
      for (let j in result[0]){
          vals.push(result[i][j]);
       }
     }
     for (let i=0;i<vals.length;i++){
       if(i%2==1) count.push(vals[i]);
       else date_list.push(regExp.exec(JSON.stringify(vals[i]))[0]);
     }
    res.render('time_handler', { date: [date_list],count:[count]});
      });
任务3 时间热度与来源分析

为了从更多维度展示数据,我加入了这样一个任务。

  1. 用get方法添加路由’\time_source’,渲染页面’time_source.pug’,显示一个表单,表单中有两个字段,标题、内容,这两个字段可以都填,也可有不填的字段,如果填了多个字段就是复合查询,表单提交后传入后端路由’\time_source_handler’。
  2. 用post方法添加路由’\time_source_handler’。首先明确我需要传给前端的数据类型是这样的
 ['date', '2021-4-28', ...],
 ['搜狐新闻', 26...],
 ['网易新闻', 13...],
 ['澎湃新闻', 21...],

我先想到的是这样:

select date,source,count(*) cou from news where title like '%%' and content like '%新冠%' 
group by date,source order by date asc;

这样确实得到了我们想要的数据,但这样的话相当于将数据库的事情交到了后端代码来完成,没有充分利用好数据库…更何况JS索引sql返回对象这个事情真是奇奇怪怪…
所以我们要在数据库中充分完成数据规整这个任务,于是我参考了这里这里
于是

select date,
sum(case when source ='搜狐新闻' THEN cou Else '0' End) '搜狐新闻',
sum(case when source ='网易新闻' THEN cou Else '0' End) '网易新闻',
sum(case when source ='澎湃新闻' THEN cou Else '0' End) '澎湃新闻',
sum(cou) 
from
(select date,source,count(*) cou from news where title like '%%' and content like '%新冠%' 
group by date,source order by date asc) as table1
group by date order by date asc;

结果:
在这里插入图片描述
这下终于体会到sql的好了!真yyds。之前用了orm就觉得sql不好用,但这样的复杂查询orm好像有点困难。
然后就根据任务2如法炮制吧,
执行sql以及其回调
这部分相关代码略长,就不贴出了,具体见作业压缩包中代码文件或pdf报告文件

Express框架:前端

总体来说,我没有再使用样例中的方法,直接使用html文件作为访问地址。
在我的实现中,需要先访问定义的路由,然后渲染前端表单页面,提交表单再重定向到新的路由进行与数据库的交互,再将数据渲染到前端进行展示。以搜索任务为例大概就是这样:
在这里插入图片描述

pug文件渲染

为什么要使用pug?

对于html文件,在后期维护和修改时,一不小心少了一个尖括号,或者某个标签的开始和闭合没有对应上,就会导致DOM结构的混乱甚至是错误。所以,有人发明了HAML,它最大的特色就是使用缩进排列来代替成对标签。受HAML的启发,pug进行了javascript的实现。Pug原名不叫Pug,是大名鼎鼎的jade,后来由于商标的原因,改为Pug,哈巴狗。其实只是换个名字,语法都与jade一样。丑话说在前面,Pug有它本身的缺点——可移植性差,调试困难,性能并不出色,但使用它可以加快开发效率。

在实践过程中,我发现使用pug确实很方便,无需JS与html的切换,不需要标签的闭合尖括号记号。
以news_list.pug文件为例,这个文件渲染了任务1的查询结果页面,以下是表格生成部分

table(width="100%")
    ul
      each news in news_list
        tr(class='cardLayout')
          td
            a(href=news.source_url) #{news.title}
          td
            | #{news.source}
          td
            | #{news.date}

      else
        li There are no news.

但如果使用JS+HTML(使用jQuery)确实要复杂很多:

 <div class="cardLayout" style="margin: 10px 0px">
        <table width="100%" id="record2"></table>
    </div>
    <script>
        $(document).ready(function() {
            $("input:button").click(function() {
                $.get('/process_get?title=' + $("input:text").val(), function(data) {
                    $("#record2").empty();
                    $("#record2").append('<tr class="cardLayout"><td>url</td><td>source_name</td>' +
                        '<td>title</td><td>author</td><td>publish_date</td></tr>');
                    for (let list of data) {
                        let table = '<tr class="cardLayout"><td>';
                        Object.values(list).forEach(element => {
                            table += (element + '</td><td>');
                        });
                        $("#record2").append(table + '</td></tr>');
                    }
                });
            });

        });
    </script>

又利用pug文件生成了导航栏

 div(class='container-fluid')
      div(class='row')
        div(class='col-sm-2')
          block sidebar
            ul(class='sidebar-nav')
              li 
                a(href='/search') Search
              li
                a(href='/time') Time Analysis
              li
                a(href='/time_source') Time and Source

由于脚手架默认是用ejs渲染,所以render文件时需要加上文件后缀名.pug。

ejs文件渲染实现echarts

需使用HTML+JS,实现echarts要好好看看官方网站,例子给的也很全了。

用CDN方法引入echarts文件

<script src="https://cdn.staticfile.org/echarts/4.3.0/echarts.min.js"></script>

任务2中时间热度分析渲染折线图

有点迷的事情又出现了,后端是数组传到前端之后却变成了字符串类型…使用split方法切分字符串生成数组。

var linedate = '<%= date %>';
    var linecount = '<%= count %>';
    var ldate = linedate.split(",");
    var lcount = linecount.split(",");
    var chartDom = document.getElementById('main');
    var myChart = echarts.init(chartDom);
    var option;


    option = {
        title: {
                text: 'Time Heat Analysis'
            },
    xAxis: {
        type: 'category',
        data: ldate
    },
    yAxis: {
        type: 'value'
    },
    series: [{
        data: lcount,
        type: 'line'
    }]
};

option && myChart.setOption(option);

任务3中时间热度与来源分析渲染

主要参考官方API
同任务2,传到前端的数据还是要做些处理
代码挺长的就不贴了

结果展示

Navicat中展示爬取的数据

只要程序运行,数据量可以不断变大,目前大概有上千条
图片见作业压缩包中pdf文件

访问’localhost:5000/search’
在这里插入图片描述
左侧是导航栏,点击跳转
向表单中输入查询词
如只在content中输入某个特定的词,会返回内容中带有该词的全部新闻
图片请见作业压缩包中pdf文件

访问’localhost:5000/time’
时间热度分析
在这里插入图片描述
content中输入某个特定的词,提交
在这里插入图片描述
当数据多了之后,时间跨度变长,可以使用每一周或每一月作为横轴的一个标度来进行可视化。

由于我开始爬取数据的时间比较晚,可能从4月26日左右才开始,所以前面几日新闻的总量就少,所以图中显示前面几日的新闻少,可能是因为新闻总量少,不一定是词的热度就很低。

另外对于时间热度来说,感觉应该在每一日有相同新闻总量的条件下,每日之间的新闻量才具可比性,进行这样的时间热度分析才更有意义,这是一个可优化的地方。

访问’localhost:5000/time_source’
在这里插入图片描述
content中搜索某词
在这里插入图片描述
饼图反映了新闻在三个新闻来源的比例,下面的折线图反应每一种新闻来源中的新闻随时间的变化量。
该图有一定的交互性,比如在折线图中移动时间,饼图就会随之变动:
在这里插入图片描述
以上搜索都可以对多个查询条件进行复合查询

总结

项目完成了对于三个新闻网站的爬虫,并用Express框架实现了前后端。通过这个项目我对javascript语言与前端Web开发都有了进一步的熟悉。
对于爬虫部分,重要的是要分析好网页,学会jQuery的语法解析网页,并想清楚程序的请求逻辑,要注意判空操作。
对于网站前后端部分,大概有如下几点体会:

  1. 数据库:好好利用sql语句进行数据查询与整理,数据库能做的就不要交给代码
  2. Express框架:要了解其路由、重定向、中间件与模板渲染等技术与概念
  3. 前端:尝试使用pug模板,对程序员比较友好;尝试使用echarts进行可视化,要多参考官方的API

完成这个项目学到了很多,也遇到了很多奇奇怪怪的问题,而且整个开发速度有些慢,归根到底还是我对javascript与node.js不够熟悉。对这些技术点的学习确实还有一段长路要走。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值