我们这节的目标是学习完本节课程后,能进行网页简单的分析与抓取,对抓取到的信息进行输出和文本保存。

爬虫的思路很简单:

  1. 确定要抓取的URL;
  2. 对URL进行抓取,获取网页内容;
  3. 对内容进行分析并存储;
  4. 重复第1步

本文地址: http://www.xiabingbao.com/node/2017/01/19/node-spider.html

总索引:

  1. 从0到1学习node(一)之模块规范
  2. 从0到1学习node(二)之搭建http服务器
  3. 从0到1学习node(三)之文件操作
  4. 从0到1学习node(四)之简易的网络爬虫
  5. 从0到1学习node(五)之mysql数据库的操作

在这节里做爬虫,我们使用到了两个重要的模块:

  • request : 对http进行封装,提供更多、更方便的接口供我们使用,request进行的是异步请求。更多信息可以去[request-github]上进行查看
  • cheerio : 类似于jQuery,可以使用$(), find(), text(), html()等方法提取页面中的元素和数据,不过若仔细比较起来,cheerio中的方法不如jQuery的多。

1. hello world

说是hello world,其实首先开始的是最简单的抓取。我们就以cnode网站为例(https://cnodejs.org/),这个网站的特点是:

  • 不需要登录即可访问首页和其他页面
  • 页面都是同步渲染的,没有异步请求的问题
  • 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() );
}
});

这样的一段代码就实现了一个简单的网络爬虫,爬取到源码后,再对源码进行拆解分析,比如我们要获取首页中第1页的 问题标题,作者,跳转链接,点击数量,回复数量。通过chrome,我们可以得到这样的结构:

每个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, "");
}

2. 爬取多个页面

上面我们只爬取了一个页面,怎么在一个程序里爬取多个页面呢?还是以CNode网站为例,刚才只是爬取了第1页的数据,这里我们想请求它前6页的数据(别同时抓太多的页面,会被封IP的)。每个页面的结构是一样的,我们只需要修改url地址即可。

2.1 同时抓取多个页面

首先把request请求封装为一个方法,方便进行调用,若还是使用console.log方法的话,会把6页的数据都输出到控制台,看起来很不方便。这里我们就使用到了上节文件操作内容,引入fs模块,将获取到的内容写入到文件中,然后新建的文件放到file目录下(需手动创建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();

这样就实现了异步调用时控制同时请求的数量。

3. 抓取需要登录的页面

比如我们在抓取CNode,百度贴吧等一些网站,是不需要登录就可以直接抓取的,那么如知乎等网站,必须登录后才能抓取,否则直接跳转到登录页面。这种情况我们该怎么抓取呢?

使用cookie。 用户登录后,都会在cookie中记录下用户的一些信息,我们在抓取一些页面,带上这些cookie,服务器就会认为我们处于登录状态,程序就能抓取到我们想要的信息。

先在浏览器上登录我们的帐号,然后在console中使用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来进行伪造。

4. 保存抓取到的图片

页面中的文本内容可以提炼后保存到文本或者数据库中,那么图片怎么保存到本地呢。

图片可以使用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/),就可以看到下载的图片了。

5. 总结

我们这里只是写了一个简单的爬虫,针对更复杂的功能,则需要更复杂的算法的来控制了。还有如何抓取ajax的数据,我们会在后面进行讲解。

本文地址: http://www.xiabingbao.com/node/2017/01/19/node-spider.html

