BBS爬虫系统架构介绍

系统概述

  • 渠道监测目的在于通过爬取各类渠道、网盘、论坛、贴吧等抓取和App相关的音讯,通过对获取信息的剖析识别正盗版,下发总结分析报告,援救App开发者监控应用市场、帖子、论坛、网盘等上的正盗版境况。

  • 上边是实际的效能点:

  1. 通过后台上传APP提交渠道监测系列。
  2. 后台获取到上传APP后对APP举行拍卖获取使用签名信息、获取使用名称、获取使用包名、获取使用文本结构等音信。
  3. 按照取得的应用新闻与网络爬虫技术对互联网渠道拓展定位爬取。
  4. 对国内428家应用市场、网盘、70家开发者论坛与开发者社区、78个关系客户端安全息息相关的贴吧等渠道举行实时爬取。(完善中……)
  5. 对爬取数据举行联合入库储存。
  6. 对发现的可疑盗版应用举办一键解析,发现可疑盗版应用注入的恶意代码、修改的资源等。(完善中……)
  7. 对发现的可疑盗版应用举行下架扶助。(后续计划)
  8. 对监测的水道数据、发现的可疑盗版应用数据和可疑盗版的下架操作过程与结果生成文档予以举报。

功效正在渐渐完善中…..

  • 依附一张效果图:
效果图

寻思导图

渠道监控思维导图

上边黄色线框圈起来的就是明日我要给大家介绍的bbs爬虫系统。

BBS爬虫系统的实现

bbs爬虫系统遵照github上开源的pyspider爬虫系统贯彻,start的数码一度过万,fork的数额也超过了2600多,也毕竟一个相比优质的开源项目了。我先大致介绍下pyspider的效用点和架构设计:

pyspider的介绍

pyspider的介绍

地方的截图,来自pyspider官网文档中的介绍。pyspider拔取python语言编写,匡助可视化界面在线编辑、调试爬虫脚本,辅助爬虫任务的实时监督,补助分布式部署,数据存储帮忙mysql、sqlite、ES、MongoDB等常见的数据库。其分布式部署通过消息中间件实现,可以应用redis,rabbitmq等中间件作为其分布式部署的音信中间件。除此之外,还援助爬虫任务的重试,通过令牌桶的方法限速,任务的先行级可控制,爬虫任务的晚点时间等等。它的过多表征都是通用的爬虫系统所应有具备的,假如我们要起来写一个爬虫系统,就要去解决那多少个问题,费时费劲,不如站在巨人的肩膀上,把它改造成符合大家轨道的轮子。

Pyspider的架构设计

  • pyspider爬虫可以分为下面多少个基本组件:
  1. Fetcher –
    依照url抓取互联网资源,下载html内容。Fetcher通过异步IO的不二法门实现,可以襄助很大并发量的抓取,瓶颈首要在IO开销和IP资源上。单个ip假诺爬取过快,很容易触动了bbs的反爬虫系统,很容易被挡住,可以运用ip代理池的点子解决ip限制的题材。假如没有ip代理池可以透过pyspider的限速机制,防止触碰反爬虫发机制。补助多节点部署。

  2. Processor –
    处理我们编辑的爬虫脚本。比如,提取页面中的链接,提取翻页链接,提取页面中的详细信息等,这块相比较消耗CPU资源。帮忙多节点部署。

  3. WebUI –
    可视化的界面,辅助在线编辑、调试爬虫脚本;扶助爬虫任务的实时在线监控;可经过界面启动、截止、删除、限速爬虫任务。襄助多节点部署。

  4. Scheduler –
    定时任务组件。每个url对应一个task,scheduler负责task的散发,新的url的入库,通过信息队列协调各种零部件。只好单节点部署。

  5. ResultWorker –
    结果写入组件。襄助爬虫结果的自定义实现,比如大家贯彻了遵照RDS的自定义结果写入。可以不实现,默认使用sqlite作为结果输出,可以导出为execel,json等格式。

  • 架构图
