Node.js编写爬虫的基本思路及抓取百度图片的实例分享


Posted in Javascript onMarch 12, 2016

其实写爬虫的思路十分简单:

  • 按照一定的规律发送 HTTP 请求获得页面 HTML 源码(必要时需要加上一定的 HTTP 头信息,比如 cookie 或 referer 之类)
  • 利用正则匹配或第三方模块解析 HTML 代码,提取有效数据
  • 将数据持久化到数据库中

但是真正写起这个爬虫来,我还是遇到了很多的问题(和自己的基础不扎实也有很大的关系,node.js 并没有怎么认真的学过)。主要还是 node.js 的异步和回调知识没有完全掌握,导致在写代码的过程中走了很多弯路。

模块化

模块化对于 node.js 程序是至关重要的,不能像原来写 PHP 那样所有的代码都扔到一个文件里(当然这只是我个人的恶习),所以一开始就要分析这个爬虫需要实现的功能,并大致的划分了三个模块。

主程序,调用爬虫模块和持久化模块实现完整的爬虫功能
爬虫模块,根据传来的数据发送请求,解析 HTML 并提取有用数据,返回一个对象
持久化模块,接受一个对象,将其中的内容储存到数据库中
模块化也带来了困扰了我一个下午的问题:模块之间的异步调用导致数据错误。其实我至今都不太明白问题到底出在哪儿,鉴于脚本语言不那么方便的调试功能,暂时还没有深入研究。

另外一点需要注意的是,模块化时尽量慎用全局对象来储存数据,因为可能你这个模块的一个功能还没有结束,这个全局变量已经被修改了。

Control Flow

