零web编程基础开发一个nodejs爬虫
目标:
爬取目标网站所有新闻并存入本地文件/mysql,搭建简单网页完成对爬取结果的搜索与展示分析。
目标网站:
新浪新闻:https://news.sina.com.cn/
网易新闻:https://news.163.com/
搜狐新闻:http://news.sohu.com/
(本篇采用图文结合,对于其中的代码作用,部分直接说明,部分在代码注释中说明)
①分析网站首页结构
找到新闻部分对应url
分析编写锁定新闻url的正则表达式
var url_reg = /\/(\d{4})\-(\d{2})\-(\d{2})\/doc\-(\w{8})(\d{7})\.shtml/;
②分析新闻页面内容
找出所需title、author、keywords等信息,记录其class、id,准备相应代码
备注:新浪新闻部分新闻页面没有提供作者信息,或者为无题、关键字页面,之后进行录入时若网页未提供,则相关栏目留空白。
var source_name = "新浪";//来源
var myencoding = "utf-8";//解码
var seedurl = 'https://news.sina.com.cn/';//主页面
var seedurl_format = "$('a')";//寻找子页面
var keywords_format = " $('meta[name=\"keywords\"]').eq(0).attr(\"content\")";//关键词
var title_format = "$('.main-title').text()";//标题
var date_format = "$('.date').text()";//日期
var author_format = "$('.show_author').text()";//编辑
var content_format = "$('.article').text()";//文章
var des_format = " $('meta[name=\"description\"]').eq(0).attr(\"content\")";//摘要
③引入爬取所需的nodejs包
var fs = require('fs');//文件储存用
var myrequest = require('request');//访问页面用
var mycheerio = require('cheerio');//解析html用
var myiconv = require('iconv-lite');//编码用
var mysql = require('./mysql.js');//mysql存储用
require('date-utils');//日期工具类,用于格式化日期
④伪装访问(此处使用win环境的safari)
var headers =
{
'User-Agent': 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50'
}
⑤构建request回调函数
function request(url, callback) {
var options = {
url: url,
encoding: null,
headers: headers,
timeout: 10000
}
myrequest(options, callback)
}
⑥构建针对主页面(seedurl)的函数,用以进入具体的各个新闻页面
request(seedurl, function(err, res, body) //访问并且读取种子页面
{
//用iconv转换编码
var html = myiconv.decode(body, myencoding);
//console.log(html);
//准备用cheerio解析html
var $ = mycheerio.load(html, { decodeEntities: true });
var seedurl_news;
try {
seedurl_news = eval(seedurl_format);
} catch (e) { console.log('url中html识别error:' + e) };
seedurl_news.each(function(i, e) { //遍历种子页面里所有的a链接,获取所有新闻url
var myurl = "";
try {
//获取新闻url,可在此处设计循环计数以限制爬取上限数量
var href = "";
href = $(e).attr("href");
if (typeof(href) == "undefined") { // 部分新闻地址为undefined
return true;
}
if (href.toLowerCase().indexOf('http://') >= 0 || href.toLowerCase().indexOf('https://') >= 0) myurl = href; //针对http://或者https://开头
else if (href.startsWith('//')) myurl = 'http:' + href; //针对//开头
else myurl = seedurl.substr(0, seedurl.lastIndexOf('/') + 1) + href; //针对其他
} catch (e) { console.log('seed中新闻链接error:' + e) }
if (!url_reg.test(myurl)) return; //检验符合该网站新闻url的正则表达式
getnews(myurl); //进入新闻页面
});
});
⑦编写获取新闻页面中所需信息的函数
function getnews(myurl) {
request(myurl, function(err, res, body) {//读取新闻页面
var html_news = myiconv.decode(body, myencoding); //用iconv编码
var $ = mycheerio.load(html_news, { decodeEntities: true }); //用cheerio解析html_news
myhtml = html_news;
console.log("转码读取成功:" + myurl);
//format字符串,开始写入
var fetch = {};
fetch.publish_date = (new Date()).toFormat("YYYY-MM-DD");
fetch.content = "";
fetch.title = "";
fetch.source_name = source_name;
fetch.url = myurl;
fetch.source_encoding = myencoding; //编码
fetch.crawltime = new Date();
if (keywords_format == "") fetch.keywords = source_name;//没有关键词就用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); //publish日期
console.log('date: ' + fetch.publish_date);//发布日期
console.log(myurl);//爬取对象
if (author_format == "") fetch.author = source_name; //作者
else fetch.author = eval(author_format);
if (content_format == "") fetch.content = "";
else fetch.content = eval(content_format); //内容
fetch.content = fetch.content.replace(/[\r\n]/g,"");
//去除回车换行
if (des_format == "") fetch.des = fetch.title;
else fetch.des = eval(des_format); //摘要
});
}
⑧在getnews函数中补充储存爬取信息的方式,此处分别采用生成文件和mysql存储
生成json文件:
var filename = source_name + "_" + (new Date()).toFormat("YYYY-MM-DD") +
"_" + myurl.substr(myurl.lastIndexOf('/') + 1) + ".json";
//存储json
fs.writeFileSync(filename, JSON.stringify(fetch));//将fetch转化为字符串录入
mysql存储:
var fetchadd = 'INSERT INTO fetches(source_name,url,source_encoding,title,' +
'author,keywords,publish_date,crawltime,description,content) VALUES(?,?,?,?,?,?,?,?,?,?)';
var fetchadd_params =
[
fetch.source_name,fetch.url,fetch.source_encoding,
fetch.title, fetch.author, fetch.keywords, fetch.publish_date,
fetch.crawltime.toFormat("YYYY-MM-DD HH24:MI:SS"), fetch.des,fetch.content
];
//执行sql,库中fetch里的url属性为unique,防止重复
mysql.query(fetchadd, fetchadd_params, function(qerr, vals, fields)
{
if (qerr)
{
console.log(qerr);
}
}); //写入mysql
附上对应使用的mysql包原文件(根据本地环境设置登录参数)
var mysql = require("mysql");
var pool = mysql.createPool({
host: '127.0.0.1',
user: 'root',
password: 'hdx',
database: 'crawl'
});
var query = function(sql, sqlparam, callback)
{
pool.getConnection(function(err, conn) {
if (err) {
callback(err, null, null);
} else {
conn.query(sql, sqlparam, function(qerr, vals, fields) {
conn.release(); //释放连接
callback(qerr, vals, fields); //事件驱动回调
});
}
});
};
var query_noparam = function(sql, callback)
{
pool.getConnection(function(err, conn) {
if (err) {
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;
使用mysql建立一个简单的表格,其中database名字为crawl,含有一个名为fetches的表,表中各个表头类型如下所列出(部分规定了内容类型与长度)。
CREATE TABLE `fetches` (
`id_fetches` int(11) NOT NULL AUTO_INCREMENT,
`source_name` varchar(300) DEFAULT NULL,
`url` varchar(300) DEFAULT NULL,
`source_encoding` varchar(50) DEFAULT NULL,
`title` varchar(300) DEFAULT NULL,
`author` varchar(300) DEFAULT NULL,
`keywords` varchar(300) DEFAULT NULL,
`publish_date` longtext,
`crawltime` datetime DEFAULT NULL,
`description` longtext,
`content` longtext,
`createtime` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id_fetches`),
UNIQUE KEY `id_fetches_UNIQUE` (`id_fetches`),
UNIQUE KEY `url_UNIQUE` (`url`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
(注意与爬虫程序中的信息类型对应,longtext等属性设置正确,约束正确)
⑨试运行
存为文件成功:
存于mysql(用select相关命令查询对应项)(后续的网页查询原理在此处):
代码:
var source_name = "新浪";//来源
var myencoding = "utf-8";//解码
var seedurl = 'https://news.sina.com.cn/';//主页面
var seedurl_format = "$('a')";//寻找子页面
var keywords_format = " $('meta[name=\"keywords\"]').eq(0).attr(\"content\")";//关键词
var title_format = "$('.main-title').text()";//标题
var date_format = "$('.date').text()";//日期
var author_format = "$('.show_author').text()";//编辑
var content_format = "$('.article').text()";//文章
var des_format = " $('meta[name=\"description\"]').eq(0).attr(\"content\")";//摘要
var url_reg = /\/(\d{4})\-(\d{2})\-(\d{2})\/doc\-(\w{8})(\d{7})\.shtml/;//用于匹配主页面中的新闻子页面的正则表达式
var fs = require('fs');//文件
var myrequest = require('request');//访问
var mycheerio = require('cheerio');//解析html
var myiconv = require('iconv-lite');//编码用
var mysql = require('./mysql.js');//mysql存储用
require('date-utils');//日期工具类,用于格式化日期
//伪装为windows的safari5.1进行访问
var headers =
{
'User-Agent': 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50'
}
//利用request模块回调函数
function request(url, callback) {
var options = {
url: url,
encoding: null,
headers: headers,
timeout: 10000
}
myrequest(options, callback)
}
request(seedurl, function(err, res, body) //访问并且读取种子页面
{
//用iconv转换编码
var html = myiconv.decode(body, myencoding);
//console.log(html);
//准备用cheerio解析html
var $ = mycheerio.load(html, { decodeEntities: true });
var seedurl_news;
try {
seedurl_news = eval(seedurl_format);
} catch (e) { console.log('url中html识别error:' + e) };
seedurl_news.each(function(i, e) { //遍历种子页面里所有的a链接
var myurl = "";
try {
//获取新闻url
var href = "";
href = $(e).attr("href");
if (typeof(href) == "undefined") { // 部分新闻地址为undefined
return true;
}
if (href.toLowerCase().indexOf('http://') >= 0 || href.toLowerCase().indexOf('https://') >= 0) myurl = href; //针对http://或者https://开头
else if (href.startsWith('//')) myurl = 'http:' + href; //针对//开头
else myurl = seedurl.substr(0, seedurl.lastIndexOf('/') + 1) + href; //针对其他
} catch (e) { console.log('seed中新闻链接error:' + e) }
if (!url_reg.test(myurl)) return; //检验符合该网站新闻url的正则表达式
getnews(myurl); //进入新闻页面
});
});
function getnews(myurl) {
request(myurl, function(err, res, body) {//读取新闻页面
var html_news = myiconv.decode(body, myencoding); //用iconv编码
var $ = mycheerio.load(html_news, { decodeEntities: true });//用cheerio解析html_news
myhtml = html_news;
console.log("转码读取成功:" + myurl);
//format字符串,开始写入
var fetch = {};
fetch.publish_date = (new Date()).toFormat("YYYY-MM-DD");
fetch.content = "";
fetch.title = "";
fetch.source_name = source_name;
fetch.url = myurl;
fetch.source_encoding = myencoding; //编码
fetch.crawltime = new Date();
if (keywords_format == "") fetch.keywords = source_name;//没有关键词就用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); //publish日期
console.log('date: ' + fetch.publish_date);//发布日期
console.log(myurl);//爬取对象
if (author_format == "") fetch.author = source_name; //作者
else fetch.author = eval(author_format);
if (content_format == "") fetch.content = "";
else fetch.content = eval(content_format); //内容
fetch.content = fetch.content.replace(/[\r\n]/g,"");//去除回车换行
if (des_format == "") fetch.des = fetch.title;
else fetch.des = eval(des_format); //摘要
var filename = source_name + "_" + (new Date()).toFormat("YYYY-MM-DD") +
"_" + myurl.substr(myurl.lastIndexOf('/') + 1) + ".json";
//存储json
fs.writeFileSync(filename, JSON.stringify(fetch));//将fetch转化为字符串录入
/*
var fetchadd = 'INSERT INTO fetches(source_name,url,source_encoding,title,' +
'author,keywords,publish_date,crawltime,description,content) VALUES(?,?,?,?,?,?,?,?,?,?)';
var fetchadd_params =
[
fetch.source_name,fetch.url,fetch.source_encoding,
fetch.title, fetch.author, fetch.keywords, fetch.publish_date,
fetch.crawltime.toFormat("YYYY-MM-DD HH24:MI:SS"), fetch.des,fetch.content
];
//执行sql,库中fetch里的url属性为unique,防止重复
mysql.query(fetchadd, fetchadd_params, function(qerr, vals, fields)
{
if (qerr)
{
console.log(qerr);
}
}); //写入mysql
*/
});
}
⑩建立网页对mysql进行搜索:
为了网页清晰美观,此处选用express框架。
在cmd中用express -e search_site建立项目文件夹,(-e为使用ejs),并将前之前使用的mysql.js文件拷入备用
之后在项目文件夹中安装需要的包(mysql等)
npm install --save;npm install;
再之后对routes/index.js进行编辑
var express = require('express');
var router = express.Router();
var mysql = require('../mysql.js');
/* GET 主页 */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
router.get('/process_get', function(request, response) {
//sql
var fetchSql = "select url,source_name,title,author,publish_date " +
"from fetches where author like '%" + request.query.author + "%'";
var tarray=request.query.title.split(" ");
for(var i=0;i<tarray.length;i++){
fetchSql+="and title like '%"+ tarray[i] + "%'";
}
fetchSql+="order by publish_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;
其中,fetchsql为对mysql操作的核心,此处作出解释:
第一,本次搜索参数有author和title两个,其中author为人名,无须复合查询,因而直接使用request.query.author输入即可;
var fetchSql = "select url,source_name,title,author,publish_date " +
"from fetches where author like '%" + request.query.author + "%'";
第二,由于title较长,为了便于复合要素的查询(例如对“A B”的查询,空格作为分割符号),采用.split(“ ”)按空格分割获得数组(如“A,B”),再利用“and title like”语句循环导入,即可成功完成符合搜索(用like与通配符完成模糊查询)。
var tarray=request.query.title.split(" ");
for(var i=0;i<tarray.length;i++){
fetchSql+="and title like '%"+ tarray[i] + "%'";
}
第三,为了方便分析新闻时间热度情况,先按时间序排放搜索结果便于查看
fetchSql+="order by publish_date"
最后,使用fetchSql完成搜索即可
mysql.query(fetchSql, function(err, result, fields) {
response.writeHead(200, {
"Content-Type": "application/json"
});
response.write(JSON.stringify(result));
response.end(); ``
});
紧接着,完成html文件前后端的构建:
<!DOCTYPE html>
<html>
<header>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
</header>
<head>
<style>
.cardLayout{
border:rgb(147, 64, 163) solid 10px;
margin: 5px 0px;
}
tr{
border: solid 3px;
}
td{
border:solid 3px;
}
</style>
</head>
<body>
<form>
<br> 标题:<input type="text" id="input1" name="title_text">
<br> 作者:<input type="text" id="input2" name="title_text">
<input class="form-submit" type="button" id="btn1"value="查询">
<input type="reset">
</form>
<div class="cardLayout">
<table width="100%" id="record2"></table>
</div>
<script>
$(document).ready(function() {
$("#btn1").click(function() {
$.get('/process_get?title=' + $("#input1").val()+'&author='+$("#input2").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>
</body>
</html>
css格式按照喜好设置即可(此处主要满足显示清楚)
<style>
.cardLayout{
border:rgb(147, 64, 163) solid 10px;
margin: 5px 0px;
}
tr{
border: solid 3px;
}
td{
border:solid 3px;
}
</style>
搜索展示:
<script>
$(document).ready(function() {
$("#btn1").click(function() {
$.get('/process_get?title=' + $("#input1").val()+'&author='+$("#input2").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>
其中获得form中信息传入get进行搜索(注意与input的id各自对应)
$("#btn1").click(function() {
$.get('/process_get?title=' + $("#input1").val()+'&author='+$("#input2").val(), function(data)
紧接着按格式输出表格即可(表格标签style通过css调整)
$("#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>');
}
});
});
});
构建完成后将html文件放入项目文件夹
之后在cmd启动express
浏览器访问html
测试各种功能是否正常:
单独搜索标题
复合搜索标题
标题+作者
发布时间从早到晚顺序排布
最后,为了美化展示,引入bootstrap中css表单进行结果分页展示,对html文件进行微调:
先在原先基础上引入bootstrap包,原css格式不变
<head>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
<link href="http://www.itxst.com/package/bootstrap-table-1.14.1/bootstrap-4.3.1/css/bootstrap.css" rel="stylesheet" />
<link href="http://www.itxst.com/package/bootstrap-table-1.14.1/bootstrap-table-1.14.1/bootstrap-table.css" rel="stylesheet" />
<script src="http://www.itxst.com/package/bootstrap-table-1.14.1/bootstrap-table-1.14.1/bootstrap-table.js"></script>
<style>
.cardLayout{
border:rgb(147, 64, 163) solid 10px;
margin: 5px 0px;
}
tr{
border: solid 3px;
}
td{
border:solid 3px;
}
</style>
</head>
紧接着修改get函数中展示表单的部分(使用bootstrap方法)
<script>
$(document).ready(function() {
$("#btn1").click(function() {
/*$.get('/process_get?title=' + $("#input1").val()+'&author='+$("#input2").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>');
}
});*/
$.get('/process_get?title=' + $("#input1").val()+'&author='+$("#input2").val(), function(data) {
$("#record2").bootstrapTable({
search:true, //加上搜索控件
method: 'get', //请求方式
pagination: true, //是否显示分页
striped: true, //是否显示行间隔色
uniqueId: "userId", //每一行的唯一标识,一般为主键列
pageSize: 5, //每页的记录行数
sidePagination : 'client',
columns:[{
field:'url', //对应数据库字段名
title:'链接',
},{
field:'source_name',
title:'来源'
},{
field:'title',
title:'标题'
},{
field:'author',
title:'作者'
},{
field:'keywords',
title:'关键词'
},{
field:'publish_date',
title:'发布时间',
}],
data: data,
});
});
});
});
</script>
(bootstrap能简单表答进一步搜索,详细分页,时间热度分析等功能)
search:true, //加上搜索控件,注意此处可进一步完善时间热度分析
method: 'get', //请求方式
pagination: true, //是否显示分页
striped: true, //是否显示行间隔色
uniqueId: "userId", //每一行的唯一标识,一般为主键列
pageSize: 5, //每页的记录行数
尝试效果:
测试在搜索结果中进行search
分页成功,并且可以显示满足条件的搜索结果一共有多少条,也可以通过此处在进一步搜索中得到特定时间某个话题的新闻量(此处67rows即表示满足搜索条件的新闻一共有67条,结合日期信息可完成进一步时间热度分析)(成功加强了时间热度分析):
前后两种搜索页面均保存备用
代码通用性检测,对网易新闻进行爬取
查看网页结构,与前者风格类似
修改爬虫中关于页面结构中子页面、class、id等内容的检索规则(同根据新闻页结构)
var source_name = "网易";//来源
var myencoding = "utf-8";//解码
var seedurl = 'https://news.163.com/';//主页面
var seedurl_format = "$('a')";//寻找子页面
var keywords_format = "$('meta[name=\"keywords\"]').eq(0).attr(\"content\")";//关键词
var title_format = "$('title').text()";//标题
var date_format = "$('meta[property=\"article:published_time\"]').eq(0).attr(\"content\")";//日期
var author_format = "$('.post_author').text()";//编辑
var content_format = "$('.article').text()";//文章
var des_format = " $('meta[name=\"description\"]').eq(0).attr(\"content\")";//摘要
var url_reg = /\/news\/article\//;//用于匹配主页面中的新闻子页面的正则表达式
存入与之前相同的一个数据库进行搜索
(成功检索到新浪网易二者的新闻)
代码通用性检测,对搜狐新闻进行爬取
同样查看网页结构
修改爬虫中关于页面结构中子页面、class、id等内容的检索规则(同根据新闻页结构)
var source_name = "搜狐";//来源
var myencoding = "utf-8";//解码
var seedurl = 'http://news.sohu.com/';//主页面
var seedurl_format = "$('a')";//寻找子页面
var keywords_format = "$('meta[name=\"keywords\"]').eq(0).attr(\"content\")";//关键词
var title_format = "$('title').text()";//标题
var date_format = "$('meta[property=\"og:release_date\"]').eq(0).attr(\"content\")";//日期
var author_format = "$('meta[name=\"mediaid\"]').eq(0).attr(\"content\")";//编辑
var content_format = "$('article').text()";//文章
var des_format = " $('meta[name=\"description\"]').eq(0).attr(\"content\")";//摘要
var url_reg = /\/a\/(\d{9})\_/;//用于匹配主页面中的新闻子页面的正则表达式
存入与之前相同的一个数据库进行搜索
均成功实现。
总结:
本次开发耗时约半个学期,最终在老师的引导和前人提供的各类教程帮助下成功完成了项目。其中最大的收获莫过于积累了从零学习/开发web项目的经验,提高了查阅、应用相关资料的技能。虽然过程中充满了不成熟,但好歹迈出了第一步,今后要继续加油~