这是计划的第1~2步
对比了各要求爬取的网站之后,先选择HTML结构简单的的雪球网进行尝试。
1)分析HTML结构
F12打开Chrome的控制台,可以看见其HTML源码;
其结构比较简单:首先,观察到每则新闻都在各自的class=AnonymousHome_home__timeline__item_3vU下,各种信息都以文本方式存储在结构中。
2)一级网址信息爬取
先试试能否爬取标题,
console.log($('.AnonymousHome_home__timeline__item_3vU','div').find('h3').children('a').text());
观察到一下结构,于是利用.find(‘h3’).children(‘a’).text()选出其中的标题并找出纯文本内容,并将这一行插入示例源码中。
很明显所有标题都抓取成功,接下来整理标题
var html = body;
var titles = []
var $ = myCheerio.load(html, { decodeEntities: false });
$('.AnonymousHome_home__timeline__item_3vU', 'div').find('h3').children('a').each(function (idx, element){
titles.push({
title:$(element).text()
})
})
console.log(titles);
利用each()对每个标题做处理,利用一个匿名函数吧每个标题作为一个对象存入数组titles中(利用element变量传递函数外的查找路径)
接下来为了便于整理数据,将每篇文章的标题都作为一篇文章对象的属性
var html = body;
var newsArr = [];
var $ = myCheerio.load(html, { decodeEntities: false });
var item = $('.AnonymousHome_home__timeline__item_3vU', 'div')
item.map(function (idx, element) {
var news = {};
news.title = $(element).find('h3').children('a').text();
newsArr.push(news);
})
console.log(newsArr);
将每一个标题存储为一个news中的title属性,并且存入数组newsArr中;
接下来,设计爬取其他信息的方式:
news.link = "https://xueqiu.com" + $(element).find('h3').children('a').attr('href');
//链接
news.editor = $(element).find('div').children('a').text();
//编辑
temp1 = $(element).find('.AnonymousHome_category_5zp').text().split(' ');
news.from = temp1[temp1.length - 1];
//来源
temp2 = $(element).find('div').children('span').text();
news.time = temp2.substr(temp2.length - 11, temp2.length);
//发布时间
news.read = $(element).find('.AnonymousHome_read_2t5').text();
//阅读量
news.contain = $(element).find('p').text();
//摘要
基本是使用相同的方式,对于部分数据做了简单字符串操作,使其美观
每项信息都可以正确的爬取到,接下来为了爬取每则新闻的正文内容,对获得的二级网址再进行爬取
3)二级网址信息爬取
首先同样分析HTML结构,非常之简洁
//先单独写一个获取信息的脚本
var myRequest = require('request')
var myCheerio = require('cheerio')
var myURL = 'https://xueqiu.com/1333325987/142925519'
function request(url, callback) {//request module fetching url
var options = {
url: url, encoding: null, heagers: null
}
myRequest(options, callback)
}
request(myURL, function (err, res, body) {
var html = body;
var $ = myCheerio.load(html, { decodeEntities: false });
var source = $('.article__bd__from', 'div').children('a').text();
var texts = [];
$('.article__bd__detail', 'div').children('p').each(function (idx, element){
texts.push({
texts:$(element).text()
})
})
console.log(texts);
})
可以看见正文内容可以正常地爬取到,接下来用简单的字符串操作删去不需要的首行和空文本
texts.shift();
for(var i=0,n=0;i<texts.length;i++){
if(texts[i].texts.length == 0) {
texts.splice(i, 1);
}
}
于是,接下来尝试利用函数将爬取二级网址的功能植入源码中
var myRequest = require('request')
var myCheerio = require('cheerio')
var Promise = require('bluebird');
var myURL = 'https://xueqiu.com'
function request(url, callback) {//request module fetching url
var options = {
url: url, encoding: null, heagers: null
}
myRequest(options, callback)
}
var newsArr = [];
function getTexts(url) {
request(url, function (err, res, body) {
var html = body;
var $ = myCheerio.load(html, { decodeEntities: false });
var source = $('.article__bd__from', 'div').children('a').text();
var texts = [];
$('.article__bd__detail', 'div').children('p').each(function (idx, element) {
texts.push({
texts: $(element).text()
})
})
texts.shift();
for (var i = 0, n = 0; i < texts.length; i++) {
if (texts[i].texts.length == 0) {
texts.splice(i, 1);
}
}
console.log(source);
return source;
})
}
request(myURL, function (err, res, body) {
var html = body;
var $ = myCheerio.load(html, { decodeEntities: false });
var item = $('.AnonymousHome_home__timeline__item_3vU', 'div')
item.map(function (idx, element) {
var news = {};
news.title = $(element).find('h3').children('a').text();
news.link = "https://xueqiu.com" + $(element).find('h3').children('a').attr('href');
news.editor = $(element).find('div').children('a').text();
news.source = '';
temp1 = $(element).find('.AnonymousHome_category_5zp').text().split(' ');
news.category = temp1[temp1.length - 1];
temp2 = $(element).find('div').children('span').text();
news.time = temp2.substr(temp2.length - 11, temp2.length);
news.read = $(element).find('.AnonymousHome_read_2t5').text();
news.contain = $(element).find('p').text();
news.texts = '';
news.source = getTexts(news.link);
newsArr.push(news);
})
console.log(newsArr);
})
然而,二级网址的信息并没有爬取到;
查阅了资料,这是因为JavaScript具有异步行为,在执行到 news.source = getTexts(news.link); 的时候,程序并不等待二级网址的爬取,而是先执行 newsArr.push(news); 将news存入newsArr中,因此 source 依然是 undefined。
接下来对解决这个问题做了尝试,先使用了request-promise代替request,并将 newsArr.push(news); 作为 getText() 的回调函数,然而依然无法解决问题
var cheerio = require('cheerio')
var rp = require('request-promise')
var options = {
uri: 'https://xueqiu.com',
transform: function (body) {
return cheerio.load(body);
}
}
function getTexts(newsArr, news, callback) {
var options = {
uri: news.link,
transform: function (body) {
return cheerio.load(body);
}
}
rp(options)
.then(function ($) {
news.source = $('.article__bd__from', 'div').children('a').text();
var texts = news.texts;
$('.article__bd__detail', 'div').children('p').each(function (idx, element) {
texts.push({
texts: $(element).text()
})
})
texts.shift();
for (var i = 0, n = 0; i < texts.length; i++) {
if (texts[i].texts.length == 0) {
texts.splice(i, 1);
}
}
})
callback(newsArr, news);
}
rp(options).then(function ($) {
var newsArr = [];
var item = $('.AnonymousHome_home__timeline__item_3vU', 'div')
item.map(function (idx, element) {
var news = {};
news.title = $(element).find('h3').children('a').text();
news.link = "https://xueqiu.com" + $(element).find('h3').children('a').attr('href');
news.editor = $(element).find('div').children('a').text();
news.source = '';
temp1 = $(element).find('.AnonymousHome_category_5zp').text().split(' ');
news.category = temp1[temp1.length - 1];
temp2 = $(element).find('div').children('span').text();
news.time = temp2.substr(temp2.length - 11, temp2.length);
news.read = $(element).find('.AnonymousHome_read_2t5').text();
news.contain = $(element).find('p').text();
news.texts = [];
getTexts(newsArr, news, function(newsArr, news) {
newsArr.push(news);
});
})
console.log(newsArr);
})
即使将push进行回调,依然没有source和texts的信息,推测是request-promise的异步行为所致。由于不深入了解其异步行为,暂时跳过这些内容,在获得二级目录下信息后立即输出,这样就不用考虑request和push的先后了
爬取部分的最终代码:
var cheerio = require('cheerio')
var rp = require('request-promise')
var options = {
uri: 'https://xueqiu.com',
transform: function (body) {
return cheerio.load(body);
}
}
//获得二级目录信息
function getTexts(newsArr, news, callback) {
var options = {
uri: news.link,
transform: function (body) {
return cheerio.load(body);
}
}
rp(options).then(function ($) {
//获得source
news.source = $('.article__bd__from', 'div').children('a').text();
//获得texts,并且清除无效部分
var texts = news.texts;
$('.article__bd__detail', 'div').children('p').each(function (idx, element) {
texts.push({
texts: $(element).text()
})
})
texts.shift();
for (var i = 0, n = 0; i < texts.length; i++) {
if (texts[i].texts.length == 0) {
texts.splice(i, 1);
}
}
console.log(news); //直接输出news,不再push入newsArr中
})
//callback(newsArr, news);
}
rp(options).then(function ($) {
var newsArr = [];
var item = $('.AnonymousHome_home__timeline__item_3vU', 'div')
item.map(function (idx, element) {
var news = {};
news.title = $(element).find('h3').children('a').text();
news.link = "https://xueqiu.com" + $(element).find('h3').children('a').attr('href');
news.editor = $(element).find('div').children('a').text();
news.source = '';
temp1 = $(element).find('.AnonymousHome_category_5zp').text().split(' ');
news.category = temp1[temp1.length - 1];
temp2 = $(element).find('div').children('span').text();
news.time = temp2.substr(temp2.length - 11, temp2.length);
news.read = $(element).find('.AnonymousHome_read_2t5').text();
news.contain = $(element).find('p').text();
news.texts = [];
getTexts(newsArr, news, function(newsArr, news) {
newsArr.push(news);
});
})
//console.log(newsArr);
})
可以看到,所有信息都可以被正确的爬取到,最后一步是将其存储至文件中
4)存储信息
需要用到的语法
var fs = require('fs') //导入文件操作模块
var str = JSON.stringify(object) //将对象信息转换为JSON文本
fs.writeFile('./xueqiu.json', JSON.stringify(news) + '\n', {'flag': 'a'}, function(err) {
if (err) {
return console.error(err);
}
})
//以追加方式存入文件
最终的效果:
最终的代码:
var cheerio = require('cheerio')
var rp = require('request-promise')
var fs = require('fs')
var options = {
uri: 'https://xueqiu.com',
transform: function (body) {
return cheerio.load(body);
}
}
fs.unlink('./xueqiu.json', function(err) {
if (err) {
return console.error(err);
}
})
//获得二级目录信息
function getTexts(newsArr, news, callback) {
var options = {
uri: news.link,
transform: function (body) {
return cheerio.load(body);
}
}
rp(options).then(function ($) {
//获得source
news.source = $('.article__bd__from', 'div').children('a').text();
//获得texts,并且清除无效部分
var texts = news.texts;
$('.article__bd__detail', 'div').children('p').each(function (idx, element) {
texts.push({
texts: $(element).text()
})
})
texts.shift();
for (var i = 0, n = 0; i < texts.length; i++) {
if (texts[i].texts.length == 0 || texts[i].texts == ' ') {
texts.splice(i, 1);
}
}
fs.writeFile('./xueqiu.json', JSON.stringify(news) + '\n', {'flag': 'a'}, function(err) {
if (err) {
return console.error(err);
}
})
console.log(news); //直接输出news,不再push入newsArr中
})
//callback(newsArr, news);
}
rp(options).then(function ($) {
var newsArr = [];
var item = $('.AnonymousHome_home__timeline__item_3vU', 'div')
item.map(function (idx, element) {
var news = {};
news.title = $(element).find('h3').children('a').text();
news.link = "https://xueqiu.com" + $(element).find('h3').children('a').attr('href');
news.editor = $(element).find('div').children('a').text();
news.source = '';
temp1 = $(element).find('.AnonymousHome_category_5zp').text().split(' ');
news.category = temp1[temp1.length - 1];
temp2 = $(element).find('div').children('span').text();
news.time = temp2.substr(temp2.length - 11, temp2.length);
news.read = $(element).find('.AnonymousHome_read_2t5').text();
news.contain = $(element).find('p').text();
news.texts = [];
getTexts(newsArr, news, function(newsArr, news) {
newsArr.push(news);
});
})
//console.log(newsArr);
})
本实验项目源码:https://github.com/AquariusAQ/Web-Crawler-in-Node.js