这个东西很难翻译,直译叫控制流(吗)。众所周知,node.js 的核心思想就是异步,但是异步多了就会产生好几层嵌套,代码实在难看。这个时候,你需要借助一些 Control Flow 模块来重新整理你的逻辑。在这里就要推荐开发社区十分活跃,用起来也很顺手的 async.js(https://github.com/caolan/async/)。

async 提供了很多实用的方法,我在写爬虫时主要用到了

  • async.eachSeries(arr, fn, callback)  依次把 arr 中的每一个元素传给 fn,若 fn 回调没有返回错误对象就继续传下一个,否则把错误对象传给 callback,循环结束
  • async.parallel(fn[, fn] , callback)  当所有的 fn 都执行完成后执行 callback

这些控制流方法给爬虫的开发工作带来了很大的方便。考虑这么一个应用场景,你需要把若干条数据插入数据库(属于同一个学生),你需要在所有数据都插入完成后才能返回结果,那么如何保证所有的插入操作都结束了呢?只能是层层回调保证,如果用 async.parallel 就方便多了。

这里再多提一句,本来保证所有的插入都完成这个操作可以在 SQL 层实现,即 transaction,但是 node-mysql 截止我使用的时候还是没有很好的支持 transaction,所以只有自己手动用代码保证了。

解析 HTML

在解析过程中也遇到一些问题,这里一并记录下来。

最基本的发送 HTTP 请求获得 HTML 代码,使用 node 自带的 http.request 功能即可。如果是爬简单的内容,比如获得某个指定 id 元素中的内容(常见于抓去商品价格),那么正则足以完成任务。但是对于复杂的页面,尤其是数据项较多的页面,使用 DOM 会更加方便高效。

而 node.js 最好的 DOM 实现非 cheerio(https://github.com/MatthewMueller/cheerio) 莫属了。其实 cheerio 应该算是 jQuery 的一个针对 DOM 操作优化和精简的子集,包含了 DOM 操作的大部分内容,去除了其它不必要的内容。使用 cheerio 你就可以像用普通 jQuery 选择器那样选择你需要的内容。

下载图片
在爬数据时,我们可能还需要下载图片。其实下载图片的方式和普通的网页没有太大的区别,但是有一点让我吃了苦头。

注意下面代码中言辞激烈的注释,那就是我年轻时犯下的错误……

var req = http.request(options, function(res){

  //初始化数据!!!
  var binImage = '';

  res.setEncoding('binary');
  res.on('data', function(chunk){
   binImage += chunk;
  });

  res.on('end', function(){

   if (!binImage) {
    console.log('image data is null');
    return null;
   }

   fs.writeFile(imageFolder + filename, binImage, 'binary', function(err){
    if (err) {
     console.log('image writing error:' + err.message);
     return null;
    }
    else{
     console.log('image ' + filename + ' saved');
     return filename;
    }
   });
  });

  res.on('error', function(e){
   console.log('image downloading response error:' + e.message);
   return null;
  });
 });

 req.end();

GBK 转码
另外一个值得说明的问题就是 node.js 爬虫在爬 GBK 编码内容时转码的问题,其实这个问题很好解决,但是新手可能会绕弯路。这里就把源码全部奉上:

var req = http.request(options, function(res) {
  res.setEncoding('binary');
  res.on('data', function (chunk) {
  html += chunk;
  });

  res.on('end', function(){
  //转换编码
  html = iconv.decode(html, 'gbk');
  });
 });

 req.end();

这里我使用的转码库是 iconv-lite(https://github.com/ashtuchkin/iconv-lite),完美支持 GBK 和 GB2312 等双字节编码。

实例:爬虫批量下载百度图片

var fs = require('fs'), 
 path = require('path'), 
 util = require('util'), // 以上为Nodejs自带依赖包 
 request = require('request'); // 需要npm install的包 
 
// main函数,使用 node main执行即可 
patchPreImg(); 
 
// 批量处理图片 
function patchPreImg() { 
 var tag1 = '摄影', tag2 = '国家地理', 
  url = 'http://image.baidu.com/data/imgs?pn=%s&rn=60&p=channel&from=1&col=%s&tag=%s&sort=1&tag3=', 
  url = util.format(url, 0, tag1, tag2), 
  url = encodeURI(url), 
  dir = 'D:/downloads/images/', 
  dir = path.join(dir, tag1, tag2), 
  dir = mkdirSync(dir); 
 
 request(url, function(error, response, html) { 
  var data = JSON.parse(html); 
  if (data && Array.isArray(data.imgs)) { 
   var imgs = data.imgs; 
   imgs.forEach(function(img) { 
    if (Object.getOwnPropertyNames(img).length > 0) { 
     var desc = img.desc || ((img.owner && img.owner.userName) + img.column); 
     desc += '(' + img.id + ')'; 
     var downloadUrl = img.downloadUrl || img.objUrl; 
     downloadImg(downloadUrl, dir, desc); 
    } 
   }); 
  } 
 }); 
} 
 
// 循环创建目录 
function mkdirSync(dir) { 
 var parts = dir.split(path.sep); 
 for (var i = 1; i <= parts.length; i++) { 
  dir = path.join.apply(null, parts.slice(0, i)); 
  fs.existsSync(dir) || fs.mkdirSync(dir); 
 } 
 return dir; 
} 
 
var index = 1; 
// 开始下载图片,并log统计日志 
function downloadImg(url, dir, desc) { 
 var fileType = 'jpg'; 
 if (url.match(/\.(\w+)$/)) fileType = RegExp.$1; 
 desc += '.' + fileType; 
 var options = { 
  url: url, 
  headers: { 
   Host: 'f.hiphotos.baidu.com', 
   Cookie: 'BAIDUID=810ACF57B5C38556045DFFA02C61A9F8:FG=1;' 
  } 
 }; 
 var startTime = new Date().getTime(); 
 request(options) 
  .on('response', function() { 
   var endTime = new Date().getTime(); 
   console.log('Downloading...%s.. %s, 耗时: %ss', index++, desc, (endTime - startTime) / 1000); 
  }) 
  .pipe(fs.createWriteStream(path.join(dir, desc))); 
}
Javascript 相关文章推荐
关于取不到由location.href提交而来的上级页面地址的解决办法
Jul 30 Javascript
网站导致浏览器崩溃的原因总结(多款浏览器) 推荐
Apr 15 Javascript
Javascript实现字数统计
Jul 03 Javascript
Javascript打印局部页面实例
Jun 21 Javascript
VUE2实现事件驱动弹窗示例
Oct 21 Javascript
jQuery实现动态加载select下拉列表项功能示例
May 31 jQuery
jQuery实现ajax回调函数带入参数的方法示例
Jun 26 jQuery
关于vue状态过渡transition不起作用的原因解决
Apr 09 Javascript
微信小程序网络请求实现过程解析
Nov 06 Javascript
JavaScript原型式继承实现方法
Nov 06 Javascript
如何基于layui的laytpl实现数据绑定的示例代码
Apr 10 Javascript
Vue作用域插槽实现方法及作用详解
Jul 08 Javascript
JavaScript中循环遍历Array与Map的方法小结
Mar 12 #Javascript
Node.js的Express框架使用上手指南
Mar 12 #Javascript
Node.js项目中调用JavaScript的EJS模板库的方法
Mar 11 #Javascript
JavaScript操作HTML DOM节点的基础教程
Mar 11 #Javascript
举例说明JavaScript中的实例对象与原型对象
Mar 11 #Javascript
JavaScript中setTimeout和setInterval函数的传参及调用
Mar 11 #Javascript
原生JavaScript制作微博发布面板效果
Mar 11 #Javascript
You might like
浅析PHP substr,mb_substr以及mb_strcut的区别和用法
2013/06/21 PHP
php处理静态页面:页面设置缓存时间实例
2017/06/22 PHP
javascript引用对象的方法
2007/01/11 Javascript
js字符编码函数区别分析
2008/06/05 Javascript
EXTjs4.0的store的findRecord的BUG演示代码
2013/06/08 Javascript
JS实现遮罩层效果的简单实例
2013/11/12 Javascript
jquery 合并内容相同的单元格(示例代码)
2013/12/13 Javascript
js实现仿网易点击弹出提示同时背景变暗效果
2015/08/13 Javascript
js中使用使用原型(prototype)定义方法的好处详解
2016/07/04 Javascript
jQuery事件委托之Safari
2016/07/05 Javascript
bootstrap table 表格中增加下拉菜单末行出现滚动条的快速解决方法
2017/01/05 Javascript
bootstrap选项卡使用方法解析
2017/01/11 Javascript
js鼠标跟随运动效果
2017/03/11 Javascript
JS开发中百度地图+城市联动实现实时触发查询地址功能
2017/04/13 Javascript
jsonp跨域及实现百度首页联想功能的方法
2018/08/30 Javascript
详解实现一个通用的“划词高亮”在线笔记功能
2019/04/23 Javascript
vue iview多张图片大图预览、缩放翻转
2019/07/13 Javascript
微信小程序使用canvas自适应屏幕画海报并保存图片功能
2019/07/25 Javascript
BootStrap表单验证中的非Submit类型按钮点击时触发验证的坑
2019/09/05 Javascript
微信小程序文章详情功能完整实例
2020/06/03 Javascript
Python自动化测试Eclipse+Pydev 搭建开发环境
2016/08/15 Python
Python+tkinter使用40行代码实现计算器功能
2018/01/30 Python
Django中使用 Closure Table 储存无限分级数据
2019/06/06 Python
Python中字符串List按照长度排序
2019/07/01 Python
Django组件content-type使用方法详解
2019/07/19 Python
Python3压缩和解压缩实现代码
2021/03/01 Python
国际贸易专业个人职业生涯规划
2014/02/15 职场文书
酒店保安领班职务说明书
2014/03/04 职场文书
分层教学实施方案
2014/03/19 职场文书
初二学习计划书范文
2014/04/27 职场文书
国庆节促销广告语2014
2014/09/19 职场文书
地道战观后感300字
2015/06/04 职场文书
高一数学教学反思
2016/02/18 职场文书
在CSS中使用when/else的方法
2022/01/18 HTML / CSS
Consul在linux环境的集群部署
2022/04/08 Servers
使用python生成大量数据写入es数据库并查询操作(2)
2022/09/23 Python