Node.js网络爬虫实战:从基础抓取到异步控制与登录处理

作者:岩茶

时间:2026-10-21

浏览:117

本文详解Node.js网络爬虫开发,利用request和cheerio模块实现网页数据抓取。内容涵盖基础页面解析、多页数据批量获取、通过调度器控制并发请求数量以防止IP封禁,以及利用Cookie和Referer处理需要登录验证的页面,并介绍图片保存方法。

文章目录

Node.js网络爬虫基础与核心模块

前言

此前已介绍Node.js的基础知识,本节课程旨在掌握网页内容的分析与抓取技术,实现抓取数据的输出及文本持久化存储。

爬虫的基本逻辑流程如下:

  1. 第一步,确定目标URL;
  2. 第二步,发起请求获取网页源码;
  3. 第三步,解析网页内容并存储所需数据;
  4. 重复第1步

本节爬虫开发主要依赖两个核心模块:

  • request模块:对HTTP请求进行封装,提供便捷的异步接口,具体用法可参考相关文档。
  • cheerio模块:功能类似jQuery,支持$()、find()、text()、html()等方法提取页面元素和数据,虽然方法数量不及jQuery丰富,但足以满足常规解析需求。

基础页面抓取与DOM解析

一、基础抓取示例

所谓的Hello World示例,即实现最简单的网页抓取。以CNode社区(https://cnodejs.org/)为例,该网站具备以下特点:

  1. 无需登录即可访问首页及内容页;
  2. 页面采用同步渲染,不存在异步加载数据的复杂情况;
  3. DOM结构清晰,易于解析。

具体实现代码如下:


var request = require('request'),
 cheerio = require('cheerio');

request('https://cnodejs.org/', function(err, response, body){
 if( !err && response.statusCode == 200 ){
 // body为源码
 // 使用 cheerio.load 将字符串转换为 cheerio(jQuery) 对象,
 // 按照jQuery方式操作即可
 var $ = cheerio.load(body);
 
 // 输出导航的html代码
 console.log( $('.nav').html() );
 }
});

上述代码实现了基础的网络爬虫功能,获取源码后需进行解析分析。例如,我们需要提取首页第一页的问题标题、作者、跳转链接、点击数和回复数。通过浏览器开发者工具,可观察到如下DOM结构:

每个div[.cell]代表一个完整的题目单元,在此处暂称为$item。


{
 title : $item.find('.topic_title').text(),
 url : $item.find('.topic_title').attr('href'),
 author : $item.find('.user_avatar img').attr('title'),
 reply : $item.find('.count_of_replies').text(),
 visits : $item.find('.count_of_visits').text()
}

因此,通过循环遍历div[.cell],即可提取所需信息:


request('https://cnodejs.org/?_t='+Date.now(), function(err, response, body){
 if( !err && response.statusCode == 200 ){
 var $ = cheerio.load(body);

 var data = [];
 $('#topic_list .cell').each(function(){
  var $this = $(this);
 
 // 使用trim去掉数据两端的空格
  data.push({
  title : trim($this.find('.topic_title').text()),
  url : trim($this.find('.topic_title').attr('href')),
  author : trim($this.find('.user_avatar img').attr('title')),
  reply : trim($this.find('.count_of_replies').text()),
  visits : trim($this.find('.count_of_visits').text())
  })
 });
 // console.log( JSON.stringify(data, ' ', 4) );
 console.log(data);
 }
});

// 删除字符串左右两端的空格
function trim(str){ 
 return str.replace(/(^\s*)|(\s*$)/g, "");
}

多页面抓取与并发控制策略

二、多页面数据抓取

前述示例仅抓取了单页数据,如何在程序中实现多页面抓取?仍以CNode网站为例,此前仅抓取第一页,现需请求前6页数据(注意:避免短时间内高频抓取过多页面,以免导致IP被封禁)。各页面结构一致,仅需修改URL地址即可。

2.1 并发抓取多个页面

首先将request请求封装为独立方法以便调用。若继续使用console.log方法,会将6页数据全部输出至控制台,不利于查看。此处结合上一节介绍的fs模块,将获取的内容写入文件,并将新建文件存放于file目录下(需手动创建该目录):


// 把page作为参数传递进去,然后调用request进行抓取
function getData(page){
 var url = 'https://cnodejs.org/?tab=all&page='+page;
 console.time(url);
 request(url, function(err, response, body){
 if( !err && response.statusCode == 200 ){
  console.timeEnd(url); // 通过time和timeEnd记录抓取url的时间

  var $ = cheerio.load(body);

  var data = [];
  $('#topic_list .cell').each(function(){
  var $this = $(this);

  data.push({
   title : trim($this.find('.topic_title').text()),
   url : trim($this.find('.topic_title').attr('href')),
   author : trim($this.find('.user_avatar img').attr('title')),
   reply : trim($this.find('.count_of_replies').text()),
   visits : trim($this.find('.count_of_visits').text())
  })
  });
  // console.log( JSON.stringify(data, ' ', 4) );
  // console.log(data);
  var filename = './file/cnode_'+page+'.txt';
  fs.writeFile(filename, JSON.stringify(data, ' ', 4), function(){
  console.log( filename + ' 写入成功' );
  })
 }
 });
}

CNode分页请求链接格式为:https://cnodejs.org/?tab=all&page=2,只需修改page参数值即可:


var max = 6;
for(var i=1; i<=max; i++){

 getData(i);
}

如此即可同时请求前6页数据。执行脚本后,将输出每个链接抓取成功所消耗的时间,抓取完成后将相关信息写入文件:


$ node test.js
开始请求...
https://cnodejs.org/?tab=all&page=1: 279ms
./file/cnode_1.txt 写入成功
https://cnodejs.org/?tab=all&page=3: 372ms
./file/cnode_3.txt 写入成功
https://cnodejs.org/?tab=all&page=2: 489ms
./file/cnode_2.txt 写入成功
https://cnodejs.org/?tab=all&page=4: 601ms
./file/cnode_4.txt 写入成功
https://cnodejs.org/?tab=all&page=5: 715ms
./file/cnode_5.txt 写入成功
https://cnodejs.org/?tab=all&page=6: 819ms
./file/cnode_6.txt 写入成功

在file目录下即可看到生成的6个文件。

2.2 控制并发请求数量

若直接使用for循环,会同时发起所有请求。若同时请求100、200或500个页面,将对服务器造成巨大压力,极易导致IP被封禁。为此,编写了一个调度方法,限制每次最多同时发起5个请求,待前一个请求完成后,再从队列中取出下一个进行请求。


/*
 @param data [] 需要请求的链接的集合
 @param max num 最多同时请求的数量
*/
function Dispatch(data, max){
 var _max = max || 5, // 最多请求的数量
 _dataObj = data || [], // 需要请求的url集合
 _cur = 0, // 当前请求的个数
 _num = _dataObj.length || 0,
 _isEnd = false,
 _callback;

 var ss = function(){
 var s = _max - _cur;
 while(s--){
  if( !_dataObj.length ){
  _isEnd = true;
  break;
  }
  var surl = _dataObj.shift();
  _cur++;

  _callback(surl);
 }
 }

 this.start = function(callback){
 _callback = callback;

 ss();
 },

 this.call = function(){
 if( !_isEnd ){
  _cur--;
  ss();
 }
 }
}

var dis = new Dispatch(urls, max);
dis.start(getData);

在getData函数中,写入文件操作后调用dis的回调函数:


var filename = './file/cnode_'+page+'.txt';
fs.writeFile(filename, JSON.stringify(data, ' ', 4), function(){
 console.log( filename + ' 写入成功' );
})
dis.call();

由此实现了异步调用时对并发请求数量的有效控制。

登录验证处理与图片保存

三、抓取需登录的页面

例如抓取CNode或百度贴吧等网站时,无需登录即可直接获取数据;而像知乎等网站,必须登录后才能抓取,否则会被重定向至登录页。针对此类情况,应如何处理?

使用Cookie。用户登录后,服务器会在Cookie中记录用户信息。在抓取某些页面时,携带这些Cookie,服务器即认为用户处于登录状态,程序便可获取所需信息。

首先在浏览器上登录账号,然后在控制台使用document.domain获取所有Cookie字符串,将其复制至程序代码的cookie字段中(若知晓某些Cookie无需保留,可剔除)。


request({
 url:'https://www.zhihu.com/explore',
 headers:{
 // "Referer":"www.zhihu.com"
 cookie : xxx
 }
}, function(error, response, body){
 if (!error && response.statusCode == 200) {
 // console.log( body );
 var $ = cheerio.load(body);

 
 }
})

此外,在request中还可设置referer。部分接口或数据设置了referer限制,必须在特定域名下才能访问。此时可在request中设置referer进行伪造,以满足访问条件。

四、保存抓取到的图片

页面中的文本内容可提炼后保存至文本文件或数据库,那么图片如何保存至本地?

图片可通过request模块的pipe方法输出至文件流,随后使用fs.createWriteStream将其保存为图片文件。

此处将图片保存至以日期命名的目录中,mkdirp模块可一次性创建多级目录(如./img/2017/01/22)。图片名称可使用原始名称,也可根据自定义规则进行命名。


var request = require('request'),
 cheerio = require('cheerio'),
 fs = require('fs'),
 path = require('path'), // 用于分析图片的名称或者后缀名
 mkdirp = require('mkdirp'); // 用于创建多级目录

var date = new Date(),
 year = date.getFullYear(),
 month = date.getMonth()+1,
 month = ('00'+month).slice(-2), // 添加前置0
 day = date.getDate(),
 day = ('00'+day).slice(-2), // 添加前置0
 dir = './img/'+year+'/'+month+'/'+day+'/';

// 根据日期创建目录 ./img/2017/01/22/
var stats = fs.statSync(dir);
if( stats.isDirectory() ){
 console.log(dir+' 已存在');
}else{
 console.log('正在创建目录 '+dir);
 mkdirp(dir, function(err){
 if(err) throw err;
 })
}

request({
 url : 'http://desk.zol.com.cn/meinv/?_t='+Date.now()
}, function(err, response, body){
 if(err) throw err;

 if( response.statusCode == 200 ){
 var $ = cheerio.load(body);
 
 $('.photo-list-padding img').each(function(){
  var $this = $(this),
  imgurl = $this.attr('src');
  
  var ext = path.extname(imgurl); // 获取图片的后缀名,如 .jpg, .png .gif等
  var filename = Date.now()+'_'+ parseInt(Math.random()*10000)+ext; // 命名方式:毫秒时间戳+随机数+后缀名
  // var filename = path.basename(imgurl); // 直接获取图片的原名称
  // console.log(filename);
  download(imgurl, dir+filename); // 开始下载图片
 })
 }
});

// 保存图片
var download = function(imgurl, filename){
 request.head(imgurl, function(err, res, body) {
 request(imgurl).pipe(fs.createWriteStream(filename));
 console.log(filename+' success!');
 });
}

在对应的日期目录(如./img/2017/01/22/)下,即可查看下载的图片。

总结

本文仅实现了简单的爬虫功能,针对更复杂的功能,则需要更复杂的算法来控制。关于如何抓取Ajax数据,将在后续章节讲解。以上就是本文的全部内容,希望本文的内容对大家的学习或工作能带来一定的帮助,小编还会继续分享关于Node入门学习的文章,感兴趣的朋友们请继续关注本站。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章