node.js,网络爬虫
文章目录
前言
一、网站演示
二、网站构造过程
1.爬取数据
2.网站构建
总结
前言
这是我首次接触和使用javascript语言,也是我制作的第一个网站,虽然在制作的过程中也遇到了许多问题。
接下来先演示一下我的网站,然后逐步分析网站制作过程。
一、网站演示
因为本人对军事相关的讯息和新闻较为感兴趣,所以本次项目我做制作的网站是一个军事类新闻搜索网站。
网站主页如下:
网站支持的功能全部可以通过主页直接到达,除了支持根据输入的信息单项查找,还支持多种复合查找,从而增大了网站查找的准确率;对于搜索得到的结果,会采用时间顺序排列,便于使用者第一时间获取到最新的新闻;网站还提供了时间热度搜索,可以绘制出所搜索的词近日在新闻中出现的次数,从而直观地看出这个搜索词的热度变化情况。通过导航栏我们可以看到网站的简介:
接下来演示一下网站的核心搜素功能。
单项搜索:以标题搜索为例,其它的单项搜索都是类似的:
标题搜索的主界面如下,有一个按键可以回到主页。在输入框中输入关键字后点击“查询”即可获得结果。这里我们以“中国”为例,获取所有标题带“中国”的军事新闻,结果如下:
网站会呈现所有标题带“中国”字样的新闻的相关信息,使用者可以直观地获取到自己想要的信息,或是通过原始url去访问新闻页面。
复合搜索:这里以新闻来源+时间复合搜索为例,其它的复合搜索是相似的。
在输入框内输入我们想要的新闻来源和新闻发布时间,就可以搜索到相应的新闻信息。这里我们以“网易军事”和“2022-07-19”为例,结果如下:
时间热度搜索:输入任意的关键字,网站可以立即绘制出一幅时间热度图,显示今日该关键字的出现频率情况,我们以“乌克兰”为例。
二、网站构造过程
1.爬取数据
我们的新闻数据是从网易军事、新浪军事、凤凰军事以及环球军事网上爬取并储存到本地的数据库中的。因此我们有四段爬虫代码,分别负责爬取不同的网站,但这四段代码是相似的,它们的主要不同点在于数据爬取方式不同(因网站格式而异)。我们这里以爬取网易军事的爬虫为例,对爬虫代码进行说明。
先使用命令创建一个数据库crawl,并在这个数据库中创建表fetches用于储存数据,fetches的格式如下:
自定义一个mysql.js文件,它负责向本地mysql库插入或是查询数据。
var mysql = require("mysql");
var pool = mysql.createPool({
host: '127.0.0.1',
user: 'root',
password: 'root',
database: 'crawl',
port: '3306'
});
var query = function(sql, sqlparam, callback) {
pool.getConnection(function(err, conn) {
if (err) {
console.log("数据库连接失败")
callback(err, null, null);
} else {
try{
conn.query(sql, sqlparam, function(qerr, vals, fields) {
conn.release(); //释放连接
callback(qerr, vals, fields); //事件驱动回调
});
}catch(e){console.log("该数据已经获取")}
}
});
};
var query_noparam = function(sql, callback) {
pool.getConnection(function(err, conn) {
if (err) {
console.log("数据库连接失败")
callback(err, null, null);
} else {
conn.query(sql, function(qerr, vals, fields) {
conn.release(); //释放连接
callback(qerr, vals, fields); //事件驱动回调
});
}
});
};
exports.query = query;
exports.query_noparam = query_noparam;
对于爬虫代码,第一步是引入各种库,并定义网站url以及网站相关信息:
///网易军事
var fs = require('fs');
var myRequest = require('request');
var myCheerio = require('cheerio');
var myIconv = require('iconv-lite');
require('date-utils');
var mysql = require('./mysql.js');
var source_name = "网易军事";
var domain = 'https://war.163.com/';
var myEncoding = "utf-8";
var seedURL = 'https://war.163.com/';
第二步是定义数据读取方式,这里我们选择使用Jquery来获取网站数据:
var seedURL_format = "$('a')";
var keywords_format = "$('meta[name="keywords"]').eq(0).attr("content")";
var title_format = "$('.post_title').text()";
var date_format = "$('.post_info').text()";
var author_format = "$('meta[name="author"]').eq(0).attr("content")";
var content_format = "$('.post_body').text()";
var url_reg = //article//;
该部分的读取方式会因网站而异,不同网站储存数据的方式有所不同,需要编写不同的读取方式,以keywords为例,检查网易军事的源代码,获取读取keywords的路径如下:
然后模拟浏览器请求,防止我们被反爬:
//防止网站屏蔽我们的爬虫
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)
};
第四步是读取种子网页,从中获取所有新闻的url:
seedget();
function seedget() {
request(seedURL, function(err, res, body) { //读取种子页面
// try {
//用iconv转换编码
var html = myIconv.decode(body, myEncoding);
//console.log(html);
//准备用cheerio解析html
var $ = myCheerio.load(html, { decodeEntities: true });
// } catch (e) { console.log('读种子页面并转码出错:' + e) };
var seedurl_news;
try {
seedurl_news = eval(seedURL_format);
} catch (e) { console.log('url列表所处的html块识别出错:' + e) };
seedurl_news.each(function(i, e) { //遍历种子页面里所有的a链接
var myURL = "";
try {
//得到具体新闻url
var href = "";
href = $(e).attr("href");
if (href == undefined) return;
if (href.toLowerCase().indexOf('http://') >= 0) myURL = href; //http://开头的
else if (href.startsWith('//')) myURL = 'http:' + href; 开头的
else myURL = href; //其他
} catch (e) { console.log('识别种子页面中的新闻链接出错:' + e) }
if (!url_reg.test(myURL)) return; //检验是否符合新闻url的正则表达式
//console.log(myURL);
var fetch_url_Sql = 'select url from fetches 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); //读取新闻页面
});
});
});
};
最后一步是在刚刚获取到的子url中,获取到每条新闻的相关信息:
function newsGet(myURL) { //读取新闻页面
request(myURL, function(err, res, body) { //读取新闻页面
if (body == undefined) return;
//try {
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对象准备写入文件或数据库
console.log()
var 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(fetch.keywords);
console.log(fetch.title);
if (fetch.publish_date.length != 0){
fetch.publish_date = fetch.publish_date.trim();
fetch.publish_date = fetch.publish_date.substring(0,10);
console.log(fetch.publish_date)
if (author_format == "") fetch.author = source_name; //eval(author_format); //作者
else fetch.author = eval(author_format);
console.log(fetch.author);
if (content_format == "") fetch.content = "";
else fetch.content = eval(content_format).replace("rn" + fetch.author, ""); //内容,是否要去掉作者信息自行决定
console.log(fetch.content);
// var filename = source_name + "_" + (new Date()).toFormat("YYYY-MM-DD") +
// "_" + myURL.substr(myURL.lastIndexOf('/') + 1) + ".json";
// 存储json
// fs.writeFileSync(filename, JSON.stringify(fetch));
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写入
}
});
}
至此,我们已经成功地将网易军事的新闻全部读取并保存到了我们的数据库中。
对于其它的几个网站,我们只需根据每个网站的数据读取路径,稍微改写一下这段代码,便也可以轻松地获取到网站数据
2.网站构建
我们选择使用express脚手架来完成整个项目的搭建。
对于网站框架的主要思路是,先构建一个主页面,上面会有多个按钮,每个按钮通向一个子页面,实现某个具体的功能;再加上一个导航栏,让用户可以通过导航栏获取网站简介。对于简介页面,只需简单的呈现介绍文字即可;而对于其它子页面,我们要让用户可以输入关键字,然后点击搜索按钮从而呈现出对应的结构;另外每个主页面还需要加上一个按钮,以方便用户返回主页面。
所有html文件均保存在express项目的public文件下,对应的图片背景保存在public/images文件下。
我们首先修改bin文件下的www文件,使得项目监听的端口号为8081。
#!/usr/bin/env node
var app = require('../app');
var debug = require('debug')('eps-1:server');
var http = require('http');
var port = normalizePort(process.env.PORT || '8081');
app.set('port', port);
var server = http.createServer(app);
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}
接着我们编辑index.js文件,用来处理和响应各种请求。修改后的代码如下:
var express = require('express');
var router = express.Router();
var mysql = require('../mysql.js');
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
router.get('/searcht', function(request, response) {
//标题搜索
response.writeHead(200, { 'Content-Type': 'application/json' });
var sql_search = "select url,source_name,title,keywords,author,publish_date from fetches where title like '%" +request.query.title + "%' order by publish_date desc";
mysql.query(sql_search, function(err, result, fields) {
response.write(JSON.stringify(result));
response.end();
});
});
router.get('/searchs', function(request, response) {
//来源搜索
response.writeHead(200, { 'Content-Type': 'application/json' });
var sql_search = "select url,source_name,title,keywords,author,publish_date from fetches where source_name like '%" +request.query.source_name + "%' order by publish_date desc";
mysql.query(sql_search, function(err, result, fields) {
response.write(JSON.stringify(result));
response.end();
});
});
router.get('/searchk', function(request, response) {
//关键词搜索
response.writeHead(200, { 'Content-Type': 'application/json' });
var sql_search = "select url,source_name,title,keywords,author,publish_date from fetches where keywords like '%" +request.query.keywords + "%' order by publish_date desc";
mysql.query(sql_search, function(err, result, fields) {
response.write(JSON.stringify(result));
response.end();
});
});
router.get('/searchp', function(request, response) {
//时间搜索
response.writeHead(200, { 'Content-Type': 'application/json' });
var sql_search = "select url,source_name,title,keywords,author from fetches where publish_date like '%" +request.query.publish_date + "%' order by publish_date desc";
mysql.query(sql_search, function(err, result, fields) {
response.write(JSON.stringify(result));
response.end();
});
});
router.get('/searchts', function(request, response) {
//标题+来源复合搜索
response.writeHead(200, { 'Content-Type': 'application/json' });
var sql_search = "select url,source_name,title,keywords,author,publish_date from fetches where title like '%" +request.query.title + "%' and source_name like '%" +request.query.source_name + "%' order by publish_date desc";
mysql.query(sql_search, function(err, result, fields) {
response.write(JSON.stringify(result));
response.end();
});
});
router.get('/searchks', function(request, response) {
//关键词+来源复合搜索
response.writeHead(200, { 'Content-Type': 'application/json' });
var sql_search = "select url,source_name,title,keywords,author,publish_date from fetches where keywords like '%" +request.query.keywords + "%' and source_name like '%" +request.query.source_name + "%' order by publish_date desc";
mysql.query(sql_search, function(err, result, fields) {
response.write(JSON.stringify(result));
response.end();
});
});
router.get('/searchps', function(request, response) {
//时间+来源复合搜索
response.writeHead(200, { 'Content-Type': 'application/json' });
var sql_search = "select url,source_name,title,keywords,author from fetches where publish_date like '%" +request.query.publish_date + "%' and source_name like '%" +request.query.source_name + "%' order by publish_date desc";
mysql.query(sql_search, function(err, result, fields) {
response.write(JSON.stringify(result));
response.end();
});
});
module.exports = router;
准备工作进行完后,我们编写前端代码。首先编写主页面代码。
军事新闻搜索主页
- 首页
- 介绍
欢迎浏览军事新闻搜索网!
单项查找:复合查找:
时间热度图:
主页面包含了导航栏以浏览简介,以及多个按钮,点击它们进入子界面完成查询功能。
介绍页面代码如下:
军事新闻搜索主页
- 首页
- 介绍
简介:
本站包含来自网易军事,新浪军事,凤凰军事,以及环球军事网的最新新闻。可以通过标题搜索、来源搜索、关键词搜索以及新闻时间搜索等多种模式来获取您感兴趣的新闻,获取的新闻会按时间顺序呈现。
同时本站支持复合搜索,可同时使用多个字段进行搜索,更加准确。
提供了每个关键词的热度分析图,可以直观地看出一个热点近期的热度变化情况。
Tips:使用时间搜索时,需按照对应的时间格式输入。
对于单项查找子页面,它们的结构是类似的,这里以标题单项查找为例:
标题搜索
点击按钮以触发查询操作,并将查询的结果以表格的形式整齐地呈现在网页上。
对于复合查找,我们这里以标题+来源复合查找为例子,演示源代码:
标题+来源复合搜索
输入两个关键词,进而使得查询的sql语句变成双重条件查询,获取符合两个要求的数据。其他复合查找的网页结构也是类似的。
时间热度搜索网页源代码如下:
时间热度搜索
在数据库中获取数据后,对数据进行简单处理,然后以图表形式呈现在网页上
至此,我们完成了网站的搭建。
总结
我们的网站除了满足了期末项目的全部要求外,还额外实现了对查询结果的分项排序(按发布时间排序),进而方便用户可以第一时间找到最新的新闻;也实现了对多个查询条件进行复合搜索;并将各个页面分离开,使得每个页面的功能单一化,便于维护和处理(这为我的开发过程节约了不少功夫)。由于我本次选择的新闻是军事方面的垂直领域新闻,所以新闻条目并不多,故没有加入分页系统。但如果新闻条目很大,每次搜索都可以搜索到较大数量的新闻时,引入分页功能就会十分必要。通过这个项目,我第一次实际上手使用js做项目,也是首次将数据库的使用带入项目中来,更是第一次接触到前端开发,学习前端开发的思维模式,收获颇丰。由于是第一次做网页,在开发过程中也遇到了许许多多的问题,但好在最后都得到了解决,做出了这个简陋但令我比较满意的网页(妈妈怎么会嫌弃自家儿子丑呢)。
希望几年后的我,再回头看这篇文章的时候,我的编码水平可以让我感叹道:“这写的什么xx玩意?”



