相关文章推荐
有情有义的匕首  ·  HtmlMeta 类 ...·  2 年前    · 
小眼睛的机器猫  ·  nodejs kill process ...·  2 年前    · 

Node.js实现网络新闻爬虫及搜索功能(一)

1 年前 · 来自专栏 Node.js实现网络新闻爬虫及搜索功能

Node.js实现网络新闻爬虫及搜索功能(一)

项目要求

一、爬虫部分

1、完成目标网站的网页分析和爬虫设计。

2、爬取不少于100条数据(每条数据包括7个字段,新闻关键词、新闻标题、新闻日期、新闻作者、新闻来源、新闻摘要、新闻内容),并存储在数据库中。

二、搜索网站部分

1、完成对数据库中爬取新闻内容和标题的搜索功能,搜索结果以表格形式展示在前端页面中。

2、完成对搜索内容的时间热度分析,使用表格展示爬取数据内容中每一天包含搜索内容的条数。

本文是该项目第一部分:爬取网易新闻

一、爬虫部分

我选择了两个典型的综合新闻门户网站进行新闻爬取,分别是网易新闻( news.163.com/ )和新浪新闻( news.sina.com.cn/ )。本文是对网易新闻爬虫的说明。

1. 引入相关包

写新闻爬虫的第一步,需要先引入爬虫需要的相关包。我们爬取新闻网站所需要的相关工具包有四个,分别是request、iconv-lite、cheerio和date-utils包,而存储新闻信息需要mysql包,这五个工具包主要功能如下表所示:

工具包 作用
request 向被爬取的URL发送请求
iconv-lite 字符编码转换
cheerio 解析并提取HTML中指定内容
mysql 连接数据库并执行SQL语句
date-utils 日期时间的格式化

其中前四个包是需要声明之后作为变量调用的,而date-utils是通过Date()对象调用的。

新建 crawler_163.js 文件。其中,引入相关包的代码为:

var crawler_request = require('request');
var crawler_iconv = require('iconv-lite');
var crawler_cheerio = require('cheerio');
require('date-utils');
var crawler_sql = require("mysql");

2. 建立数据库连接

Node.js程序建立数据库连接之前,需要本地配置并创建好数据库。

因为之前的项目安装过mysql,这里就不再赘述mysql本地安装过程,为了配合程序的一致性,修改本地root密码为root,命令为:

alter user 'root'@'localhost' identified by 'root';

数据库只需要存储新闻信息一张表,计划爬取的新闻信息分别为新闻关键词、新闻标题、新闻日期、新闻作者、新闻来源、新闻摘要、新闻内容共七个字段。创建news表使用如下SQL指令创建:

CREATE TABLE `news` (
  `id_news` int(11)  NOT NULL AUTO_INCREMENT,
  `url` varchar(200) DEFAULT NULL,
  `source` varchar(200) DEFAULT NULL,
  `url_encoding` varchar(45) DEFAULT NULL,
  `title` varchar(200) DEFAULT NULL,
  `keywords` varchar(200) DEFAULT NULL,
  `author` varchar(200) DEFAULT NULL,
  `date` date DEFAULT NULL,
  `crawler_time` datetime DEFAULT NULL,
  `content` longtext,
  `summary` longtext,
  PRIMARY KEY (`id_news`),
  UNIQUE KEY `id_news_UNIQUE` (`id_news`),
  UNIQUE KEY `url_UNIQUE` (`url`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

使用创建数据库连接池的方式实现数据库连接和SQL指令执行:

var pool = crawler_sql.createPool({
     host: '127.0.0.1',
     user: 'root',
     password: 'root',
     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;

3. 爬取并解析网页首页

想要爬取页面上的所有新闻,首先需要获取新闻网页首页的所有相应超链接,即新闻URL。而想要获取这些新闻URL,就需要先请求并解析新闻网页首页。

定义request请求操作函数,该函数不仅在请求获取网页首页HTML代码时需要调用,还会在请求获取新闻URL HTML代码时调用。

function request(url, callback) {
    var options = {
        url: url,
        encoding: null,
        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'
        timeout: 10000
    crawler_request(options, callback);

定义网页首页URL以及爬虫主函数:

var crawler_url = 'https://news.163.com/';
function crawler() {
    request(crawler_url, function(err, res, body) {
        // 网页解析
        try {
            // 编码转换
            var url_encoding = 'UTF-8';
            var url_html = crawler_iconv.decode(body, url_encoding);
            //解析网页
            var $ = crawler_cheerio.load(url_html, { decodeEntities: true });
        } catch (e) {
            console.log('页面解码错误:' + e);
        // 判断网页是否存在超链接
        var url_hrefs;
        try {
            url_hrefs = eval("$('a')");
        } catch (e) {
            console.log('页面不存在超链接' + e);
        // 遍历网页中所有超链接
        url_hrefs.each(function(i, e) {
            // 获取新闻
            var news_url = "";
            try {
                var url_href = "";
                url_href = $(e).attr("href");
                if (typeof(url_href) == "undefined") {
                    return true;
                if (url_href.toLowerCase().indexOf('http://') >= 0 || url_href.toLowerCase().indexOf('https://') >= 0) {
                    news_url = url_href;
                } else if (url_href.startsWith('//')) {
                    news_url = 'https:' + url_href;
                } else {
                    news_url = crawler_url.substr(0, crawler_url.lastIndexOf('/') + 1) + url_href;
            } catch (e) {
                console.log('获取新闻页面出错' + e);
            // 检验新闻网页url是否符合url命名格式
            var news_reg = /\/news\/article\/([a-zA-Z0-9]{16}).html/;
            var news_reg_special = /\/news\/article\/([a-zA-Z0-9]{8})0001982T.html/
            // 如:https://www.163.com/news/article/G8HQOAKE0001899O.html
            if (!news_reg.test(news_url) || news_reg_special.test(news_url)) {
                console.log('新闻链接不符合格式!');
                return;
            // 爬取新闻页面
            var news_search_sql = 'select url from news where url=?';
            var news_search = [news_url];
            crawler_sql.query(news_search_sql, news_search, function(qerr, vals, fields) {
                if (vals.length > 0) {
                    console.log('该新闻页面已被爬取!')
                } else {
                    crawler_news_url(news_url);

下面对crawler()函数进行说明:

首先,需要使用工具包iconv-lite对request得到的网页首页HTML代码进行解码解析,通过观察网页HTML源码可以看到该网页编码方式为UTF-8。

编码转换完成之后,使用cheerio工具包对首页进行解析。

// 网页解析
        try {
            // 编码转换
            var url_encoding = 'UTF-8';
            var url_html = crawler_iconv.decode(body, url_encoding);
            //解析网页
            var $ = crawler_cheerio.load(url_html, { decodeEntities: true });
        } catch (e) {
            console.log('页面解码错误:' + e);
注意:为了避免出现特殊异常而导致的爬虫程序终止,我们尽量在可能出现异常的地方使用try方法

其次,使用cheerio工具包的jquery索引方式判断首页是否存在超链接,并获取所有超链接。

// 判断网页是否存在超链接
        var url_hrefs;
        try {
            url_hrefs = eval("$('a')");
        } catch (e) {
            console.log('页面不存在超链接'




    
 + e);

之后,对获取得到的所有超链接进行遍历,获取超链接中的href值,即新闻URL。因为有些超链接中href是简写的,没有https://头或者没有网站主路由(href中以//开头的链接),需要对href值进行一定的字符串处理,从而得到正确的URL格式。

// 获取新闻
            var news_url = "";
            try {
                var url_href = "";
                url_href = $(e).attr("href");
                if (typeof(url_href) == "undefined") {
                    return true;
                if (url_href.toLowerCase().indexOf('http://') >= 0 || url_href.toLowerCase().indexOf('https://') >= 0) {
                    news_url = url_href;
                } else if (url_href.startsWith('//')) {
                    news_url = 'https:' + url_href;
                } else {
                    news_url = crawler_url.substr(0, crawler_url.lastIndexOf('/') + 1) + url_href;
            } catch (e) {
                console.log('获取新闻页面出错' + e);

通过对网易新闻网页的观察发现,网易新闻网页URL格式是以主路由 163.com/ 开头,后面接上 news/article/{16位字母数字ID}.html 的格式组成(如: 163.com/news/article/G8 )。因此定义正则表达式 /\/news\/article\/([a-zA-Z0-9]{16}).html/ 对新闻URL进行测试。

通过对网易新闻网页的观察发现,网易新闻网页URL格式是以主路由 163.com/ 开头,后面接上 news/article/{16位字母数字ID}.html 的格式组成(如: 163.com/news/article/G8 ),并且16位字母数字ID后八位是特定的0001982T,因此定义第二个正则表达式 /\/news\/article\/([a-zA-Z0-9]{8})0001982T.html/ 对谈心社的新闻文章进行筛选。

即:只有满足第一个正则表达式且不满足第二个正则表达式的新闻URL才是我们需要的的新闻URL。

// 检验新闻网页url是否符合url命名格式
            var news_reg = /\/news\/article\/([a-zA-Z0-9]{16}).html/;
            var news_reg_special = /\/news\/article\/([a-zA-Z0-9]{8})0001982T.html/;
            // 如:https://www.163.com/news/article/G8HQOAKE0001899O.html
            if (!news_reg.test(news_url) || news_reg_special.test(news_url)) {
                console.log('新闻链接不符合格式!');
                return;

最后,在数据库中检索新闻URL,若在数据库中存在该新闻URL,则说明该新闻之前已经被爬取过了,不需要再进行爬取。反正则要执行crawler_news_url()函数,对该新闻URL进行爬取。

// 爬取新闻页面
            var news_search_sql = 'select url from news where url=?';
            var news_search = [news_url];
            crawler_sql.query(news_search_sql, news_search, function(qerr, vals, fields) {
                if (vals.length > 0) {
                    console.log('该新闻页面已被爬取!')
                } else {
                    crawler_news_url(news_url);

4. 爬取并解析新闻URL

和爬取新闻网页首页一样,首先要对新闻URL网页HTML代码进行解码解析。

// 网页解析
        try {
            // 编码转换
            var url_encoding = 'UTF-8';
            var url_html = crawler_iconv.decode(body, url_encoding);
            //解析网页
            var $ = crawler_cheerio.load(url_html, { decodeEntities: true });
        } catch (e) {
            console.log('页面解码错误:' + e);

之后,定义需要爬取的新闻信息json,并使用date-utils工具包初始化日期相关变量。

// 定义新闻信息json
        var news = {};
        news.crawler_time = (new Date()).toFormat("YYYY-MM-DD HH:MM:SS.SSSS");
        news.url = news_url;
        news.url_encoding = 'UTF-8';
        news.keywords = '';
        news.title = '';
        news.date = new Date();
        news.author = '';
        news.source = '';
        news.summary = '';
        news.content = '';

下面就是对具体的信息进行获取了,这一步是爬虫过程的关键,需要在新闻URL网页HTML代码中寻找到所需要信息的位置,并使用cheerio工具包的jquery索引工具定位并获取该信息。

获取新闻关键词:观察新闻URL网页HTML代码,发现name为keywords的meta元素content值存储新闻关键词信息。

相关代码为:

// 获取新闻关键词
        try {
            news.keywords = eval("$('meta[name=\"keywords\"]').eq(0).attr(\"content\")");
        } catch (e) {
            console.log('新闻关键词获取错误:' + e);

获取新闻标题:观察新闻URL网页HTML代码,发现title标签存储新闻标题信息。

相关代码为:

// 获取新闻标题
        try {
            news.title = eval("$('title').text()").replace(/[\r\n\s]/g, "");
        } catch (e) {
            console.log('新闻标题获取错误:' + e);

获取新闻发布时间:观察新闻URL网页HTML代码,发现html标签存储新闻发布时间信息,可以使用id索引。

相关代码为:

// 获取新闻时间
        try {
            news.date = eval("$('#ne_wrap').eq(0).attr(\"data-publishtime\")");
        } catch (e) {
            console.log('新闻日期获取错误:' + e);

获取新闻作者:观察新闻URL网页HTML代码,发现class为icon的img标签alt值存储新闻作者信息,可以使用class索引。


但是有一部分新闻URL网页代码中alt值为默认的netease,因此当获取到的新闻作者是netease时执行第二种获取方式,索引class为post_author的div的text值,并进行正则筛选和字符串替换操作获取正确的新闻作者信息。

相关代码为:

// 获取新闻作者
        try {
            news.author = eval("$('.icon').eq(0).attr(\"alt\")");
            if (news.author == 'netease') {
                news.author = eval("$('.post_author').text()").replace(/[\r\n\s]/g, "").replace("本文来源:", "");
                var author_reg = /责任编辑:.+_/;
                news.author  = author_reg.exec(news.author).toString().replace("责任编辑:", "").replace("_", "");
        } catch (e) {
            console.log('新闻作者获取错误:' + e);

获取新闻来源:观察新闻URL网页HTML代码,发现class为post_info的div标签的第一个子节点中存储新闻来源信息,可以使用class索引。

但在爬取的过程中发现,第一个子节点的值有时获取为‘举报’,不是真正的新闻来源,此时需要换一种索引方式,并对获取的字符串进行正则筛选和字符串替换操作,从而得到正确的新闻来源信息。 相关代码为:

// 获取新闻来源
        try {
            news.source = eval("$('.post_info').children(':first').text()").replace(/[\r\n\s]/g, "");
            if (news.source == '举报') {
                news.source = eval("$('.post_info').prop('firstChild').nodeValue").replace(/[\r\n\s]/g, "");
                var source_reg = /.+来源:/;
                var tmp = source_reg.exec(news.source).toString();
                news.source = news.source.replace(tmp, "");
        } catch (e) {
            console.log('新闻来源获取错误:' + e);

获取新闻摘要:观察新闻URL网页HTML代码,发现name为description的meta元素的content值存储新闻摘要信息。

相关代码为:

// 获取新闻摘要
        try {
            news.summary = eval("$('meta[name=\"description\"]').eq(0).attr(\"content\")").replace(/[\r\n\s]/g, "");
        } catch (e) {
            console.log('新闻摘要获取错误:' + e);

获取新闻内容:使用chrome开发者工具定位工具,发现class为post_body的div标签内的text为新闻正文信息。

相关代码为:

// 获取新闻内容
        try {
            news.content = eval("$('.post_body').text()").replace(/[\r\n\s]/g, "");
        } catch (e) {
            console.log('新闻内容获取错误:' + e);

爬取完毕需要爬取的新闻信息json之后,将相关新闻信息存入数据库中,对于新闻正文为空的新闻不予存储:

// 写入数据库
        if (news.content != '') {
            var news_add_sql = 'INSERT INTO news(url, source, url_encoding, title, keywords, author, date, crawler_time, summary, content) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
            var news_add = [news.url, news.source, news.url_encoding,
                news.title, news.keywords, news.author, news.date,
                news.crawler_time, news.summary, news.content
            crawler_sql.query(news_add_sql, news_add, function(qerr, vals, fields) {
                if (qerr) {
                    console.log(qerr);

crawler_news_url()函数整体代码为:

// 爬取新闻链接
function crawler_news_url(news_url) {
    request(news_url, function(err, res, body) {
        // 网页解析
        try {
            // 编码转换
            var url_encoding = 'UTF-8';
            var url_html = crawler_iconv.decode(body, url_encoding);
            //解析网页
            var $ = crawler_cheerio.load(url_html, { decodeEntities: true });
        } catch (e) {
            console.log('页面解码错误:' + e);
        // 定义新闻信息json
        var news = {};
        news.crawler_time = (new Date()).toFormat("YYYY-MM-DD HH:MM:SS.SSSS");
        news.url = news_url;
        news.url_encoding = 'UTF-8';
        news.keywords = '';
        news.title = '';
        news.date = new Date();
        news.author = '';
        news.source = '';
        news.summary = '';
        news.content = '';
        // 获取新闻关键词
        try {
            news.keywords = eval("$('meta[name=\"keywords\"]').eq(0).attr(\"content\")");
        } catch (e) {
            console.log('新闻关键词获取错误:' + e);
        // 获取新闻标题
        try {
            news.title = eval("$('title').text()").replace(/[\r\n\s]/g, "");
        } catch (e) {
            console.log('新闻标题获取错误:' + e);
        // 获取新闻时间
        try {
            news.date = eval("$('#ne_wrap').eq(0).attr(\"data-publishtime\")");
        } catch (e) {
            console.log('新闻日期获取错误:' + e);
        // 获取新闻作者
        try {
            news.author = eval("$('.icon').eq(0).attr(\"alt\")");
            if (news.author == 'netease') {
                news.author = eval("$('.post_author').text()").replace(/[\r\n\s]/g, "").replace("本文来源:", "");
                var author_reg = /责任编辑:.+_/;
                news.author  = author_reg.exec(news.author).toString().replace("责任编辑:", "").replace("_", "");
        } catch (e) {
            console.log('新闻作者获取错误:' + e);
        // 获取新闻来源
        try {
            news.source = eval("$('.post_info').children(':first').text()").replace(/[\r\n\s]/g, "");
            if (news.source == '举报') {
                news.source = eval("$('.post_info').prop('firstChild').nodeValue").replace(/[\r\n\s]/g, "");
                var source_reg = /.+来源:/;
                var tmp = source_reg.exec(news.source).toString();
                news.source = news.source.replace(tmp, "");
        } catch (e) {
            console.log('新闻来源获取错误:' + e);
        // 获取新闻摘要
        try {
            news.summary = eval("$('meta[name=\"description\"]').eq(0).attr(\"content\")").replace(/[\r\n\s]/g, "");
        } catch (e) {
            console.log('新闻摘要获取错误:' + e);
        // 获取新闻内容
        try {
            news.content = eval("$('.post_body').text()").replace(/[\r\n\s]/g, "");
        } catch (e) {
            console.log('新闻内容获取错误:' + e);
        console.log(JSON.stringify(news));
        // 写入数据库
        if (news.content != '') {
            var news_add_sql = 'INSERT INTO news(url, source, url_encoding, title, keywords, author, date, crawler_time, summary, content) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
            var news_add = [news.url, news.source, news.url_encoding,
                news.title, news.keywords, news.author, news.date,
                news.crawler_time, news.summary, news.content
            crawler_sql.query(news_add_sql, news_add, function(qerr, vals, fields) {
                if (qerr) {
                    console.log(qerr);