pyspider的架构
  • pyspider的webui界面长下边这多少个样子

webui

  • 我们要编制的台本

from pyspider.libs.base_handler import *

class Handler(BaseHandler):
    # 全局设定,遇到反爬的情况需要在其中配置对应的措施(如代理,UA,Cookies,渲染.......),
    crawl_config = {
    }

    @every(minutes=24 * 60)
    def on_start(self):
        # seed urls
        self.crawl('http://scrapy.org/', callback=self.index_page)

    @config(age=10 * 24 * 60 * 60)
    def index_page(self, response):
        for each in response.doc('a[href^="http"]').items():
            self.crawl(each.attr.href, callback=self.detail_page)

    def detail_page(self, response):
        # result
        return {
            "url": response.url,
            "title": response.doc('title').text(),
        }

有关Pyspider更多的文档,可以参考:pyspider文档

BBS爬虫系统中Pyspider的运用

流程图

流程图的步子表达如下:

  • 1-1步:
    用户通过WebUI界面写好剧本,调试、保存好之后,点击Run启动爬虫脚本。WebUI会通过xmlrpc的章程调用scheduler的new_task方法,创立新的爬虫任务。(这一步通过java对pyspider的开创脚本过程进展了包装,实现了基于用户上传的apk自动成立爬虫脚本)

  • 2步:scheduler通过xmlrpc的主意得到爬虫任务后(json_string定义),起先了部分革新项目状况,更新优先级,更新时间等拍卖。处理完这个逻辑后,通过send_task方法把url发往fetcher。

  • 3,4步:fetcher通过异步IO的法子抓取html页面,然后把结果发往processor。

  • 5步:processor得到fetcher发过来的html页面后,调用用户的index_page方法。在
    index_page方法中赢得页面中的超链接,同时回调detial_page方法。Processor把分析后的结果发送到ResultWorker(基于NCR的list实现的queue)。

  • 6,7步: ResultWorker组件从NCR获取result,写入数据库RDS中。

  • 1-2步:processor中的index_page除了可以提取所需页面的详情外,还足以获取翻页链接,然后把拿到后的分页链接发往scheduler,从而形成一整个闭环。

布置结构图

部署图

  • Fetcher部署在外网中,制止反爬虫系统获拿到ip来源。线上配备了7个Fetcher,Fetcher重要用于http访问,抓取html页面,使用异步IO的点子。
  • Processor部署在内网中,线上配置了2个节点。
  • Scheduler部署在内网中,线上部署了1一个节点,并且和WebUI部署在一台机器上。
  • ResultWorker部署在内网中,部署了2个节点。
  • sqlite,WebUI部署在内网中,只安排了2个节点,并且应用Http
    Auth对页面访问举行权力限制。
  • Fetcher, Processor, Scheduler,
    ResultWorker,它们均通过Redis的队列(基于list实现)相互通信,Scheduler是决定中,负责爬虫任务(task)的散发。一个url的爬虫就是一个task,task对象中有task_id,默认基于url的md5值实现,用于url去重。每个task都有一个默认的优先级,用户可以接纳@priority在index_page和detail_page方法上应用;通过自定义优先级,我们能够实现页面的深浅优先或广度优先的遍历。

BBS相比较正盗版流程

活动登录和死灰复燃

