@TOC
完整代码可以访问GitHub链接
实验要求
一、核心需求
1、选取3-5个代表性的新闻网站(比如新浪新闻、网易新闻等,或者某个垂直领域权威性的网站比如经济领域的雪球财经、东方财富等,或者体育领域的腾讯体育、虎扑体育等等)建立爬虫,针对不同网站的新闻页面进行分析,爬取出编码、标题、作者、时间、关键词、摘要、内容、来源等结构化信息,存储在数据库中。
2、建立网站提供对爬取内容的分项全文搜索,给出所查关键词的时间热度分析。
二、技术要求
1、必须采用Node.JS实现网络爬虫
2、必须采用Node.JS实现查询网站后端,HTML+JS实现前端(尽量不要使用任何前后端框架)
结果展示
demo展示(不包含讲解)
(带有讲解的前端展示已放入文件夹中)
实验过程
一、爬取新闻网站内容
设定了每天每隔两个小时进行定时爬虫,经过了一天多的新闻数据爬取,最终获得125条中国广播网的新闻、336条东方财富的新闻和69条网易体育的新闻,具体数据如下:
中国广播网:东方财富网:
网易体育网:
1、总体流程
- 根据url读取种子页面
- 获得种子页面中的网页链接
- 处理链接的url,筛选出所需的新闻url
- 读取每条新闻url的页面
- 分析页面结构,提取页面中标题、内容、刊登时间等字段
- 将提取的字段存储入mysql数据库中
(其中由于不同新闻网站的url格式和页面结构不同,导致不同网页的筛选url格式和爬取字段内容不同,所以对于不同的页面,步骤三、五、六将会存在区别,以下将分为爬取新闻网站中相似步骤和不同步骤两部分详细讲述)
2、爬取不同新闻网站中相同的步骤
接下来以中国广播网为例,进行读取种子页面、获得种子页面中的网页链接、读取每条新闻url的页面三个步骤
1)读取种子页面
==request==:第三方HTTP请求工具,向网页发起访问,接收返回内容
使用request中的GET方法,根据URL获取页面信息(GET方法的模板)
// 使用request库,向网页发起访问,接收返回内容
var request = require('request')
request('url', function (err, res, body) {
// 页面请求成功
if (!err && res.statusCode == 200) {
console.log(body)
}
})
在GET方法中设置参数headers,防止网站屏蔽我们的爬虫
// 使用request库,向网页发起访问,接收返回内容
var request = require('request')
// 定义request模块,进行异步fetch url
function MyRequest(url, callback) {
// 设置GET方法中的参数,其中包括headers
var options = {
url: url,
encoding: null,
headers: headers,
timeout: 10000
}
request(options, callback)
}
// 调用request模块,传入种子页面的URL,爬取网站信息
MyRequest(seedURL, function(err, res, body) {
// 页面请求成功
if (!err && res.statusCode == 200) {
console.log(body)
}
});
==iconv-lite==:纯javascript转化字符编码工具
使用iconv-lite中的decode方法将爬取的网站内容转码至utf-8格式
// 使用iconv-lite库,对网页内容进行转码
var iconv = require('iconv-lite')
if (!err && res.statusCode == 200) {
// 对爬取内容进行转码
seed_html = iconv.decode(body,Encode)
console.log(seed_html)
}
此时将获得网页F12后的所有内容
2)获得种子页面中的网页链接
由于我们之前爬取的种子页面相当于一个新闻目录,我们需爬取其中的所有链接,来获取目录中每条新闻的网址
==cheerio==:从html的片断中构建DOM结构,然后提供像jquery一样的css选择器查询
var cheerio = require('cheerio')
我们首先通过cheerio模块中的load函数创建一个和jQuery选择器用法差不多的选择器 $
其中load函数第一个参数html就是之前http.get方法中所获得的数据;第二个参数可选,主要是用来设置格式,比如decodeEntities:false设置了不会出现中文乱码
// 创建一个选择器 $,decodeEntities:false设置了不会出现中文乱码
var $ = cheerio.load(seed_html, { decodeEntities: false });
由于链接的格式为,所以采用选择器$对此格式的链接进行获取,返回选择到的伪数组实例对象
// 获取种子页面中的所有新闻网页的链接,格式为<a href="">
var URL_format = 'a'
try {
html_URL = eval(URL_format)
} catch (error) {
console.log("获取种子页面中的链接出现错误:" + error)
}
结果为所有链接的信息
使用$('div').each(function)方法,遍历 jQuery 选择器选择到的伪数组实例对象,再通过jquery中用attr()方法来获取href后的网址
attr(属性名) :获取属性的值(取得第一个匹配元素的属性值。通过这个方法可以方便地从第一个匹配元素中获取一个属性的值。如果元素没有相应属性,则返回 undefined )
html_URL.each(function(i,body){ // 遍历伪数组实例对象,获取每个对象的序号i、信息内容body
try {
// 获取href后的页面网址
var href = "";
href = $(body).attr("href");
//console.log(href)
} catch (error) {
console.log("获取页面网址出现错误:" + error)
}
})
爬取的网页链接存在以下几种情况,对其分别进行处理
- undefined 元素没有相应属性,不进行网址保存即可
- //www.cnr.cn/ 网址缺少了http:开头,在最前方加上即可
- http://military.cnr.cn/ 完整的网址,无需改动
- https://www.cnrmall.com/ 完整的网址,无需改动
- ./ent/zx/20210417/t20210417_525464354.shtml 网址中的一部分,需在其前面加上种子页面的网址,即为完整的网址
- javascript:void(0) 表示网页不存在,在其前面加上种子页面的网址和/,即为完整的网址
// 处理网址信息
if (typeof(href) == "undefined") { // 为获取到href属性,返回undefined,不进行网址保存即可
return true
}
// http://开头的或者https://开头,完整的网址,无需改动
if (href.toLowerCase().indexOf('http://') >= 0 || href.toLowerCase().indexOf('https://') >= 0) whole_URL = href
// //开头的,缺少了http:开头,在最前方加上即可
else if (href.startsWith('//')) whole_URL = 'http:' + href
// 其他,在其前面加上种子页面的网址
else whole_URL = seedURL + href
3)读取每条新闻url的页面
与获取种子页面信息类似,通过MyRequest方法进行访问,获取页面信息,再通过iconv模块的decode方法将其解码为utf-8,之后使用cheerio模块的load方法创建选择器$
获得的页面信息如图所示,与F12内容相同
3、根据不同网站的页面信息,分别提取编码、标题、作者、时间、关键词、摘要、内容、来源等内容
1)中国广播网
- 根据正确新闻网址的格式,对获得的网址进行判断,选取正确的新闻网址
观察到网页最后为(八位的年月日/t八位数字_九位数字.shtml)组成,以此进行正则化筛选
// 根据正确的网页格式,正则化筛选网址
var url_reg = /\/(\d{8})\/t(\d{8})_(\d{9}).shtml/
// 检验是否符合新闻url的正则表达式
if (!url_reg.test(whole_URL)) return
else{ // 否则根据网址解析网页,获得所需内容
news_get_info(whole_URL)
}
- 从页面中提取标题、作者、时间、内容和来源五个字段
标题:
获取为“article-header”的class类,再进入标签h1,从而获得标题的内容
var title_format = "$('.article-header > h1').text()"
// 存储标题,若不存在标题则设置为空
if (title_format == "") fetch.title = ""
else fetch.title = eval(title_format)
作者:
获取为“editor”的class类,从而获得作者的内容,由于获得的内容中存在\n、\t等符号,使用replace将其删除
var author_format = "$('.editor').text()"
// 存储作者名称,若不存在,将其设置为网页名,其中删除空格、\t、\n等字符
if (author_format == "") fetch.author = source_name
else {
fetch.author = eval(author_format)
if (fetch.author != null){
fetch.author = fetch.author.replace(/\s/g, "")
}
}
时间:
获取为“source”的class类,再进入标签span,从而获得时间的内容,删除空格、\t、\n等字符,根据设定的正则表达式获取年月日的信息,将其中的年月代替为-,将日删除,即为新闻的刊登日期
var date_format = "$('.source > span').text()"
// 解析时间日期
var regExp = /((\d{4}|\d{2})(\-|\/|\.)\d{1,2}\3\d{1,2})|(\d{4}年\d{1,2}月\d{1,2}日)/
// 存储刊登日期,由于爬得的日期第一部分则为所需格式的年月日,无需进行时间提取
if (date_format != "") {
fetch.publish_date = eval(date_format)
if (fetch.publish_date != null){
fetch.publish_date = fetch.publish_date.replace(/\s/g, "")
}
}
// 根据正则化式,在一个指定字符串中执行一个搜索匹配,返回匹配得到的数组
fetch.publish_date = regExp.exec(fetch.publish_date)[0];
// 将年月替代为-,将日删除
fetch.publish_date = fetch.publish_date.replace('年', '-')
fetch.publish_date = fetch.publish_date.replace('月', '-')
fetch.publish_date = fetch.publish_date.replace('日', '')
console.log('date: ' + fetch.publish_date)
// 将其转换为Date的格式
fetch.publish_date = new Date(fetch.publish_date).toFormat("YYYY-MM-DD")
内容:
获取为“article-body”的class类,再进入标签div,从而获得新闻的内容,由于获得的内容中存在\n、空格等符号,使用replace将其删除
var content_format = "$('.article-body > div').text()"
// 存储内容,若不存在,将其设置为空,其中删除空格、\t、\n等字符
if (content_format == "") fetch.content = ""
else {
fetch.content = eval(content_format)
if (fetch.content != null){
fetch.content = fetch.content.replace(/\s/g, "")
}
}
来源:
获取为“source”的class类,再进入标签span,从而获得来源的内容,将其根据冒号进行分割,获取第二个元素,并且删除内容"原创版权禁止商业转载",即为来源的信息
var source_format = "$('.source > span').text()"
// 存储来源名,若不存在,将其设置为网页名,其中根据格式获取冒号后内容,再删除"原创版权禁止商业转载"内容
if (source_format == "") fetch.source = source_name
else {
fetch.source = eval(source_format).split(" ")[1].split(":")[1]
if (fetch.source != null){
fetch.source = fetch.source.replace("原创版权禁止商业转载","")
}
}
- 表格字段内容
创建表格news_info用于存放中国广播网的信息,news_info表格式如下所示:
CREATE TABLE IF NOT EXISTS `news_info` (
// 用于标记每条数据的id,其值不可以为空,并且在插入数据后自动增加(一般用于主键)
`id` int(11) NOT NULL AUTO_INCREMENT,
// 用于存放网址url,默认为空字符
`url` varchar(200) DEFAULT NULL,
// 用于存放种子页面的来源,默认为空字符
`source_name` varchar(200) DEFAULT NULL,
// 用于存放网页的编码方式,默认为空字符
`source_encoding` varchar(45) DEFAULT NULL,
// 用于存放网页的标题,默认为空字符
`title` varchar(200) DEFAULT NULL,
// 用于存放新闻的来源,默认为空字符
`source` varchar(200) DEFAULT NULL,
// 用于存放新闻的作者,默认为空字符
`author` varchar(200) DEFAULT NULL,
// 用于存放新闻的刊登日期,默认为空字符
`publish_date` date DEFAULT NULL,
// 用于存放爬取网页的时间,默认为空字符
`crawltime` datetime DEFAULT NULL,
// 用于存放网页的内容
`content` longtext,
// 用于存放插入数据的时间,默认为插入数据的时间
`createtime` datetime DEFAULT CURRENT_TIMESTAMP,
// 将id设置为主键
PRIMARY KEY (`id`),
// id不可以重复
UNIQUE KEY `id_UNIQUE` (`id`),
// url不可以重复
UNIQUE KEY `url_UNIQUE` (`url`)
// 设置存储引擎,这是编码为utf8
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2)网易体育
- 根据正确新闻网址的格式,对获得的网址进行判断,选取正确的新闻网址
观察到网页最后为(/article/一个字母一个数字一个字母一个数字四个字母八个数字.shtml)组成,以此进行正则化筛选
// 根据正确的网页格式,正则化筛选网址
var url_reg = /\/article\/(\w{1})(\d{1})(\w{1})(\d{1})(\w{4})(\d{4})(\d{4}).html/
// 检验是否符合新闻url的正则表达式
if (!url_reg.test(whole_URL)) return
else{ // 否则根据网址解析网页,获得所需内容
news_get_info(whole_URL)
}
- 从页面中提取标题、作者、时间、来源、内容和责任编辑六个字段
标题:
获取为“post_title”的class类,从而获得标题的内容
var title_format = "$('.post_title').text()"
// 存储标题,若不存在标题则设置为空
if (title_format == "") fetch.title = ""
else fetch.title = eval(title_format)
作者:
获取为“post_author”的class类,从而获得作者的内容,将其根据冒号进行分割,获取第三个元素,并且删除空格、\t、\n等字符,删除“作者”内容,删除“责任编辑”内容,即为作者的信息
var author_format = "$('.post_author').text()"
// 存储作者名称,若不存在,将其设置为网页名,其中根据冒号进行分割,获取第三部分的内容,再删除空格、\t、\n等字符,删除“责任编辑”内容
if (author_format == "") fetch.author = source_name
else{
fetch.author = eval(author_format).split(":")[2]
if (fetch.author != null){
fetch.author = fetch.author.replace(/\s/g, "").replace("作者","").replace("责任编辑","")
}
}
时间:
获取为“post_info”的class类,从而获得时间的内容,删除空格、\t、\n等字符,根据设定的正则表达式获取年月日的信息,将其中的年月代替为-,将日删除,即为新闻的刊登日期
var date_format = "$('.post_info').text()"
// 解析时间日期
var regExp = /((\d{4}|\d{2})(\-|\/|\.)\d{1,2}\3\d{1,2})|(\d{4}年\d{1,2}月\d{1,2}日)/
// 存储刊登日期,由于爬得的日期中存在空格等字符,使用replace将其删除
if (date_format != "") {
fetch.publish_date = eval(date_format)
if (fetch.publish_date != null){
fetch.publish_date = fetch.publish_date.replace(/\s/g, "")
}
}
// 根据正则化式,在一个指定字符串中执行一个搜索匹配,返回匹配得到的数组
fetch.publish_date = regExp.exec(fetch.publish_date)[0];
// 将年月替代为-,将日删除
fetch.publish_date = fetch.publish_date.replace('年', '-')
fetch.publish_date = fetch.publish_date.replace('月', '-')
fetch.publish_date = fetch.publish_date.replace('日', '')
console.log('date: ' + fetch.publish_date)
// 将其转换为Date的格式
fetch.publish_date = new Date(fetch.publish_date).toFormat("YYYY-MM-DD")
来源:
获取为“post_author”的class类,从而获得来源的内容,将其根据冒号进行分割,获取第二个元素,并且删除空格、\t、\n等字符,删除“作者”内容,删除“责任编辑”内容,即为来源的信息
var source_format = "$('.post_author').text()"
// 存储来源名,若不存在,将其设置为网页名,其中根据冒号进行分割,获取第二部分的内容,再删除空格、\t、\n等字符,删除“作者”内容
if (source_format == "") fetch.source = source_name
else{
fetch.source = eval(source_format).split(":")[1]
if (fetch.source != null){
fetch.source = fetch.source.replace(/\s/g, "").replace("作者","").replace("责任编辑","")
}
}
内容:
获取为“post_body”的class类,再进入标签p,从而获得内容的信息
var content_format = "$('.post_body > p').text()"
// 存储内容,若不存在,将其设置为空
if (content_format == "") fetch.content = ""
else fetch.content = eval(content_format)
责任编辑:
获取为“post_author”的class类,从而获得责任编辑的内容,将其根据冒号进行分割,获取第四个元素,并且删除空格、\t、\n等字符,即为责任编辑的信息
var edit_format = "$('.post_author').text()"
// 存储责任编辑名称,若不存在,将其设置为网页名,其中根据冒号进行分割,获取第四部分的内容,再删除空格、\t、\n等字符
if (edit_format == "") fetch.edit = source_name
else {
fetch.edit = eval(edit_format).split(":")[3]
if (fetch.edit != null){
fetch.edit = fetch.edit.replace(/\s/g, "")
}
}
- 表格字段内容
创建表格sports_info用于存放网易体育网的信息,sports_info表格式如下所示:
CREATE TABLE IF NOT EXISTS `sports_info` (
// 用于标记每条数据的id,其值不可以为空,并且在插入数据后自动增加(一般用于主键)
`id` int(11) NOT NULL AUTO_INCREMENT,
// 用于存放网址url,默认为空字符
`url` varchar(200) DEFAULT NULL,
// 用于存放种子页面的来源,默认为空字符
`source_name` varchar(200) DEFAULT NULL,
// 用于存放网页的编码方式,默认为空字符
`source_encoding` varchar(45) DEFAULT NULL,
// 用于存放网页的标题,默认为空字符
`title` varchar(200) DEFAULT NULL,
// 用于存放新闻的作者,默认为空字符
`author` varchar(200) DEFAULT NULL,
// 用于存放新闻的编辑,默认为空字符
`editor` varchar(200) DEFAULT NULL,
// 用于存放新闻的刊登日期,默认为空字符
`publish_date` date DEFAULT NULL,
// 用于存放新闻的来源,默认为空字符
`source` varchar(200) DEFAULT NULL,
// 用于存放爬取网页的时间,默认为空字符
`crawltime` datetime DEFAULT NULL,
// 用于存放网页的内容
`content` longtext,
// 用于存放插入数据的时间,默认为插入数据的时间
`createtime` datetime DEFAULT CURRENT_TIMESTAMP,
// 将id设置为主键
PRIMARY KEY (`id`),
// id不可以重复
UNIQUE KEY `id_UNIQUE` (`id`),
// url不可以重复
UNIQUE KEY `url_UNIQUE` (`url`)
// 设置存储引擎,这是编码为gbk
) ENGINE=InnoDB DEFAULT CHARSET=gbk;
3)东方财富
- 根据正确新闻网址的格式,对获得的网址进行判断,选取正确的新闻网址
观察到网页最后为(a/t十八位数字.html)组成,以此进行正则化筛选
// 根据正确的网页格式,正则化筛选网址
var url_reg = /\/a\/(\d{18}).html/
// 检验是否符合新闻url的正则表达式
if (!url_reg.test(whole_URL)) return
else{ // 否则根据网址解析网页,获得所需内容
news_get_info(whole_URL)
}
- 从页面中提取标题、责任编辑、时间、来源、评论数、讨论数、内容和摘要八个字段
标题:
获取为“newsContent”的class类,然后进入h1标签,从而获得标题的内容
var title_format = "$('.newsContent > h1').text()"
// 存储标题,若不存在标题则设置为空
if (title_format == "") fetch.title = ""
else fetch.title = eval(title_format)
责任编辑:
获取为“res-edit”的class类,从而获得责任编辑的内容,删除空格、\t、\n等字符,即为责任编辑的信息
var edit_format = "$('.res-edit').text()"
// 存储责任编辑名称,若不存在,将其设置为网页名,由于爬取的内容存在\n等字符,通过replace将其删除
if (edit_format == "") fetch.edit = source_name
else {
fetch.edit = eval(edit_format)
if (fetch.edit != null){
fetch.edit = fetch.edit.replace(/\s/g, "")
}
}
时间:
获取为“time”的class类,从而获得刊登日期的内容,将其根据空格进行分割,获取第一个元素,将其中的年月代替为-,将日删除,即为新闻的刊登日期
var date_format = "$('.time').text()"
// 存储刊登日期,根据空格将其分割,获取第一个元素,即为日期
if (date_format != "") {
fetch.publish_date = eval(date_format).split(" ")[0]
}
// 将年月替代为-,将日删除
fetch.publish_date = fetch.publish_date.replace('年', '-')
fetch.publish_date = fetch.publish_date.replace('月', '-')
fetch.publish_date = fetch.publish_date.replace('日', '')
console.log('date: ' + fetch.publish_date)
// 将其转换为Date的格式
fetch.publish_date = new Date(fetch.publish_date).toFormat("YYYY-MM-DD")
来源:
获取为“source data-source”的class类,从而获得来源的内容,删除空格、\t、\n等字符,即为来源的信息
var source_format = "$('.source.data-source').text()"
// 存储内容,若不存在,将其设置为空
if (content_format == "") fetch.content = ""
else {
fetch.content = eval(content_format)
if (fetch.content != null){
fetch.content = fetch.content.replace(/\s/g, "")
}
}
评论数:
获取为“cNumShow num”的class类,从而获得评论数的内容
var comment_format = "$('.cNumShow.num').text()"
// 存储参与人数,若不存在,将其设置为空
if (partici_format == "") fetch.partici = 0
else fetch.partici = eval(partici_format)
讨论数:
获取为“num ml5”的class类,从而获得讨论数的内容
var partici_format = "$('.num.ml5').text()"
// 存储评论数量,若不存在,将其设置为空
if (comment_format == "") fetch.comment = 0
else fetch.comment = eval(comment_format)
内容:
获取为“Body”的class类,然后进入p标签,从而获得新闻内容,删除空格、\t、\n等字符,即为内容的信息
var content_format = "$('.Body > p').text()"
// 存储内容,若不存在,将其设置为空,由于爬取的内容存在\n等字符,通过replace将其删除
if (content_format == "") fetch.content = ""
else {
fetch.content = eval(content_format)
if (fetch.content != null){
fetch.content = fetch.content.replace(/\s/g, "")
}
}
摘要:
获取为“b-review”的class类,从而获得摘要的内容
var desc_format = " $('.b-review').text()"
// 存储摘要,若不存在,将其设置为空
if (desc_format == "") fetch.desc = ""
else fetch.desc = eval(desc_format)
- 表格字段内容
创建表格finance_info用于存放网易体育网的信息,finance_info表格式如下所示:
CREATE TABLE IF NOT EXISTS `finance_info` (
// 用于标记每条数据的id,其值不可以为空,并且在插入数据后自动增加(一般用于主键)
`id` int(11) NOT NULL AUTO_INCREMENT,
// 用于存放网址url,默认为空字符
`url` varchar(200) DEFAULT NULL,
// 用于存放种子页面的来源,默认为空字符
`source_name` varchar(200) DEFAULT NULL,
// 用于存放网页的编码方式,默认为空字符
`source_encoding` varchar(45) DEFAULT NULL,
// 用于存放网页的标题,默认为空字符
`title` varchar(200) DEFAULT NULL,
// 用于存放新闻的编辑,默认为空字符
`editor` varchar(200) DEFAULT NULL,
// 用于存放新闻的刊登日期,默认为空字符
`publish_date` date DEFAULT NULL,
// 用于存放新闻的来源,默认为空字符
`source` varchar(200) DEFAULT NULL,
// 用于存放爬取网页的时间,默认为空字符
`crawltime` datetime DEFAULT NULL,
// 用于存放网页的内容
`content` longtext,
// 用于存放网页的摘要,默认为空字符
`description` varchar(200) DEFAULT NULL,
// 用于存放网页的参与人数,默认为空字符
`participate_number` varchar(45) DEFAULT NULL,
// 用于存放网页的评论数,默认为空字符
`comment_number` varchar(45) DEFAULT NULL,
// 用于存放插入数据的时间,默认为插入数据的时间
`createtime` datetime DEFAULT CURRENT_TIMESTAMP,
// 将id设置为主键
PRIMARY KEY (`id`),
// id不可以重复
UNIQUE KEY `id_UNIQUE` (`id`),
// url不可以重复
UNIQUE KEY `url_UNIQUE` (`url`)
// 设置存储引擎,这是编码为utf-8
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
4、将爬取数据存入数据库中
1)判断是否重复爬取相同的URL
==mysql==:用于连接数据库,对数据库进行增删改查等操作
在mysql.js文件中使用mysql库设置数据库的配置,包括用户名、密码以及数据库名称
// 使用mysql库连接数据库
var mysql = require("mysql")
// 设置数据库的配置,包括用户名、密码以及数据库名称
var pool = mysql.createPool({
host: '127.0.0.1',
user: 'root',
password: 'root',
database: 'test1'
})
然后,使用getConnection函数连接数据库,定义函数query用于执行具有查询语句和查询参数的数据库查询,输入sql语句和参数,返回callback
// 执行具有查询语句和查询参数的数据库查询,输入sql语句和参数,返回callback
var query = function(sql, sqlparam, callback) {
// 根据设置的数据库参数连接数据库
pool.getConnection(function(err, conn) {
if (err) {
callback(err, null)
} else {
// 进行查询语句的查询
conn.query(sql, sqlparam, function(err, result) {
conn.release(); //释放连接
callback(err, result); //事件驱动回调
})
}
})
}
定义函数query_noparam用于执行具有查询语句,没有查询参数的数据库查询,输入sql语句,返回callback
// 执行具有查询语句,没有查询参数的数据库查询,输入sql语句,返回callback
var query_noparam = function(sql, callback) {
// 根据设置的数据库参数连接数据库
pool.getConnection(function(err, conn) {
if (err) {
callback(err, null)
} else {
// 进行查询语句的查询
conn.query(sql, function(err, result) {
conn.release() //释放连接
callback(err, result) //事件驱动回调
})
}
})
}
再将方法query和query_noparam模块化,便于在其余文件中进行使用
// 将方法query和query_noparam模块化,便于在其余文件中进行使用
exports.query = query
exports.query_noparam = query_noparam
由于在数据库中将url设置为UNIQE类型,因此需要url不能重复,所以在根据url爬取页面之前,先判断其url是否存在于数据库中,避免重复爬取相同的页面
// 从数据库中查询URL,判断之前是否进行该网址的爬取
var fetch_url_Sql = 'select url from news_info where url=?' // 从表web_info中提取相应URL的数据
var fetch_url_Sql_Params = [whole_URL] // 查询的参数
// 调用mysql文件中的query模块(具有参数的数据库语句查询)
mysql.query(fetch_url_Sql, fetch_url_Sql_Params, function(err, result) {
if (err) console.log(err)
// 查询返回内容表述数据库中存在该URL的数据,即重复爬取
if (result[0] != null) {
console.log('URL:' + whole_URL + 'duplicate!')
} else newsGet(myURL); // 未爬取过这个网页,进行新闻页面的读取
})
2)将爬取的数据存入数据库中
编写将爬取的数据写入数据库的mysql语句,编写插入语句中相应的参数,然后调用方法query,将数据写入mysql
// 编写将爬取的数据写入数据库的mysql语句
var fetchAddSql = 'INSERT INTO news_info(url,source_name,source_encoding,title,' +
'source,author,publish_date,crawltime,content) VALUES(?,?,?,?,?,?,?,?,?)'
// 插入语句中相应的参数
var fetchAddSql_Params = [fetch.url, fetch.source_name, fetch.source_encoding,
fetch.title, fetch.source, fetch.author, fetch.publish_date,
fetch.crawltime.toFormat("YYYY-MM-DD HH24:MI:SS"), fetch.content
]
// 执行sql,数据库中fetch表里的url属性是unique的,不会把重复的url内容写入数据库
mysql.query(fetchAddSql, fetchAddSql_Params, function(err, result) {
if (err) {
console.log(err);
}
}) //mysql写入
5、爬虫定时操作
==node-schedule==:用于设置定时任务
// 调用设置定时任务的模块
var schedule = require('node-schedule')
使用RecurrrenceRule函数制定任务的规则,将其设置为每天凌晨0点整开始,每隔两个小时自动爬取
// 定义规则
var rule = new schedule.RecurrenceRule()
// 设置参数
rule.hour = [0,2,4,6,8,10,12,14,16,18,20,22] // 每隔两小时进行自动爬取数据
rule.minute = 0
rule.second = 0
使用scheduleJob函数,进行函数get_info的执行,从而实现爬虫的定时操作
//定时执行 get_info() 函数
schedule.scheduleJob(rule, function() {
get_info()
})
二、分项全文搜索以及关键词的时间热度分析
1、前端设计
1)前端页面概述
由于爬取了三个类别的新闻网站,我将其分为三种主题的新闻信息查询,分别为正式新闻、体育新闻和财经新闻。
用户可以登入主页面,根据主页面中的新闻网站简介,选择查询哪一板块的新闻
在查询信息内容中,会提供不同的查询内容(包括标题、内容等字段)进行复合查询,若用户在相应字段不输入内容,则默认对其字段进行全文搜索在查询信息内容中,会以表格的形式向用户返回相应板块中最新的5条新闻内容
在查询信息内容中,在用户点击查询按钮后,会以表格形式返回用户的查询结果,并且以折线图的形式返回标题的内容的时间热度分析(展示用户搜索标题的字段在爬取内容中每一天包含的条数)
以下是查询标题中包含“疫苗”的结果
在查询词热度分析中,以柱状图向用户展示标题和内容的查询词的频率,便于用户了解哪些查询词是最常被查询的
2)主页面:home.html
主页面主要包括滚动的幻灯片用于欢迎用户前往网站、对于这个网站的介绍、对三种查询新闻网站的简介、提供链接进行查询新闻以及查看查询词的热度分析
- 滚动的幻灯片
使用了==Bootstrap==中的轮播(Carousel)插件
<div class="carousel slide" id="carousel-832580">
<ol class="carousel-indicators">
<li data-slide-to="0" data-target="#carousel-832580">
</li>
<li data-slide-to="1" data-target="#carousel-832580">
</li>
<li data-slide-to="2" data-target="#carousel-832580" class="active">
</li>
</ol>
<div class="carousel-inner">
<div class="item">
<img alt="" src="博客.jpg" class="img-responsive center-block" />
<div class="carousel-caption">
</div>
</div>
<div class="item">
<img alt="" src="欢迎.jpg" class="img-responsive center-block" />
<div class="carousel-caption">
</div>
</div>
<div class="item active">
<img alt="" src="欢迎表情包.jpg" class="img-responsive center-block" />
<div class="carousel-caption">
</div>
</div>
</div> <a class="left carousel-control" href="#carousel-832580" data-slide="prev"><span class="glyphicon glyphicon-chevron-left"></span></a> <a class="right carousel-control" href="#carousel-832580" data-slide="next"><span class="glyphicon glyphicon-chevron-right"></span></a>
</div>
- 对于这个网站的介绍
<div class="container">
<div class="row clearfix">
<div class="col-md-12 column">
<p class="lead text-left">
欢迎来到<strong>新闻内容查询的网站</strong> ,在这里你可以查询到<strong>中国新闻</strong>、<strong>体育娱乐</strong>和<strong>财经信息</strong>三个板块的新闻内容,其分别来自于网站<strong>中国广播网</strong>、<strong>网易体育</strong>和<strong>东方财富</strong>。
</p>
</div>
</div>
</div>
- 对三种查询新闻网站的简介
- 提供链接进行查询新闻和查看查询词的热度分析
使用了==Bootstrap==中的缩略图(thumbnail)插件
<div class="row">
<div class="col-md-4">
<div class="thumbnail">
<img alt="300x200" src="中国广播网.jpg" />
<div class="caption">
<h3>
中国广播网新闻
</h3>
<p>
中国广播网,由中央人民广播电台主办,具有鲜明的广播特色,致力于打造“全天24小时不间断直播的中文互动在线广播第一品牌”,建设全球最大中文音频网络门户,通过互联网"让中国的声音传向世界各地"。
</p>
<p>
<a class="btn btn-primary" href="/news_info.html">查询信息</a>
<a class="btn btn-primary" href="/news_search.html">查询词热度分析</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="thumbnail">
<img alt="300x200" src="网易体育网.jpg" />
<div class="caption">
<h3>
网易体育新闻
</h3>
<p>
网易体育,有态度的体育门户,包含体育新闻,NBA,CBA,英超,意甲,西甲,冠军杯,体育比分,足彩,福彩,体育秀色,网球,F1,棋牌,乒羽,体育论坛,中超,中国足球,综合体育等专业体育门户网站。
</p>
<p>
<a class="btn btn-primary" href="/sports_info.html">查询信息</a>
<a class="btn btn-primary" href="/sports_search.html">查询词热度分析</a>
</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="thumbnail">
<img alt="300x200" src="东方财富网.jpg" />
<div class="caption">
<h3>
东方财富网新闻
</h3>
<p>
东方财富网,专业的互联网财经媒体,提供7*24小时财经资讯及全球金融市场报价,汇聚全方位的综合财经资讯和金融市场资讯,覆盖股票、财经、基金、期货等。
</p>
<p>
<a class="btn btn-primary" href="/finance_info.html">查询信息</a>
<a class="btn btn-primary" href="/finance_search.html">查询词热度分析</a>
</p>
</div>
</div>
</div>
</div>
3)查询新闻页面(news_info.html、finance_info.html、sports_info.html)
三个新闻的查询页面结构大致相同,包括用户可以进行查询的字段、返回的5条推荐最新新闻、返回的用户查询内容以及查询词时间热度分析
- 用户可以查询的字段
由于每个新闻网站爬取的字段不同,我对于三个网站选取不同的字段,其中中国广播网包含标题、作者、内容和刊登时间;网易体育网包含标题、作者、编辑、内容和刊登时间;东方财富网包括标题、编辑、摘要、内容和刊登时间
<form>
<br> 标题:<input type="text" name="title_text">
<br>
<br> 内容:<input type="text" name="content_text">
<br>
<br> 作者:<input type="text" name="author_text">
<br>
<br> 刊登时间:<input type="text" name="publish_time_text">
<br>
<br> <input class="form-submit" type="button" value="查询">
</form>
- 返回的5条推荐最新新闻
其中为了美观,使用css改变了表格的格式,表格的css格式如下:
table thead,table tr {
border-top-width: 1px;
border-top-style: solid;
border-top-color: #a8bfde;
}
table {
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: #a8bfde;
}
/* Padding and font style */
table td, table th {
padding: 5px 10px;
font-size: 12px;
font-family: Verdana;
color: #5b7da3;
}
/* Alternating background colors */
table tr:nth-child(even) {
background: #d3dfed
}
table tr:nth-child(odd) {
background: #FFF
}
构建id = record1的表格,将其清空,然后添加表头,其中包括url、来源、标题、作者、刊登日期字段,再对GET操作得到的每一行数据改成符合表格形式,最后添加到record1中
<div class="cardLayout" style="margin: 10px 0px">
<table width="100%" id="record1"></table>
</div>
<script>
$(document).ready(function() {
$.get('/get_recommand_news_info', function(data) {
$("#record1").empty();
$("#record1").append('<tr class="cardLayout"><td>url</td><td>source</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>');
});
$("#record1").append(table + '</td></tr>');
}
})
});
</script>
- 返回的用户查询内容
其中使用了==bootstrap table==插件,使得对查询结果分页并以某个字段进行排序
构建id = record2的表格,并且为前面的提交按钮,绑定一个脚本,使得用户点击按钮后,方能显示查询结果的表格
通过GET方法访问给定的url,并且将返回的json文件,解析生成表格,其中field的值应该和返回的json文件对应属性的key/value相同
<div class="cardLayout" style="margin: 10px 0px">
<table width="100%" id="record2"></table>
</div>
<script>
$(document).ready(function() {
$("input:button").click(function() {
var params = '/get_news_info?title=' + $('input[name="title_text"]').val() +
'&content=' + $('input[name="content_text"]').val() +
'&author=' + $('input[name="author_text"]').val() +
'&publish_time=' + $('input[name="publish_time_text"]').val()
$(function(){
$("#record2").empty();
$('#record2').bootstrapTable({
url:params,
method:'GET',
pagination:true,
sidePagination:'client',
pageSize:5,
striped : true,
showRefresh:true,
search:true,
showToggle: true,
toolbar: '#toolbar',
showColumns : true,
columns:[{
field:'url',
title:'url',
},{
field:'source',
title:'source',
},{
field:'title',
title:'title',
sortable : true
},{
field:'author',
title:'author',
},{
field:'new_publish_date',
title:'publish_date',
sortable : true
}]
})
});
});
});
</script>
- 查询词时间热度分析
使用了==Echarts==,一个纯 Javascript 的图表库,用于绘制柱状图、折线图等图标,便于用户更直观的了解结果
针对查询词 ==标题字段== 的返回结果绘制查询词的时间热度分析图,折线图的横坐标为新闻刊登时间,纵坐标为查询结果中刊登时间为横坐标的条数
(由于只是针对标题字段,若用户未输入标题,则将返回数据库中所有新闻的时间热度分析图)
<!-- 为ECharts准备一个具备大小(宽高)的Dom -->
<div id="time" style="width: 800px;height:500px;"></div>
<script type="text/javascript">
$(document).ready(function() {
$("input:button").click(function() {
// 基于准备好的dom,初始化echarts实例
var myChart2 = echarts.init(document.getElementById('time'));
// 异步加载数据
var param = '/news_time_info?title=' + $('input[name="title_text"]').val()
$.get(param).done(function (result) {
myChart2.setOption({
tooltip: {},
xAxis: {
data: result[0]
},
yAxis: {},
series: [{
name: '频数',
type: 'line',
data: result[1]
}]
})
})
})
})
</script>
4)查询词热度分析页面(news_search.html、finance_search.html、sports_search.html)
使用了==Echarts==,一个纯 Javascript 的图表库
三个新闻的查询词热度分析页面结构大致相同,其中包括标题的查询词热度分析图和内容的查询词热度分析图,两张柱状图的横坐标为按照查询次数从高到低排序的查询词,纵坐标为查询次数
<!-- 为ECharts准备一个具备大小(宽高)的Dom -->
<div id="title" style="width: 400px;height:300px;"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart1 = echarts.init(document.getElementById('title'));
// 异步加载数据
$.get('/news_title_info').done(function (result) {
myChart1.setOption({
title: {
text: '标题查询词的热度分析'
},
tooltip: {},
xAxis: {
data: result[0]
},
yAxis: {},
series: [{
name: '频数',
type: 'bar',
data: result[1]
}]
})
})
</script>
<!-- 为ECharts准备一个具备大小(宽高)的Dom -->
<div id="content" style="width: 400px;height:300px;"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart2 = echarts.init(document.getElementById('content'));
// 异步加载数据
$.get('/news_content_info').done(function (result) {
myChart2.setOption({
title: {
text: '内容查询词的热度分析'
},
tooltip: {},
xAxis: {
data: result[0]
},
yAxis: {},
series: [{
name: '频数',
type: 'bar',
data: result[1]
}]
})
})
</script>
2、后端设计
三种新闻内容的后端设计大致相同,区别在于分别从不同的表中选择信息(分别从news_info表、finance_info表和sports_info表中获得查询和推荐新闻的结果、查询词时间热度分析结果;分别从newssearch表、sportssearch表和financesearch表中获得查询词的热度分析结果)
以下将以中国广播网的后端为例:
1)用户查询返回新闻
首先,从前端获得用户提交查询字段的内容,编写插入语句,将用户的查询内容插入newssearch表中
由于用户查询的内容均为字符串,所以将publish_date类型设置为字符类型,newssearch表的字段内容如下所示:
CREATE TABLE IF NOT EXISTS `newssearch` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(200) DEFAULT NULL,
`author` varchar(200) DEFAULT NULL,
`publish_date` varchar(200) DEFAULT NULL,
`content` longtext,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=gbk;
var searchSql = 'INSERT INTO newssearch(title,publish_date,author,content)' + ' VALUES (?,?,?,?)'
var searchSql_Params = [request.query.title,request.query.publish_time,request.query.author,request.query.content]
mysql.query(searchSql, searchSql_Params, function(err, result, fields) {
if (err) console.log(err)
})
然后,编写查询语句,从news_info表中选取包含用户查询内容的新闻信息,将结果转换为JSON类型,发送至前端
var fetchSql = "select url,source,title,author,cast(date_format(publish_date,'%Y-%m-%d') as char) as new_publish_date from news_info where title like '%" +
request.query.title + "%' and author like '%" +
request.query.author + "%' and content like '%" +
request.query.content + "%' and publish_date like '%" +
request.query.publish_time + "%'";
mysql.query(fetchSql, function(err, result, fields) {
response.writeHead(200, {
"Content-Type": "application/json"
})
response.status = true;
response.write(JSON.stringify(result));
response.end()
})
2)用户不查询,返回推荐的最新新闻
编写查询语句,将news_info表中信息根据publish_date进行降序排序,然后选取前5条的新闻信息,将结果转换为JSON类型,发送至前端
var fetchSql = "select url,source,title,author,cast(date_format(publish_date,'%Y-%m-%d') as char) as new_publish_date from news_info order by publish_date DESC limit 5";
mysql.query(fetchSql, function(err, result, fields) {
response.writeHead(200, {
"Content-Type": "application/json"
})
response.status = true;
response.write(JSON.stringify(result));
response.end()
})
3)查询词的热度分析
考虑到用户根据标题和内容查询的次数较多,所以针对这两个字段进行查询词的热度分析,提供用户在这两个字段中查询较高频率的内容
编写查询语句,选取news_info表中的标题或内容不为空的信息,对其根据标题或内容进行分组,再根据标题或内容的频数进行排序,返回标题或内容以及其频数至前端
// 返回标题查询词的热度分析
var fetchSql = "select title,count(*) from newssearch where title != '' group by title order by count(*) desc";
mysql.query(fetchSql, function(err, result, fields) {
response.writeHead(200, {
"Content-Type": "application/json"
})
var search = [];
var the_count = [];
for (var i = 0; i < result.length; i++) {
search[i] = (result[i]['title']).toString();
the_count[i] = result[i]['count(*)'];
}
var tempt = [search,the_count]
response.write(JSON.stringify(tempt));
response.end()
})
// 返回内容查询词的热度分析
var fetchSql = "select content,count(*) from newssearch where content != '' group by content order by count(*) desc";
mysql.query(fetchSql, function(err, result, fields) {
response.writeHead(200, {
"Content-Type": "application/json"
})
var search = [];
var the_count = [];
for (var i = 0; i < result.length; i++) {
search[i] = (result[i]['content']).toString();
the_count[i] = result[i]['count(*)'];
}
var tempt = [search,the_count]
response.write(JSON.stringify(tempt));
response.end()
})
4)查询词的时间热度分析
这里选定对用户查询的标题字段进行时间热度分析,返回标题中包含用户查询内容的信息在每一天的条数,若用户不输入标题字段,则对所有新闻信息进行时间热度分析
编写查询语句,从news_info表中选取包含标题查询内容的信息,将其按照publish_date分组,并根据publish_date字段升序排序,返回publish_date字段以及其对应的个数
var fetchSql = "select cast(date_format(publish_date,'%Y-%m-%d') as char) as new_publish_date,count(*) from news_info where title like '%" +
request.query.title + "%' group by publish_date order by publish_date";
mysql.query(fetchSql, function(err, result, fields) {
response.writeHead(200, {
"Content-Type": "application/json"
})
var search = [];
var the_count = [];
console.log(result)
for (var i = 0; i < result.length; i++) {
search[i] = (result[i]['new_publish_date']).toString();
the_count[i] = result[i]['count(*)'];
}
var tempt = [search,the_count]
response.write(JSON.stringify(tempt));
response.end()
})
3、扩展功能
1)对查询结果进行分页显示
使用了==bootstrap table==插件,使得对查询结果分页,当返回结果数量较多时,每页返回五条新闻信息
==bootstrap table==是非常方便好用的前端表格分页插件,使用者只需要提供数据源就能实现非常完美的分页效果,其分页方式可以分成客户端分页和服务端分页,其接收的数据源都是json数据格式。服务端分页在项目中应用得非常的广泛,但有时也需要使用客户端分页来加快分页速度,加快分页浏览效率。
2)对查询结果按某个字段进行排序
使用了==bootstrap table==插件,可以选择字段,对返回结果按照选定字段进行升序或降序排序,这里暂且选定了title和publish_date字段
3)对多个查询条件进行复合查询
对用户提供了多个查询字段的输入框,用户可以进行多个查询条件的复合查询
三、实验中遇到的问题以及需要注意的点
- 在进行replace删除一些无关字符时,需先对字符串判断是否为空,若对空的字符串进行replace操作,将会报错
- 在进行爬虫时,对爬取的url已经判断其是否在数据库中存在,再进行爬取url,但运行代码时仍然出现数据库url重复的报错,最终发现可能是电脑的原因,相同的代码在助教电脑上并没有出现这个问题,所以代码的逻辑是没有问题的,就忽略了这个错误
- 在编写前后端时,发现前端网页可以直接应用js库,通过应用网页版的js库,从而代替了下载插件,这一点使得调用插件编写前端更加方便了
- 由于设计表格时,将publish_date字段设置为日期的类型,从而导致输出到前端的表格中或是制作时间热度分析图都较为不方便,所以在提取时间字段时,使用cast(date_format(publish_date,'%Y-%m-%d') as char)方法,将其先转换成年-月-日的格式,在转换为字符串类型,传给前端
- 有时候在修改前端代码之后,未重新运行命令行node bin/www,直接进行页面的渲染查看,会发现页面未更新,需要停止之前的运行,重新执行命令行node bin/www,页面才会进行更新
- 最开始尝试过将内容以表格的形式返回前端,但由于内容字段东西过多,使得表格的格式不好看,最终放弃将内容或摘要字段返回前端的表格中
四、总结与感想
- 通过这次实验,我了解了如何通过nodejs进行网页数据的爬取以及构建前后端,体会到了使用nodejs中轻量级的库的方便和容易
- 通过对网页结构进行分析,我了解了如何使用cheerio中的选择器进行页面信息的提取
- 通过对爬得的字段进行正则表达式处理,了解了正则表达式的一些基本规则
- 通过前后端的设计,了解了如何在前端进行表格分页和绘制图标,更知道了如何进行网页格式的调整,从而对于前端网页的格式有了加深的理解,对前后端之间的交互有了加深的理解
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。