前言

前文介绍过用Python写爬虫,但是当任务多的时候就比较慢, 这是由于Python自带的http库urllib2发起的http请求是阻塞式的,这意味着如果采用单线程模型,那么整个进程的大部分时间都阻塞在等待服务端把数据传输过来的过程中。所以我们这次尝试用node.js去做这个爬虫。

为什么选择node.js

node.js是一款基于google的V8引擎开发javascript运行环境。在高性能的V8引擎以及事件驱动的单线程异步非阻塞运行模型的支持下,node.js实现的web服务可以在没有Nginx的http服务器做反向代理的情况下实现很高的业务并发量。

分布式爬虫设计

这次也用上次的分布式设计,使用Redis服务器来作为任务队列。

如图:

异步

node.js是基于异步的写法,有时一个函数需要上一个函数的返回值做参数,这样下来一不小心就会陷入回调地狱的陷阱中。

所以这次我们用async模块控制流程。

准备工作

  1. 安装node.js和Redis
  2. 安装request、async与Redis相关的库

代码

主函数(master.js)

  1. "use strict"
  2. const request = require('request')
  3. const cheerio = require('cheerio')
  4. const fs = require('fs')
  5. const utils = require('./utils')
  6. const log = utils.log
  7. const config = require('./config')
  8. const task_url_head = config.task_url_head
  9. const main_url = config.main_url
  10. const proxy_url = config.proxy_url
  11. const redis_cache = require('./redis_cache')
  12. const redis_client = redis_cache.client
  13. const Task = function() {
  14. this.id = 0
  15. this.title = ''
  16. this.url = ''
  17. this.file_name = ''
  18. this.file_url = 0
  19. this.is_download = false
  20. }
  21. //总下载数
  22. var down_cont = 0
  23. //当前下载数
  24. var cur_cont = 0
  25. const taskFromBody = function(task_url, body) {
  26. const task = new Task()
  27. // cheerio.load 用字符串作为参数返回一个可以查询的特殊对象
  28. // body 就是 html 内容
  29. const e = cheerio.load(body)
  30. // 查询对象的查询语法和 DOM API 中的 querySelector 一样
  31. const title = e('.controlBar').find('.epi-title').text()
  32. const file_url = e('.audioplayer').find('audio').attr('src')
  33. const ext = file_url.substring(file_url.length-4)
  34. const task_id = task_url.substring(task_url.length-5)
  35. const file_name = task_id+'.'+title+ext
  36. task.id = task_id
  37. task.title = title
  38. task.url = task_url
  39. task.file_name = file_name.replace(/\//g,"-").replace(/:/g,":")
  40. task.file_url = file_url
  41. task.is_download = false
  42. redis_client.set('Task:id:'+task_id,JSON.stringify(task),function (error, res) {
  43. if (error) {
  44. log('Task:id:'+task_id, error)
  45. } else {
  46. log('Task:id:'+task_id, res)
  47. }
  48. cur_cont = cur_cont + 1
  49. if (down_cont == cur_cont) {
  50. // 操作完成,关闭redis连接
  51. redis_client.end(true);
  52. log('已完成')
  53. }
  54. })
  55. }
  56. const taskFromUrl = function(task_url) {
  57. request({
  58. 'url':task_url,
  59. 'proxy':proxy_url,
  60. },
  61. function(error, response, body) {
  62. // 回调函数的三个参数分别是 错误, 响应, 响应数据
  63. // 检查请求是否成功, statusCode 200 是成功的代码
  64. if (error === null && response.statusCode == 200) {
  65. taskFromBody(task_url, body)
  66. } else {
  67. log('*** ERROR 请求失败 ', error)
  68. }
  69. })
  70. }
  71. const parseLink = function(div) {
  72. let e = cheerio.load(div)
  73. let href = e('a').attr('href')
  74. return href
  75. }
  76. const dataFromUrl = function(url) {
  77. // request 从一个 url 下载数据并调用回调函数
  78. request({
  79. 'url' : url,
  80. 'proxy' : proxy_url,
  81. },
  82. function(error, response, body) {
  83. // 回调函数的三个参数分别是 错误, 响应, 响应数据
  84. // 检查请求是否成功, statusCode 200 是成功的代码
  85. if (error === null && response.statusCode == 200) {
  86. // cheerio.load 用字符串作为参数返回一个可以查询的特殊对象
  87. // body 就是 html 内容
  88. const e = cheerio.load(body)
  89. // 查询对象的查询语法和 DOM API 中的 querySelector 一样
  90. const itmeDivs = e('.epiItem.video')
  91. for(let i = 0; i < itmeDivs.length; i++) {
  92. let element = itmeDivs[i]
  93. // 获取 div 的元素并且用 itmeFromDiv 解析
  94. // 然后加入 link_list 数组中
  95. const div = e(element).html()
  96. // log(div)
  97. const url_body = parseLink(div)
  98. const task_url = task_url_head+url_body
  99. down_cont = itmeDivs.length
  100. taskFromUrl(task_url)
  101. // redis_client.set('Task:id:'+task_id+':url', task_link, )
  102. }
  103. // 操作完成,关闭redis连接
  104. // redis_client.end(true)
  105. log('*** success ***')
  106. } else {
  107. log('*** ERROR 请求失败 ', error)
  108. }
  109. })
  110. }
  111. const __main = function() {
  112. // 这是主函数
  113. const url = main_url
  114. dataFromUrl(url)
  115. }
  116. __main()

从函数(salver.js)

  1. "use strict"
  2. const http = require("http")
  3. const fs = require("fs")
  4. const path = require("path")
  5. const redis = require('redis')
  6. const async = require('async')
  7. const utils = require('./utils')
  8. const log = utils.log
  9. const config = require('./config')
  10. const save_dir_path = config.save_dir_path
  11. const redis_cache = require('./redis_cache')
  12. const redis_client = redis_cache.client
  13. //总下载数
  14. var down_cont = 0
  15. //当前下载数
  16. var cur_cont = 0
  17. const getHttpReqCallback = function(fileUrl, dirName, fileName, downCallback) {
  18. log('getHttpReqCallback fileName ', fileName)
  19. var callback = function (res) {
  20. log("request: " + fileUrl + " return status: " + res.statusCode)
  21. if (res.statusCode != 200) {
  22. startDownloadTask(fileUrl, dirName, fileName, downCallback)
  23. return
  24. }
  25. var contentLength = parseInt(res.headers['content-length'])
  26. var fileBuff = []
  27. res.on('data', function (chunk) {
  28. var buffer = new Buffer(chunk)
  29. fileBuff.push(buffer)
  30. })
  31. res.on('end', function () {
  32. log("end downloading " + fileUrl)
  33. if (isNaN(contentLength)) {
  34. log(fileUrl + " content length error")
  35. return
  36. }
  37. var totalBuff = Buffer.concat(fileBuff)
  38. log("totalBuff.length = " + totalBuff.length + " " + "contentLength = " + contentLength)
  39. if (totalBuff.length < contentLength) {
  40. log(fileUrl + " download error, try again")
  41. startDownloadTask(fileUrl, dirName, fileName, downCallback)
  42. return
  43. }
  44. fs.appendFile(dirName + "/" + fileName, totalBuff, function (err) {
  45. if (err){
  46. throw err;
  47. }else{
  48. log('download success')
  49. downCallback()
  50. }
  51. })
  52. })
  53. }
  54. return callback
  55. }
  56. var startDownloadTask = function (fileUrl, dirName, fileName, downCallback) {
  57. log("start downloading " + fileUrl)
  58. var option = {
  59. host : '127.0.0.1',
  60. port : '8087',
  61. method:'get',//这里是发送的方法
  62. path : fileUrl,
  63. headers:{
  64. 'Accept-Language':'zh-CN,zh;q=0.8',
  65. 'Host':'maps.googleapis.com'
  66. }
  67. }
  68. var req = http.request(option, getHttpReqCallback(fileUrl, dirName, fileName, downCallback))
  69. req.on('error', function (e) {
  70. log("request " + fileUrl + " error, try again")
  71. startDownloadTask(fileUrl, dirName, fileName, downCallback)
  72. })
  73. req.end()
  74. }
  75. const beginTask = function(task_key, callback) {
  76. log('beginTask', task_key)
  77. redis_client.get(task_key,function (err,v){
  78. let task = JSON.parse(v)
  79. // log('task', task)
  80. let file_url = task.file_url
  81. let dir_path = save_dir_path
  82. let file_name = task.file_name
  83. if (task.is_download === false) {
  84. startDownloadTask(file_url, dir_path, file_name,function(){
  85. task.is_download = true
  86. redis_client.set(task_key, JSON.stringify(task), function (error, res) {
  87. log('update redis success', task_key)
  88. // cur_cont = cur_cont + 1
  89. // if(cur_cont == down_cont){
  90. // redis_client.end(true)
  91. // }
  92. callback(null,"successful !");
  93. })
  94. })
  95. }else{
  96. callback(null,"successful !");
  97. }
  98. })
  99. }
  100. const mainTask = function() {
  101. redis_client.keys('Task:id:[0-9]*',function (err,v){
  102. // log(v.sort())
  103. let task_keys = v.sort()
  104. down_cont = task_keys.length
  105. log('down_cont', down_cont)
  106. //控制异步
  107. async.mapLimit(task_keys, 2, function(task_key,callback){
  108. beginTask(task_key, callback)
  109. },function(err,result){
  110. if(err){
  111. log(err);
  112. }else{
  113. // log(result); //会输出多个“successful”字符串的数组
  114. log("all down!");
  115. redis_client.end(true)
  116. }
  117. });
  118. })
  119. }
  120. const initDownFile = function() {
  121. fs.readdir(save_dir_path, function(err, files){
  122. if (err) {
  123. return console.error(err)
  124. }
  125. let file_list = []
  126. files.forEach( function (file){
  127. file_list.push(file.substring(0, 5))
  128. })
  129. // log(file_list)
  130. redis_client.keys('Task:id:[0-9]*',function (err,v){
  131. let task_keys = v
  132. // log(task_keys)
  133. let unfinish_len = task_keys.filter((item)=>file_list.includes(item.substring(item.length - 5)) == false).length
  134. let cur_unfinish_lent = 0
  135. task_keys.forEach(function (task_key){
  136. let task_id = task_key.substring(task_key.length - 5)
  137. if (file_list.includes(task_id) == false) {
  138. // log(task_key)
  139. redis_client.get(task_key,function (err,v){
  140. let task = JSON.parse(v)
  141. task.is_download = false
  142. // log(task)
  143. // log(task_key)
  144. redis_client.set(task_key, JSON.stringify(task), function (error, res) {
  145. cur_unfinish_lent++
  146. // log('cur_unfinish_lent', cur_unfinish_lent)
  147. if (cur_unfinish_lent == unfinish_len) {
  148. redis_client.end(true)
  149. log('init finish')
  150. }
  151. })
  152. })
  153. }
  154. })
  155. })
  156. })
  157. }
  158. const __main = function() {
  159. // 这是主函数
  160. // initDownFile()
  161. mainTask()
  162. }
  163. __main()

完整代码的地址

https://github.com/zhourunliang/nodejs_crawler

node.js主从分布式爬虫的更多相关文章

  1. 基于Node.js的强大爬虫 能直接发布抓取的文章哦

    基于Node.js的强大爬虫 能直接发布抓取的文章哦 基于Node.js的强大爬虫能直接发布抓取的文章哦!本爬虫源码基于WTFPL协议,感兴趣的小伙伴们可以参考一下 一.环境配置 1)搞一台服务器,什 ...

  2. Node.js 网页瘸腿爬虫初体验

    延续上一篇,想把自己博客的文档标题利用Node.js的request全提取出来,于是有了下面的初哥爬虫,水平有限,这只爬虫目前还有点瘸腿,请看官你指正了. // 内置http模块,提供了http服务器 ...

  3. Node.js大众点评爬虫

    大众点评上有很多美食餐馆的信息,正好可以拿来练练手Node.js. 1. API分析 大众点评开放了查询商家信息的API,这里给出了城市与cityid之间的对应关系,链接http://m.api.di ...

  4. 使用node.js制作简易爬虫

    最近看了些node.js方面的知识,就像拿它来做些什么.因为自己喜欢摄影,经常上蜂鸟网,所以寻思了一下,干脆做个简单的爬虫来扒论坛的帖子. 直接上代码吧. var sys = require(&quo ...

  5. node.js 89行爬虫爬取智联招聘信息

    写在前面的话, .......写个P,直接上效果图.附上源码地址  github/lonhon ok,正文开始,先列出用到的和require的东西: node.js,这个是必须的 request,然发 ...

  6. 使用Node.js搭建数据爬虫crawler

    0. 通用爬虫框架包括: (1) 将爬取url加入队列,并获取指定url的前端资源(crawler爬虫框架主要使用Crawler类进行抓取网页) (2)解析前端资源,获取指定所需字段的值,即获取有价值 ...

  7. 用Node.js写爬虫,撸羞羞的图片

    说到爬虫,很多人都认为是很高大上的东西.哇塞,是不是可以爬妹纸图啊,是不是可以爬小片片啊.答案就是对的.爬虫可以完成这些东西的操作.但是,作为一个正直的程序员,我们要在法律允许范围内用爬虫来为我们服务 ...

  8. Node.js 使用爬虫批量下载网络图片到本地

    图片网站往往广告众多,用Node.js写个爬虫下载图片,代码不长,省事不少,比手动一张张保存简直是天与地的区别.以前用Java也做过远程图片下载,但Node.js的下载速度更让人咂舌,这也是非阻塞式变 ...

  9. Node.js 网页爬虫再进阶,cheerio助力

    任务还是读取博文标题. 读取app2.js // 内置http模块,提供了http服务器和客户端功能 var http=require("http"); // cheerio模块, ...

随机推荐

  1. 算法题:整形数组找a和b使得a+b=n

    题目: 数组 A 由 1000 万个随机正整数 (int) 组成,设计算法,给定整数 n,在 A 中找出 a 和 b,使其符合如下等式: n = a + b 解题思路: 1. 1000w个随机正整数占 ...

  2. 如何动态调用 C 函数

    JSPatch 支持了动态调用 C 函数,无需在编译前桥接每个要调用的 C 函数,只需要在 JS 里调用前声明下这个函数,就可以直接调用: require('JPEngine').addExtensi ...

  3. c# 匿名函数与托付

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/han_yankun2009/article/details/26290779    在 2.0之前的 ...

  4. Geeks : Kruskal’s Minimum Spanning Tree Algorithm 最小生成树

    版权声明:本文作者靖心,靖空间地址:http://blog.csdn.net/kenden23/.未经本作者同意不得转载. https://blog.csdn.net/kenden23/article ...

  5. 1562. [NOI2009]变换序列【二分图】

    Description Input Output Sample Input 5 1 1 2 2 1 Sample Output 1 2 4 0 3 HINT 30%的数据中N≤50: 60%的数据中N ...

  6. k8s存储 pv pvc ,storageclass

    1.  pv  pvc 现在测试 glusterfs  nfs  可读可写, 多个pod绑定到同一个pvc上,可读可写. 2. storageclass  分成两种 (1)  建立pvc, 相当于多个 ...

  7. webapi中使用swagger

    net WebApi中使用swagger 我在WebApi中使用swagger的时候发现会出现很多问题,搜索很多地方都没找到完全解决问题的方法,后面自己解决了,希望对于遇到同样问题朋友有帮助.我将先一 ...

  8. 20145203盖泽双 《Java程序设计》第十周学习总结

    20145203盖泽双 <Java程序设计>第十周学习总结 教材学习内容总结 一.网络概述 1.网络编程就是两个或多个设备(程序)之间的数据交换. 2.识别网络上的每个设备:①IP地址②域 ...

  9. Jenkins构建Python项目失败

     Console Output 提示:'Python' 不是内部或外部命令,也不是可运行的程序 定位原因:python.exe 不在jenkins执行用户的PATH里面 解决:构建的时候Python命 ...

  10. 针对IE及其它的css hack

    现在一些针对针对政府的oa项目还要去解决兼容IE6 7 8,这对前端开发来说简直是灾难,在要使用一些css3,或者H5的地方,我们就要慎重了,在使用新特性的同时要兼顾老的浏览器的,做到优雅降级,或者针 ...