对bbs的爬虫是基于python语言实现的,而爬取到html页面后的逻辑是基于java语言实现的。pyspider爬虫后的结果插入到数据库了,java的定时任务读取数据库记录,举办后续的处理。

  1. 把活动登录和死灰复燃模块注册到服务登记主题。自动登录和復苏基于HtmlUnit或WebDriver +
    Headless
    chrome实现的。验证码辨识服务能够识别中文、英文、数字和问答题等。不襄助滑动验证码。对于滑动验证码的模式当下使用Chrome插件手动复制库克ie内容的方法贯彻。登录成功后,获取cookie内容用于后续的电动还原等;同时为了保证cookie的有限性,举办了定时检查cookie的行之有效,及时更新cookie。

  2. java定时任务先河读取pyspider的爬虫结果记录。

  3. 据悉数据库的字段instanceName去调用电动登录和恢复生机模块。instanceName字段是为了确保不同的爬虫结果页面正确的呼应自己bbs的cookie,方便自动还原。自动还原成功后,把苏醒过的页面插入到RDS中,方便后续逻辑的处理。

  4. 从RDS中读取回复过的页面,遍历每个页面的节点、属性等拿到下载链接。这里的遍历相比较复杂,需要各样相当和畸形的拍卖。提取到的链接或者为网盘链接,附件链接等等。论坛上的附件大多时百度网盘的短链接,并且很多都亟待输入提取码,我们实现了这个过程的自动化。可以在帖子的页面提取到百度网盘链接,并且获拿到相应的提取码;然后,使用NodeJs+PhathomJs的法子贯彻了实在下载地址的提取。由于百度网盘进行了限定速度,所有我们延续又协助了断点下载的主意,襄助更高的容错。

  5. 据悉取拿到下载链接举办分类处理。假倘使百度网盘的下载链接,则调用百度网盘相关的工具类,获取网盘中的文件的真实性下载地址,帮助单个和批量下载;倘若间接是实事求是的下载地址,则平昔开展下载。

  6. 据悉比较算法分析正盗版。

  7. 把相比较后的结果插入数据库,然后总括分析。

领取百度网盘的诚实下载地址

领取网盘链接

  • java定时任务取得到还原过的帖子后,分析、遍历html元素的节点、内容等,遵照正在提取出网盘链接和呼应的提取码。获取到链接和提取码后,PhantomJs执行js获取提取网盘链接的音讯,其中可能涉嫌到验证码辨识,因为百度网盘限制了长期内同一个链接提取真实下载地址的次数,超越两回提取就需要输入验证码了。在取得到需要的具备消息后,比如:token,loginid等,然后采取java发ajax请求带上这一个参数去取得真实的下载地址。下载到的文书或者是一个压缩包,也恐怕是一个单身的apk文件,这取决于分享的是四个文件,仍旧单个文件。

  • 领取百度网盘音信的js脚本:

var page = require('webpage').create(), stepIndex = 0, loadInProgress = false;
var fs = require('fs');
var system = require('system');

page.viewportSize = {
  width: 480,
  height: 800
};

// 命令行参数
var args = system.args;
var codeVal = (args[2] === 'null') ? "" : args[2], loadUrl = args[1];
console.log('codeVal=' + codeVal + '; loadUrl=' + loadUrl);

// 直接运行脚本的参数
// var codeVal = "nd54";
// var loadUrl = "https://pan.baidu.com/s/1sltxlYP";

// phantom.setProxy('116.62.112.142','16816', 'http', 'jingxuan2046', 'p4ke0xy1');
// 配置
page.settings.userAgent = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.78 Safari/537.36';
page.settings.resourceTimeout = 5000;

// 回调函数
page.onConsoleMessage = function (msg) {
  console.log(msg);
};

page.onResourceRequested = function (request) {
  //console.log('Request Faild:' + JSON.stringify(request, undefined, 4));
};

page.onError = function (msg, trace) {
  var msgStack = ['PHANTOM ERROR: ' + msg];
  if (trace && trace.length) {
    msgStack.push('TRACE:');
    trace.forEach(function (t) {
      msgStack.push(
          ' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function
              ? ' (in function ' + t.function + ')' : ''));
    });
  }
  console.error(msgStack.join('\n'));
  // phantom.exit(1);
};