从0到1学习node之简易的网络爬虫的更多相关文章

  1. 爬虫学习之基于Scrapy的网络爬虫

    ###概述 在上一篇文章<爬虫学习之一个简单的网络爬虫>中我们对爬虫的概念有了一个初步的认识,并且通过Python的一些第三方库很方便的提取了我们想要的内容,但是通常面对工作当作复杂的需求 ...

  2. 假期学习【六】Python网络爬虫2020.2.4

    今天通过Python网络爬虫视频复习了一下以前初学的网络爬虫,了解了网络爬虫的相关规范. 案例:京东的Robots协议 https://www.jd.com/robots.txt 说明可以爬虫的范围 ...

  3. 从0到1学习node(七)之express搭建简易论坛

    我们需要搭建的这个简易的论坛主要的功能有:注册.登录.发布主题.回复主题.下面我们来一步步地讲解这个系统是如何实现的. 总索引: http://www.xiabingbao.com/node/2017 ...

  4. 学习推荐《精通Python网络爬虫:核心技术、框架与项目实战》中文PDF+源代码

    随着大数据时代的到来,我们经常需要在海量数据的互联网环境中搜集一些特定的数据并对其进行分析,我们可以使用网络爬虫对这些特定的数据进行爬取,并对一些无关的数据进行过滤,将目标数据筛选出来.对特定的数据进 ...

  5. 前端学习 node 快速入门 系列 —— 简易版 Apache

    其他章节请看: 前端学习 node 快速入门 系列 简易版 Apache 我们用 node 来实现一个简易版的 Apache:提供静态资源访问的能力. 实现 直接上代码. - demo - stati ...

  6. 原生node实现简易留言板

    原生node实现简易留言板 学习node,实现一个简单的留言板小demo 1. 使用模块 http模块 创建服务 fs模块 操作读取文件 url模块 便于path操作并读取表单提交数据 art-tem ...

  7. 前端学习 node 快速入门 系列 —— 服务端渲染

    其他章节请看: 前端学习 node 快速入门 系列 服务端渲染 在简易版 Apache一文中,我们用 node 做了一个简单的服务器,能提供静态资源访问的能力. 对于真正的网站,页面中的数据应该来自服 ...

  8. 新手入门指导:Vue 2.0 的建议学习顺序

    起步 1. 扎实的 JavaScript / HTML / CSS 基本功.这是前置条件. 2. 通读官方教程 (guide) 的基础篇.不要用任何构建工具,就只用最简单的 <script> ...

  9. 新手向:Vue 2.0 的建议学习顺序

    新手向:Vue 2.0 的建议学习顺序 尤雨溪   1 年前 注:2.0 已经有中文文档 .如果对自己英文有信心,也可以直接阅读英文文档.此指南仅供参考,请根据自身实际情况灵活调整.欢迎转载,请注明出 ...

随机推荐

  1. Sencha Touch学习(一)

    一.Ext的内部类结构示意图 基类Ext.Base 该类是所有通过Ext.define定义出来的类的基类. 是所有Ext类的基石. 来自为知笔记(Wiz)

  2. AsParallel 用法

    http://www.cnblogs.com/leslies2/archive/2012/02/08/2320914.html AsParallel 通常想要实现并行查询,只需向数据源添加 AsPar ...

  3. 17.4.3 使用MulticastSocket实现多点广播(4)

    17.4.3  使用MulticastSocket实现多点广播(4) 通过UserInfo类的封装,所有客户端只需要维护该UserInfo类的列表,程序就可以实现广播.发送私聊信息等功能.本程序底层通 ...

  4. HDU 2412 Party at Hali-Bula

    树形DP水题.判断取法是否唯一,dp的时候记录一下每个状态从下面的子节点推导过来的时候是否唯一即可. #include<cstdio> #include<cstring> #i ...

  5. MFC中PeekMessage的使用,非阻塞消息循环

    在程序设计的时候经常要进行一个数据循环,比如播放音乐需要循环的向缓冲区里面写入数据,在这个时候比较通用的方法是建立一个线程做事情,但是有时候不想创建多线程就可以使用微软提供的PeekMessage方法 ...

  6. 9、手把手教你Extjs5(九)使用MVVM特性控制菜单样式

    菜单的样式多了,怎么可以灵活的切换是个问题. 在使用标准菜单的时候,在菜单最前面有二个按钮,可以切换到树状菜单和按钮菜单. 在树状菜单的显示区,可以切换换到标准菜单,以及折叠式菜单. 切换到按钮菜单之 ...

  7. MySQL临时表与派生表(简略版)

    MySQL临时表与派生表 当主查询中包含派生表,或者当select 语句中包含union字句,或者当select语句中包含一个字段的order by 子句(对另一个字段的group by 子句)时,M ...

  8. PHPEXCEL实例-导出EXCEL

      PHPExcel 是相当强大的 MS Office Excel 文档生成类库,当需要输出比较复杂格式数据的时候,PHPExcel 是个不错的选择. <?php /* * 导出EXCEL *  ...

  9. Java语言中IO流的操作规律学习笔记

    1,明确源和目的. 数据源:就是需要读取,可以使用两个体系:InputStream.Reader: 数据汇:就是需要写入,可以使用两个体系:OutputStream.Writer: 总结: 读:就是把 ...

  10. html bottom html submit按钮表单控件与CSS美化

    一.html submit与bottom按钮基本语法结构 1.html submit按钮在input标签里设置type="submit"即可设置此表单控件为按钮. submit按钮 ...