从0到1学习node之简易的网络爬虫
我们这节的目标是学习完本节课程后,能进行网页简单的分析与抓取,对抓取到的信息进行输出和文本保存。
爬虫的思路很简单:
- 确定要抓取的URL;
 - 对URL进行抓取,获取网页内容;
 - 对内容进行分析并存储;
 - 重复第1步
 
本文地址: http://www.xiabingbao.com/node/2017/01/19/node-spider.html
总索引:
- 从0到1学习node(一)之模块规范
 - 从0到1学习node(二)之搭建http服务器
 - 从0到1学习node(三)之文件操作
 - 从0到1学习node(四)之简易的网络爬虫
 - 从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之简易的网络爬虫的更多相关文章
- 爬虫学习之基于Scrapy的网络爬虫
		
###概述 在上一篇文章<爬虫学习之一个简单的网络爬虫>中我们对爬虫的概念有了一个初步的认识,并且通过Python的一些第三方库很方便的提取了我们想要的内容,但是通常面对工作当作复杂的需求 ...
 - 假期学习【六】Python网络爬虫2020.2.4
		
今天通过Python网络爬虫视频复习了一下以前初学的网络爬虫,了解了网络爬虫的相关规范. 案例:京东的Robots协议 https://www.jd.com/robots.txt 说明可以爬虫的范围 ...
 - 从0到1学习node(七)之express搭建简易论坛
		
我们需要搭建的这个简易的论坛主要的功能有:注册.登录.发布主题.回复主题.下面我们来一步步地讲解这个系统是如何实现的. 总索引: http://www.xiabingbao.com/node/2017 ...
 - 学习推荐《精通Python网络爬虫:核心技术、框架与项目实战》中文PDF+源代码
		
随着大数据时代的到来,我们经常需要在海量数据的互联网环境中搜集一些特定的数据并对其进行分析,我们可以使用网络爬虫对这些特定的数据进行爬取,并对一些无关的数据进行过滤,将目标数据筛选出来.对特定的数据进 ...
 - 前端学习 node 快速入门 系列 —— 简易版 Apache
		
其他章节请看: 前端学习 node 快速入门 系列 简易版 Apache 我们用 node 来实现一个简易版的 Apache:提供静态资源访问的能力. 实现 直接上代码. - demo - stati ...
 - 原生node实现简易留言板
		
原生node实现简易留言板 学习node,实现一个简单的留言板小demo 1. 使用模块 http模块 创建服务 fs模块 操作读取文件 url模块 便于path操作并读取表单提交数据 art-tem ...
 - 前端学习 node 快速入门 系列 —— 服务端渲染
		
其他章节请看: 前端学习 node 快速入门 系列 服务端渲染 在简易版 Apache一文中,我们用 node 做了一个简单的服务器,能提供静态资源访问的能力. 对于真正的网站,页面中的数据应该来自服 ...
 - 新手入门指导:Vue 2.0 的建议学习顺序
		
起步 1. 扎实的 JavaScript / HTML / CSS 基本功.这是前置条件. 2. 通读官方教程 (guide) 的基础篇.不要用任何构建工具,就只用最简单的 <script> ...
 - 新手向:Vue 2.0 的建议学习顺序
		
新手向:Vue 2.0 的建议学习顺序 尤雨溪 1 年前 注:2.0 已经有中文文档 .如果对自己英文有信心,也可以直接阅读英文文档.此指南仅供参考,请根据自身实际情况灵活调整.欢迎转载,请注明出 ...
 
随机推荐
- PAT (Advanced Level) 1081. Rational Sum (20)
			
简单模拟题. #include<cstdio> #include<cstring> #include<cmath> #include<vector> # ...
 - 记一次gitlab添加账号收不到邮件的解决办法
			
之前gitlab创建账号可以正常收到邮件,最近就收不到,查了gitlab的配置以及postfix都没有问题,发来查看了发信25端口,该端口被屏蔽,提交工单到阿里云那边收到回复说是服务器统一关闭25端口 ...
 - MySql绿色版安装过程记录
			
作为程序猿,要多动手,周末趁着有空且笔记本刚刚装了系统,所以就配置了下绿色版的MySQL. 多动手,多动手,多动手. 多总结,多总结,多总结. 以下为正文: 一.下载MySQL绿色版: 1.这个地址: ...
 - windows服务器下IIS7 安装URL Rewrite(URL重写)模块
			
URL Rewrite Module是一个基于规则的URL重写引擎,用于在URL被Web服务器处理之前改变请求的URL.对于动态Web应用程序,它可以为用户和seo/seo.html" ta ...
 - 《accelerated  c++》---------第六章
			
本章主要讲了算法部分.就是<algoruthm>里面的算法.
 - AOJ2249最短路+最小费用
			
题意:求出某个点到其他点的最短路,并求出在最短路情况下的最小费用 分析:用dijkstra求出最短路并同时更新最小费用即可,注意的是在最短路长度相同时费用取最小即可 #include <iost ...
 - c语言 inline函数
			
大学在教科书上学习过inline函数,定义为inline函数之后,会省去函数调用的开销,直接嵌套汇编代码,取代函数调用,提高效率. google的google c++ style guide 1.in ...
 - Unity3d 开发之 ulua 坑的总结
			
相同的 lua 代码在安卓上能正常运行,但在 IOS 上可能不会正常运行而导致报红,崩溃等,我在使用 lua 编程时遇到的一些坑总结如下: 1. File.ReadAllText, 诸如以下代码在 i ...
 - JQuery实现超链接鼠标提示效果
			
一.第一种方法用Jquery<p><a href="http://www.nowamagic.net/" class="tooltip" ti ...
 - VB.NET中网络编程所需组件WinHTTP的添加
			
VB.NET中网络编程所需组件: WinHTTP组件:项目-->添加引用-->COM选项卡-->Microsoft WinHTTP Services,version 5.1--> ...