Web编程第一个实验项目
题目要求:
项目完成步骤
使用Node.js和模板库Cheerio,request做一个爬虫代码,爬取新浪新闻,网易新闻,中国新闻网中的各个部分。
下面以新浪新闻为例,分析页面:
1. 新闻页面分析
1.1 对某一篇新闻页面元素进行分析:
-
标题
main-title
-
作者
show_author
-
时间
date
-
关键字:正则化匹配:
$('meta[name=\"keywords\"]').eq(0).attr(\"content\")"
-
内容
article
-
来源
source
1.2 对新闻网页的URL进行分析
https://news.sina.com.cn/c/2021-04-23/doc-ikmxzfmk8553784.shtml
https://news.sina.com.cn/c/2021-04-23/doc-ikmyaawc1428218.shtml
https://news.sina.com.cn/o/2021-04-22/doc-ikmxzfmk8371786.shtml
从上述三个网址可知,每个网页都是由
https://news.sina.com.cn
网页链接/o/
一个字母2021-04-22
年月日,由四个数字-两个数字-两个数字组成doc-ikmxzfmk8371786.shtml
doc-15个字符.shtml
因此可以写出匹配网页的正则化为:/\/(\d{4})-(\d{2})-(\d{2})\/doc-(\w{15}).shtml/
2. 数据库设计和代码实现
2.1 设计数据库
- 在mysql中创建数据库
crawlnews
,然后在该数据库里创建表fetches
:
- 根据第1步分析的网页元素,设置fetches表的结构,url设置的属性为
UNIQUE
,因此插入数据之前需要判断是否已经爬取过相同的网页。
2.2 数据库连接和查询代码
- 在js爬虫代码中使用mysql,需要先引用mysql文件。
var mysql = require('./mysql.js');
- 其中在
mysql.js
连接数据库,并且完成了带参数和不带参数的sql语句查询。(password需要写自己mysql的密码)
3. 爬虫具体代码实现
根据上述新闻分析,实现一个爬虫代码:
3.1 网页解析代码实现
- 首先定义我们想要爬取的网页名称和网页URL,和该网页的编码方式(编码方式可以在网页的header中找到)
var source_name = "新浪新闻";
var myEncoding = "utf-8"; //转码成utf-8
var seedURL = 'https://news.sina.com.cn/china/';
-
在代码中定义我们需要爬取的网页的格式:带有
a
标签,标题从class = main-title
中获取,向keywords这种从headers中读取的,就使用正则化进行匹配,每个选择的内容都在第1.1部分进行了说明。
-
另外,还需要对新浪新闻中的网页进行分析,在第1.2部分中进行了解释
同理:网易新闻的页面解析代码为:
(已提供)中国新闻网的解析代码为:
3.2 爬虫代码实现
- 需要的模块介绍
需要在代码开始导入fs,request,cheerio,iconv-lite和date-utils五个模块,其中fs为node.js内置的。下面为每个模块的含义和示例:
fs模块:Node.js 文件系统,提供一组类似 UNIX(POSIX)标准的文件操作API。Node.js 文件系统(fs 模块)模块中的方法均有异步和同步版本,例如读取文件内容的函数有异步的 fs.readFile() 和同步的 fs.readFileSync()。
异步的方法函数最后一个参数为回调函数,回调函数的第一个参数包含了错误信息(error)。建议使用异步方法,比起同步,异步方法性能更高,速度更快,而且没有阻塞。
var fs = require("fs");
// 异步读取
fs.readFile('input.txt', function (err, data) {
if (err) {
return console.error(err);
}
console.log("异步读取: " + data.toString());
});
// 同步读取
var data = fs.readFileSync('input.txt');
console.log("同步读取: " + data.toString());
console.log("程序执行完毕。");
request:属于http模块,使用前需要引入http模块,主要作为客户端向HTTP服务器发起请求。
// 发送Get请求
// 第一个参数:请求的完整URL,包括参数
// 第二个参数:请求结果回调函数,会传入3个参数,第一个错误,第二个响应对象,第三个请求数据
request(url,function (error, response, data) {
console.log(data)
});
cheerio:cheerio是jquery核心功能的一个快速灵活而又简洁的实现,主要是为了用在服务器端需要对DOM进行操作的地方。
//把HTML告诉你的服务器.
const cheerio = require('cheerio');
const $ = cheerio.load('<h2 class="title">Hello world</h2>');
$('h2.title').text('Hello there!');
$('h2').addClass('welcome');
$.html();
//=> <html><head></head><body><h2 class="title welcome">Hello there!</h2></body></html>
iconv-lite:使用纯 javascript 转化字符编码。
var iconv = require('iconv-lite');
// Convert from an encoded buffer to js string.
str = iconv.decode(new Buffer([0x68, 0x65, 0x6c, 0x6c, 0x6f]), 'win1251');
// Convert from js string to an encoded buffer.
buf = iconv.encode("Sample input string", 'win1251');
// Check if encoding is supported
iconv.encodingExists("us-ascii")
date-utils:nodejs日期时间插件。
require('date-utils');
var dt = new Date();
console.log(dt.toFormat("YYYY-MM-DD HH24:MI:SS"));
- 在代码中导入需要的模块
var fs = require('fs');
var myRequest = require('request')
var myCheerio = require('cheerio')
var myIconv = require('iconv-lite')
require('date-utils');
-
设置headers,防止网页屏蔽该爬虫,下面代码为设置的request函数。
-
实现爬取新闻主页中的所有新闻网页。seedURL为我们需要爬取的新闻主页,因此对新闻主页爬取后对里面所有的链接进行判断,判断是否是一个合理的新闻网页。使用之前找到的新闻网址的规律,进行正则化项匹配,匹配成功后需要调用mysql中的查询函数。
-
因为需要判断是否爬取过相同的网页,因此实现查询(select)的sql语句:
fetch_url_Sql
和需要传入的参数(这次处理的网页URL):fetch_url_Sql_Params
,调用mysql的query函数对数据库进行查询,通过有无查询结果来判断之前是否已经爬取。 -
如果不存在,再对其网页进行爬取,即调用
newsGet(myURL)
函数。
-
爬取新闻网页。用
cheerio
解析网页,定义一个新的变量fetch,存储我们需要的标题,作者,内容等等之类的。使用前面定义的title_format
,date_format
等之类的标准化格式,获取爬取网页中的各个元素。 -
函数最后需要将得到的网页元素插入(insert)到
fetches
表中。实现insert语句的sql代码fetchAddSql
和需要插入的网页元素值fetchAddSql_Params
,调用mysql中的query函数,对表进行插入操作。
function newsGet(myURL) { //读取新闻页面
request(myURL, function(err, res, body) { //读取新闻页面
//try {
console.log(myURL)
if (body == undefined) return;
var html_news = myIconv.decode(body, myEncoding); //用iconv转换编码
// console.log(html_news);
//准备用cheerio解析html_news
var $ = myCheerio.load(html_news, { decodeEntities: true });
myhtml = html_news;
//} catch (e) { console.log('读新闻页面并转码出错:' + e);};
console.log("转码读取成功:" + myURL);
//动态执行format字符串,构建json对象准备写入文件或数据库
var fetch = {}; //构造一个新的fetch用于存储数据
fetch.title = "";
fetch.content = "";
fetch.publish_date = (new Date()).toFormat("YYYY-MM-DD");
//fetch.html = myhtml;
fetch.url = myURL;
fetch.source_name = source_name;
fetch.source_encoding = myEncoding; //编码
fetch.crawltime = new Date();
if (keywords_format == "") fetch.keywords = source_name; // eval(keywords_format); //没有关键词就用sourcename
else fetch.keywords = eval(keywords_format);
if (title_format == "") fetch.title = ""
else fetch.title = eval(title_format); //标题
if (date_format != "") fetch.publish_date = eval(date_format); //刊登日期
console.log('date: ' + fetch.publish_date);
console.log(myURL);
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('日', '')
fetch.publish_date = new Date(fetch.publish_date).toFormat("YYYY-MM-DD");
if (author_format == "") fetch.author = source_name; //eval(author_format); //作者
else fetch.author = eval(author_format);
if (content_format == "") fetch.content = "";
else fetch.content = eval(content_format).replace("\r\n" + fetch.author, ""); //内容,是否要去掉作者信息自行决定
if (source_format == "") fetch.source = fetch.source_name;
else fetch.source = eval(source_format).replace("\r\n", ""); //来源
if (desc_format == "") fetch.desc = fetch.title;
else fetch.desc = eval(desc_format).replace("\r\n", ""); //摘要
// var filename = source_name + "_" + (new Date()).toFormat("YYYY-MM-DD") +
// "_" + myURL.substr(myURL.lastIndexOf('/') + 1) + ".json";
// 存储json
// fs.writeFileSync(filename, JSON.stringify(fetch));
//sql插入语句
var fetchAddSql = 'INSERT INTO fetches(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.toFormat("YYYY-MM-DD HH24:MI:SS"), fetch.content];
//执行sql,数据库中fetch表里的url属性是unique,不会在数据库中写入重复的url
mysql.query(fetchAddSql, fetchAddSql_Params, function(qerr, vals, fields){
if (qerr) {
console.log(qerr);
}
}); //写入mysql
});
- 任意一篇新闻爬取结果如下:
{
"title": "河海大学图书借阅排行榜第一的书居然是?|【世界读书日】",
"content": "\n\t\t\t\t 原标题:河海大学图书借阅排行榜第一的书居然是?|【世界读书日】 来源:河海大学 世 界 读 书 日",
"publish_date": "2021-04-23",
"url": "https://news.sina.com.cn/c/2021-04-23/doc-ikmyaawc1423692.shtml",
"source_name": "新浪新闻",
"source_encoding": "utf-8",
"crawltime": "2021-04-23T11:56:04.698Z",
"keywords": "河海大学图书借阅排行榜第一的书居然是?|【世界读书日】",
"author": "",
"source": "河海大学",
"desc": "原标题:河海大学图书借阅排行榜第一的书居然是?|【世界读书日】来源:河海大学"
}
- 存储到数据库crawlnews中结果如下:
4. 使用Express框架搭建web服务器实现新闻查询和时间热度分析
4.1 安装express
Express是一个简单且灵活的node.js Web应用框架,可以快速搭建一个完整功能的网站。
- 可以设置中间件响应HTTP请求
- 定义路由表执行不同的HTTP请求
- 通过向模板传递参数来动态渲染HTML页面
使用Express脚手架初始化一个规范化项目。
虽然当项目的路由过多时会使app.js文件很臃肿,但是当前项目没有很多路由,因此使用express脚手架是合理的。
- 使用express脚手架创建一个网站框架
express -e search_site
- 在该网页中需要查询mysql中fetches中的数据,因此需要安装
mysql
npm install mysql -save
- 安装其他需要的模块
npm install
- 在cmd中输入
node bin/www
查看网页127.0.0.1:3000
已搭建成功。
4.2 新闻查询
- 在search.html网页中实现可以针对标题,内容,关键词进行检索。相应的SQL查询语句为:
SELECT * FROM crawlnews
WHERE title like con1 AND keywords like con2 AND content like con3;
- 在search.html中有一个可以输入标题,内容和关键字的表单,用户可以输入想要查询的内容。
- 当后端收到html页面发送的GET请求时,对请求进行解析,使用
request.query
分别得到标题,内容和关键字的查询内容,然后构造SQL语句,对数据库进行查询。将查询结果作为response返回给网页。 - 其中的
publish_date.toLocaleDateString()
语句是将标准时间的publish_date转变为年月日的格式,更加符合观看者的习惯。
2020-01-13T16:00:00.000Z -> 2020/1/14
- 然后在前端中对response进行解析,在
id=record2
的地方构造一个table,显示查询后符合的新闻。
(虽然有实现分页代码,但因为不能正常显示新闻在网页上,等待之后完善)
4.3 时间热度分析
- 在wordhot.html中实现对于一个查询词显示每一天在新闻中出现的次数。相应的SQL查询语句为:
SELECT publish_date,count(*) AS num
FROM fetches WHERE title like con1 GROUP BY publish_date ORDER BY publish_date desc;
需要对publish_date进行分组,计算每个组中新闻的条数。
还可以使用ORDER BY publish_date desc对新闻日期进行降序排序。
- 首先在wordhot.html中构建一个输入标题的表单。使得用户可以输入想要查询的标题内容。
- 然后在后端中获取该request,使用
request.query.title
解析出标题,针对这个标题,利用前面写的SQL语法进行查询,并且将查询值返回。
- 最初,我使用表格显示查询到的数据。在表格中显示日期和总计新闻数。该表格构造的js代码和前一个search.html中的代码类似。
显示结果如下:
- 因为表格的显示不够直观,因此改为折线图展示。导入Echart的js文件,定义一个chart并且使用
echarts.init()
初始化一个echart图形。在option中填入需要的title,x值,y值,并且设定图形类型为line
。
5. 网页展示
完成第4步后,又对网页进行了:
- 主页设计:实现了两个功能页面的跳转。
- CSS设计:使得页面更加美观
- 导航栏设计:使得页面的跳转更加方便。
因此最终网页展示如下:
-
主页(localhost:3000):
可以跳转至:
- 关键词查询网页(search.html)
- 时间热度分析网页(wordhot.html)
-
关键词查询网页(search.html):
当输入想要查询的词后,可以以列表的形式显示查询结果:- 仅查询
title
:
- 查询
title
和keywords
:
- 查询
title
,content
和keywords
:
- 仅查询
-
时间热度分析网页(wordhot.html)
输入想要查询的标题,会显示数据库中的新闻中包含该词的数量,并且按照日期绘制折线图。使得时间热度分析更加直观。
总结
- 在本次项目中,理解js代码是如何运行的,该怎么设计一个网站的框架,如何使用代码对网页和服务器进行通信,是比较重要也是我比较薄弱的方面。
- 实践了使用js进行爬虫和构建网页,学习到了前后端如何通过request和response进行交互,怎么在网页中插入图表,对网页设置CSS,完成了作业的要求。
- 但因为对js和html代码仍然不太熟悉,因此没有完成分页功能,希望在之后对代码进行改进和优化。
- 通过本次项目发现,只有真实的完成过任务,才能真正掌握一门语言。希望之后也能主动使用js。