// 变量定义
var baiDuObj;
var steps = [
  function () {
    page.clearCookies();// 每次请求之前先清理cookie

    if (loadUrl === null || loadUrl.length == 0) {
      console.error('loadUrl不能为空串!');
      phantom.exit();
      return;
    }

    // 渲染页面
    console.log("加载页面中... loadUrl= " + loadUrl);

    // 目的是为了先刷新出必要的Cookie,再去访问shareUrl,不然会报403
    page.open(loadUrl, function (status) {
      console.log('status=' + status);
      setTimeout(function () {
        page.evaluate(function (loadUrl) {
          // console.log(document.cookie)
          window.location.href = loadUrl;
        }, loadUrl);
      }, 500)
    });
  },

  function () {
    // page.render('step1.png');

    var currentUrl = page.url;

    console.log("currentUrl=" + currentUrl);
    console.log("codeVal=" + codeVal);

    if (currentUrl === null || currentUrl === "") {
      console.log('当前url为空,脚本退出执行...');
      phantom.exit(1);
      return;
    }

    // 提取码为空时就不需要再输入了
    if (codeVal === null || codeVal.length == 0 || codeVal === "") {
      console.log('当前分享不需要提取码...');
      return;
    }

    // 自动输入提取码
    page.evaluate(function (codeVal) {
      // 当请求页面中不存在accessCode元素时,就不继续执行了
      var accessCodeEle = document.getElementsByTagName('input').item(0);
      console.log(accessCodeEle);
      if (accessCodeEle === null) {
        console.info("页面不存在accessCode元素..." + accessCodeEle);
      } else {
        accessCodeEle.value = codeVal;
        var element = document.getElementsByClassName('g-button').item(0);
        console.log(element);
        var event = document.createEvent("MouseEvents");
        event.initMouseEvent(
            "click", // 事件类型
            true,
            true,
            window,
            1,
            0, 0, 0, 0, // 事件的坐标
            false, // Ctrl键标识
            false, // Alt键标识
            false, // Shift键标识
            false, // Meta键标识
            0, // Mouse左键
            element); // 目标元素

        element.dispatchEvent(event);

        // 点击提交提取码,然后跳转到下载页面
        element.click();
      }
    }, codeVal);
  },
  function () {
    // page.render('step2.png');

    page.includeJs('https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js',
        function () {
          baiDuObj = page.evaluate(function () {
            var yunData = window.yunData;
            var cookies = document.cookie;
            var panAPIUrl = location.protocol + "//" + location.host + "/api/";
            var shareListUrl = location.protocol + "//" + location.host
                + "/share/list";

            // 变量定义
            var sign, timestamp, logid, bdstoken, channel, shareType, clienttype, encrypt, primaryid, uk, product, web, app_id, extra, shareid, is_single_share;
            var fileList = [], fidList = [];
            var vcode;// 验证码

            // 初始化参数
            function initParams() {
              shareType = getShareType();
              sign = yunData.SIGN;
              timestamp = yunData.TIMESTAMP;
              bdstoken = yunData.MYBDSTOKEN;
              channel = 'chunlei';
              clienttype = 0;
              web = 1;
              app_id = 250528;
              logid = getLogID();
              encrypt = 0;
              product = 'share';
              primaryid = yunData.SHARE_ID;
              uk = yunData.SHARE_UK;
              shareid = yunData.SHARE_ID;
              is_single_share = isSingleShare();

              if (shareType == 'secret') {
                extra = getExtra();
              }

              if (is_single_share) {
                var obj = {};
                if (yunData.CATEGORY == 2) {
                  obj.filename = yunData.FILENAME;
                  obj.path = yunData.PATH;
                  obj.fs_id = yunData.FS_ID;
                  obj.isdir = 0;
                } else {
                  obj.filename = yunData.FILEINFO[0].server_filename;
                  obj.path = yunData.FILEINFO[0].path;
                  obj.fs_id = yunData.FILEINFO[0].fs_id;
                  obj.isdir = yunData.FILEINFO[0].isdir;
                }
                fidList.push(obj.fs_id);
                fileList.push(obj);
              } else {
                fileList = getFileList();
                $.each(fileList, function (index, element) {
                  fidList.push(element.fs_id);
                });
              }
            }

            //判断分享类型(public或者secret)
            function getShareType() {
              return yunData.SHARE_PUBLIC === 1 ? 'public' : 'secret';
            }

            //判断是单个文件分享还是文件夹或者多文件分享
            function isSingleShare() {
              return yunData.getContext === undefined;
            }

            // 获取cookie
            function getCookie(e) {
              var o, t;
              var n = document, c = decodeURI;
              return n.cookie.length > 0 && (o = n.cookie.indexOf(e + "="), -1
              != o) ? (o = o + e.length + 1, t = n.cookie.indexOf(";", o), -1
              == t && (t = n.cookie.length), c(n.cookie.substring(o, t))) : "";
            }

            // 私密分享时需要sekey
            function getExtra() {
              var seKey = decodeURIComponent(getCookie('BDCLND'));
              return '{' + '"sekey":"' + seKey + '"' + "}";
            }

            function base64Encode(t) {
              var a, r, e, n, i, s, o = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
              for (e = t.length, r = 0, a = ""; e > r;) {
                if (n = 255 & t.charCodeAt(r++), r == e) {
                  a += o.charAt(n >> 2);
                  a += o.charAt((3 & n) << 4);
                  a += "==";
                  break;
                }
                if (i = t.charCodeAt(r++), r == e) {
                  a += o.charAt(n >> 2);
                  a += o.charAt((3 & n) << 4 | (240 & i) >> 4);
                  a += o.charAt((15 & i) << 2);
                  a += "=";
                  break;
                }
                s = t.charCodeAt(r++);
                a += o.charAt(n >> 2);
                a += o.charAt((3 & n) << 4 | (240 & i) >> 4);
                a += o.charAt((15 & i) << 2 | (192 & s) >> 6);
                a += o.charAt(63 & s);
              }
              return a;
            }

            // 获取登录id
            function getLogID() {
              var name = "BAIDUID";
              var u = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/~!@#¥%……&";
              var d = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;
              var f = String.fromCharCode;

              function l(e) {
                if (e.length < 2) {
                  var n = e.charCodeAt(0);
                  return 128 > n ? e : 2048 > n ? f(192 | n >>> 6) + f(
                      128 | 63 & n) : f(224 | n >>> 12 & 15) + f(
                      128 | n >>> 6 & 63) + f(128 | 63 & n);
                }
                var n = 65536 + 1024 * (e.charCodeAt(0) - 55296)
                    + (e.charCodeAt(1) - 56320);
                return f(240 | n >>> 18 & 7) + f(128 | n >>> 12 & 63) + f(
                        128 | n >>> 6 & 63) + f(128 | 63 & n);
              }

              function g(e) {
                return (e + "" + Math.random()).replace(d, l);
              }

              function m(e) {
                var n = [0, 2, 1][e.length % 3];
                var t = e.charCodeAt(0) << 16 | (e.length > 1 ? e.charCodeAt(1)
                        : 0) << 8 | (e.length > 2 ? e.charCodeAt(2) : 0);
                var o = [u.charAt(t >>> 18), u.charAt(t >>> 12 & 63),
                  n >= 2 ? "=" : u.charAt(t >>> 6 & 63),
                  n >= 1 ? "=" : u.charAt(63 & t)];
                return o.join("");
              }

              function h(e) {
                return e.replace(/[\s\S]{1,3}/g, m);
              }

              function p() {
                return h(g((new Date()).getTime()));
              }

              function w(e, n) {
                return n ? p(String(e)).replace(/[+\/]/g, function (e) {
                  return "+" == e ? "-" : "_";
                }).replace(/=/g, "") : p(String(e));
              }

              return w(getCookie(name));
            }

            //获取当前目录
            function getPath() {
              var hash = location.hash;
              var regx = /(^|&|\/)path=([^&]*)(&|$)/i;
              var result = hash.match(regx);
              return decodeURIComponent(result[2]);
            }

            //获取分类显示的类别,即地址栏中的type
            function getCategory() {
              var hash = location.hash;
              var regx = /(^|&|\/)type=([^&]*)(&|$)/i;
              var result = hash.match(regx);
              return decodeURIComponent(result[2]);
            }

            function getSearchKey() {
              var hash = location.hash;
              var regx = /(^|&|\/)key=([^&]*)(&|$)/i;
              var result = hash.match(regx);
              return decodeURIComponent(result[2]);
            }

            //获取当前页面(list或者category)
            function getCurrentPage() {
              var hash = location.hash;
              return decodeURIComponent(
                  hash.substring(hash.indexOf('#') + 1, hash.indexOf('/')));
            }

            //获取文件信息列表
            function getFileList() {
              var result = [];
              if (getPath() == '/') {
                result = yunData.FILEINFO;
              } else {
                logid = getLogID();
                var params = {
                  uk: uk,
                  shareid: shareid,
                  order: 'other',
                  desc: 1,
                  showempty: 0,
                  web: web,
                  dir: getPath(),
                  t: Math.random(),
                  bdstoken: bdstoken,
                  channel: channel,
                  clienttype: clienttype,
                  app_id: app_id,
                  logid: logid
                };
                $.ajax({
                  url: shareListUrl,
                  method: 'GET',
                  async: false,
                  data: params,
                  success: function (response) {
                    if (response.errno === 0) {
                      result = response.list;
                    }
                  }
                });
              }
              return result;
            }

            //生成下载时的fid_list参数
            function getFidList(list) {
              var retList = null;
              if (list.length === 0) {
                return null;
              }

              var fileidlist = [];
              $.each(list, function (index, element) {
                fileidlist.push(element.fs_id);
              });
              retList = '[' + fileidlist + ']';
              return retList;
            }

            // 初始化
            initParams();

            // console.log('fileList=---------' + fileList);
            // console.log('fidList=---------' + getFidList(fileList))

            var retObj = {
              'sign': sign,
              'timestamp': timestamp,
              'logid': logid,
              'bdstoken': bdstoken,
              'channel': channel,
              'shareType': shareType,
              'clienttype': clienttype,
              'encrypt': encrypt,
              'primaryid': primaryid,
              'uk': uk,
              'product': product,
              'web': web,
              'app_id': app_id,
              'extra': extra,
              'shareid': shareid,
              'fid_list': getFidList(fileList),// 要下载的文件id
              'file_list': fileList,
              'panAPIUrl': panAPIUrl,
              'single_share': is_single_share,
              'cookies': cookies
            };

            return retObj;
          });

          console.log("data=" + JSON.stringify(baiDuObj));
        });
  },
  function () {
  }
];

// main started
setInterval(function () {
  if (!loadInProgress && typeof steps[stepIndex] == "function") {

    console.log(
        '                                                                                               ');
    console.log(
        '===============================================================================================');
    console.log('                                    step ' + (stepIndex + 1)
        + '                               ');
    console.log(
        '===============================================================================================');
    console.log(
        '                                                                                               ');

    steps[stepIndex]();
    stepIndex++;
  }

  if (typeof steps[stepIndex] != "function") {
    console.log("Completed!");
    console.log('FinalOutPut: codeVal=' + codeVal + "; loadUrl=" + loadUrl
        + "; result=" + JSON.stringify(baiDuObj));
    phantom.exit();
  }
}, 5000);

地点的大约就是漫天bbs爬取的大约过程了。由于篇幅有限,有的地方恐怕说的相比含糊或者不明晰的地点,欢迎商量。本人技术水平有限,有不当或不足的地方欢迎批评指正。

网站地图xml地图