diff --git a/docs/Architecture/App.md b/docs/Architecture/App.md new file mode 100644 index 00000000..5d5681fd --- /dev/null +++ b/docs/Architecture/App.md @@ -0,0 +1,2 @@ +# App + diff --git a/docs/Architecture/Celery.md b/docs/Architecture/Celery.md new file mode 100644 index 00000000..96dcec50 --- /dev/null +++ b/docs/Architecture/Celery.md @@ -0,0 +1,2 @@ +# Celery + diff --git a/docs/Architecture/README.md b/docs/Architecture/README.md new file mode 100644 index 00000000..21444c29 --- /dev/null +++ b/docs/Architecture/README.md @@ -0,0 +1,2 @@ +# 架构 + diff --git a/docs/Concept/Deploy.md b/docs/Concept/Deploy.md new file mode 100644 index 00000000..12f55ebf --- /dev/null +++ b/docs/Concept/Deploy.md @@ -0,0 +1,6 @@ +# 部署 + +所有爬虫在运行前需要被部署当相应当节点中。 + +部署时,爬虫会被打包到相应的目录中,方便环境隔离,开发环境的爬虫和生产环境的爬虫需要打包部署来实现隔离。 + diff --git a/docs/Concept/Node.md b/docs/Concept/Node.md new file mode 100644 index 00000000..3132f93f --- /dev/null +++ b/docs/Concept/Node.md @@ -0,0 +1,3 @@ +# 节点 + +节点其实就是Celery中的Worker。一个节点运行时会连接到一个任务队列(例如Redis)来接收和运行任务。所有爬虫需要在运行时被部署到节点上,用户在部署前需要定义节点的IP地址和端口。 diff --git a/docs/Concept/README.md b/docs/Concept/README.md new file mode 100644 index 00000000..a36e857f --- /dev/null +++ b/docs/Concept/README.md @@ -0,0 +1,2 @@ +# 概念 + diff --git a/docs/Concept/Spider.md b/docs/Concept/Spider.md new file mode 100644 index 00000000..dd7bebc1 --- /dev/null +++ b/docs/Concept/Spider.md @@ -0,0 +1,15 @@ +# 爬虫 + +## 自动发现 + +在`config.py`文件中,修改变量`PROJECT_SOURCE_FILE_FOLDER`作为爬虫项目所在的目录。Crawlab后台程序会自动发现这些爬虫项目并储存到数据库中。是不是很方便? + +## 部署爬虫 + +所有爬虫需要在抓取前被部署当相应当节点中。在"爬虫详情"页面点击"Deploy"按钮,爬虫将被部署到所有有效到节点中。 + +## 运行爬虫 + +部署爬虫之后,你可以在"爬虫详情"页面点击"Run"按钮来启动爬虫。一个爬虫任务将被触发,你可以在任务列表页面中看到这个任务。 + + diff --git a/docs/Concept/Task.md b/docs/Concept/Task.md new file mode 100644 index 00000000..bd75b96f --- /dev/null +++ b/docs/Concept/Task.md @@ -0,0 +1,3 @@ +# 任务 + +任务被触发并被节点执行。用户可以在任务详情页面中看到任务到状态、日志和抓取结果。 diff --git a/docs/Examples/README.md b/docs/Examples/README.md new file mode 100644 index 00000000..65afe604 --- /dev/null +++ b/docs/Examples/README.md @@ -0,0 +1,2 @@ +# Examples + diff --git a/docs/QuickStart/Installation.md b/docs/QuickStart/Installation.md new file mode 100644 index 00000000..3fce3e1c --- /dev/null +++ b/docs/QuickStart/Installation.md @@ -0,0 +1,22 @@ +# 安装 + +最快安装Crawlab的方式是克隆一份代码到本地 + +```bash +git clone https://github.com/tikazyq/crawlab +``` + +安装类库 + +```bash +# 安装后台类库 +pip install -r requirements.txt +``` + +```bash +# 安装前台类库 +cd frontend +npm install +``` + + diff --git a/docs/QuickStart/README.md b/docs/QuickStart/README.md new file mode 100644 index 00000000..6a6ea76f --- /dev/null +++ b/docs/QuickStart/README.md @@ -0,0 +1,4 @@ +# 快速开始 + +- [安装](Installation.md) +- [运行](Run.md) diff --git a/docs/QuickStart/Run.md b/docs/QuickStart/Run.md new file mode 100644 index 00000000..623261fe --- /dev/null +++ b/docs/QuickStart/Run.md @@ -0,0 +1,59 @@ +# 运行 + +在运行之前需要对Crawlab进行一些配置,配置文件为`config.py`。 + +```python +# project variables +PROJECT_SOURCE_FILE_FOLDER = '/Users/yeqing/projects/crawlab/spiders' # 爬虫源码根目录 +PROJECT_DEPLOY_FILE_FOLDER = '/var/crawlab' # 爬虫部署根目录 +PROJECT_LOGS_FOLDER = '/var/logs/crawlab' # 日志目录 +PROJECT_TMP_FOLDER = '/tmp' # 临时文件目录 + +# celery variables +BROKER_URL = 'redis://192.168.99.100:6379/0' # 中间者URL,连接redis +CELERY_RESULT_BACKEND = 'mongodb://192.168.99.100:27017/' # CELERY后台URL +CELERY_MONGODB_BACKEND_SETTINGS = { + 'database': 'crawlab_test', + 'taskmeta_collection': 'tasks_celery', +} +CELERY_TIMEZONE = 'Asia/Shanghai' +CELERY_ENABLE_UTC = True + +# flower variables +FLOWER_API_ENDPOINT = 'http://localhost:5555/api' # Flower服务地址 + +# database variables +MONGO_HOST = '192.168.99.100' +MONGO_PORT = 27017 +MONGO_DB = 'crawlab_test' + +# flask variables +DEBUG = True +FLASK_HOST = '127.0.0.1' +FLASK_PORT = 8000 +``` + +启动后端API,也就是一个Flask App,可以直接启动,或者用gunicorn代替。 + +```bash +python app.py +``` + +启动Flower服务(抱歉目前集成Flower到App服务中,必须单独启动来获取节点信息,后面的版本不需要这个操作)。 + +```bash +python ./bin/run_flower.py +``` + +启动本地Worker。在其他节点中如果想只是想执行任务的话,只需要启动这一个服务就可以了。 + +```bash +python ./bin/run_worker.py +``` + +启动前端服务器。 + +```bash +cd ../frontend +npm run serve +``` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..613decad --- /dev/null +++ b/docs/README.md @@ -0,0 +1,167 @@ +# Crawlab +基于Celery的爬虫分布式爬虫管理平台,支持多种编程语言以及多种爬虫框架. + +[查看演示 Demo](http://139.129.230.98:8080) + +[English Documentation](https://github.com/tikazyq/crawlab/blob/master/README.md) + +## 要求 +- Python3 +- MongoDB +- Redis + +## 安装 + +```bash +# 安装后台类库 +pip install -r requirements.txt +``` + +```bash +# 安装前台类库 +cd frontend +npm install +``` + +## 配置 + +请更改配置文件`config.py`,配置API和数据库连接. + +## 快速开始 +```bash +# 启动后端API +python app.py + +# 启动Flower服务 +python ./bin/run_flower.py + +# 启动worker +python ./bin/run_worker.py +``` + +```bash +# 运行前端 +cd frontend +npm run serve +``` + +## 截图 + +#### 首页 + + +#### 爬虫列表 + + + +#### 爬虫详情 - 概览 + + + +#### 任务详情 - 抓取结果 + + + +## 架构 + +Crawlab的架构跟Celery非常相似,但是加入了包括前端、爬虫、Flower在内的额外模块,以支持爬虫管理的功能。 + + + +### 节点 + +节点其实就是Celery中的Worker。一个节点运行时会连接到一个任务队列(例如Redis)来接收和运行任务。所有爬虫需要在运行时被部署到节点上,用户在部署前需要定义节点的IP地址和端口。 + +### 爬虫 + +##### 自动发现 + +在`config.py`文件中,修改变量`PROJECT_SOURCE_FILE_FOLDER`作为爬虫项目所在的目录。Crawlab后台程序会自动发现这些爬虫项目并储存到数据库中。是不是很方便? + +##### 部署爬虫 + +所有爬虫需要在抓取前被部署当相应当节点中。在"爬虫详情"页面点击"Deploy"按钮,爬虫将被部署到所有有效到节点中。 + +##### 运行爬虫 + +部署爬虫之后,你可以在"爬虫详情"页面点击"Run"按钮来启动爬虫。一个爬虫任务将被触发,你可以在任务列表页面中看到这个任务。 + +### 任务 + +任务被触发并被节点执行。用户可以在任务详情页面中看到任务到状态、日志和抓取结果。 + +### 后台应用 + +这是一个Flask应用,提供了必要的API来支持常规操作,例如CRUD、爬虫部署以及任务运行。每一个节点需要启动Flask应用来支持爬虫部署。运行`python manage.py app`或`python ./bin/run_app.py`来启动应用。 + +### 中间者 + +中间者跟Celery中定义的一样,作为运行异步任务的队列。 + +### 前端 + +前端其实就是一个基于[Vue-Element-Admin](https://github.com/PanJiaChen/vue-element-admin)的单页应用。其中重用了很多Element-UI的控件来支持相应的展示。 + +## 与其他框架的集成 + +任务是利用python的`subprocess`模块中的`Popen`来实现的。任务ID将以环境变量`CRAWLAB_TASK_ID`的形式存在于爬虫任务运行的进程中,并以此来关联抓取数据。 + +在你的爬虫程序中,你需要将`CRAWLAB_TASK_ID`的值以`task_id`作为可以存入数据库中。这样Crawlab就直到如何将爬虫任务与抓取数据关联起来了。当前,Crawlab只支持MongoDB。 + +### Scrapy + +以下是Crawlab跟Scrapy集成的例子,利用了Crawlab传过来的task_id和collection_name。 + +```python +import os +from pymongo import MongoClient + +MONGO_HOST = '192.168.99.100' +MONGO_PORT = 27017 +MONGO_DB = 'crawlab_test' + +# scrapy example in the pipeline +class JuejinPipeline(object): + mongo = MongoClient(host=MONGO_HOST, port=MONGO_PORT) + db = mongo[MONGO_DB] + col_name = os.environ.get('CRAWLAB_COLLECTION') + if not col_name: + col_name = 'test' + col = db[col_name] + + def process_item(self, item, spider): + item['task_id'] = os.environ.get('CRAWLAB_TASK_ID') + self.col.save(item) + return item +``` + +## 与其他框架比较 + +限制以及有一些爬虫管理框架了,因此为啥还要用Crawlab? + +因为很多现有当平台都依赖于Scrapyd,限制了爬虫的编程语言以及框架,爬虫工程师只能用scrapy和python。当然,scrapy是非常优秀的爬虫框架,但是它不能做一切事情。 + +Crawlab使用起来很方便,也很通用,可以适用于几乎任何主流语言和框架。它还有一个精美的前端界面,让用户可以方便的管理和运行爬虫。 + +|框架 | 类型 | 分布式 | 前端 | 依赖于Scrapyd | +|:---:|:---:|:---:|:---:|:---:| +| [Crawlab](https://github.com/tikazyq/crawlab) | 管理平台 | Y | Y | N +| [Gerapy](https://github.com/Gerapy/Gerapy) | 管理平台 | Y | Y | Y +| [SpiderKeeper](https://github.com/DormyMo/SpiderKeeper) | 管理平台 | Y | Y | Y +| [ScrapydWeb](https://github.com/my8100/scrapydweb) | 管理平台 | Y | Y | Y +| [Scrapyd](https://github.com/scrapy/scrapyd) | 网络服务 | Y | N | N/A + +## TODOs +##### 后端 +- [ ] 文件管理 +- [ ] MySQL数据库支持 +- [ ] 重跑任务 +- [ ] 节点监控 +- [ ] 更多爬虫例子 + +##### 前端 +- [ ] 任务数据统计 +- [ ] 表格过滤 +- [x] 多语言支持 (中文) +- [ ] 登录和用户管理 +- [ ] 全局搜索 diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 00000000..9cbc8dec --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,18 @@ +# Summary + +* [简介](README.md) +* [快速开始](QuickStart/README.md) + * [安装](QuickStart/Installation.md) + * [运行](QuickStart/Run.md) +* [概念](Concept/README.md) + * [节点](Concept/Node.md) + * [爬虫](Concept/Spider.md) + * [任务](Concept/Task.md) + * [部署](Concept/Deploy.md) +* [架构](Architecture/README.md) + * [Celery](Architecture/Celery.md) + * [App](Architecture/App.md) +* [Examples](Examples/README.md) + * [与Scrapy集成](Examples/README.md) + * [与Puppeteer集成](Examples/README.md) + diff --git a/docs/_book/Architecture/App.html b/docs/_book/Architecture/App.html new file mode 100644 index 00000000..0123e0e6 --- /dev/null +++ b/docs/_book/Architecture/App.html @@ -0,0 +1,436 @@ + + + +
+ + +在config.py文件中,修改变量PROJECT_SOURCE_FILE_FOLDER作为爬虫项目所在的目录。Crawlab后台程序会自动发现这些爬虫项目并储存到数据库中。是不是很方便?
所有爬虫需要在抓取前被部署当相应当节点中。在"爬虫详情"页面点击"Deploy"按钮,爬虫将被部署到所有有效到节点中。
+部署爬虫之后,你可以在"爬虫详情"页面点击"Run"按钮来启动爬虫。一个爬虫任务将被触发,你可以在任务列表页面中看到这个任务。
+ + +最快安装Crawlab的方式是克隆一份代码到本地
+git clone https://github.com/tikazyq/crawlab
+
+安装类库
+# 安装后台类库
+pip install -r requirements.txt
+
+# 安装前台类库
+cd frontend
+npm install
+
+
+
+ 在运行之前需要对Crawlab进行一些配置,配置文件为config.py。
# project variables
+PROJECT_SOURCE_FILE_FOLDER = '/Users/yeqing/projects/crawlab/spiders' # 爬虫源码根目录
+PROJECT_DEPLOY_FILE_FOLDER = '/var/crawlab' # 爬虫部署根目录
+PROJECT_LOGS_FOLDER = '/var/logs/crawlab' # 日志目录
+PROJECT_TMP_FOLDER = '/tmp' # 临时文件目录
+
+# celery variables
+BROKER_URL = 'redis://192.168.99.100:6379/0' # 中间者URL,连接redis
+CELERY_RESULT_BACKEND = 'mongodb://192.168.99.100:27017/' # CELERY后台URL
+CELERY_MONGODB_BACKEND_SETTINGS = {
+ 'database': 'crawlab_test',
+ 'taskmeta_collection': 'tasks_celery',
+}
+CELERY_TIMEZONE = 'Asia/Shanghai'
+CELERY_ENABLE_UTC = True
+
+# flower variables
+FLOWER_API_ENDPOINT = 'http://localhost:5555/api' # Flower服务地址
+
+# database variables
+MONGO_HOST = '192.168.99.100'
+MONGO_PORT = 27017
+MONGO_DB = 'crawlab_test'
+
+# flask variables
+DEBUG = True
+FLASK_HOST = '127.0.0.1'
+FLASK_PORT = 8000
+
+启动后端API,也就是一个Flask App,可以直接启动,或者用gunicorn代替。
+python app.py
+
+启动Flower服务(抱歉目前集成Flower到App服务中,必须单独启动来获取节点信息,后面的版本不需要这个操作)。
+python ./bin/run_flower.py
+
+启动本地Worker。在其他节点中如果想只是想执行任务的话,只需要启动这一个服务就可以了。
+python ./bin/run_worker.py
+
+启动前端服务器。
+cd ../frontend
+npm run serve
+
+
+
+ ').html(content);
+
+ $link.appendTo($title);
+ $title.appendTo($li);
+ $content.appendTo($li);
+ $li.appendTo($searchList);
+ });
+ }
+
+ function launchSearch(q) {
+ // Add class for loading
+ $body.addClass('with-search');
+ $body.addClass('search-loading');
+
+ // Launch search query
+ throttle(gitbook.search.query(q, 0, MAX_RESULTS)
+ .then(function(results) {
+ displayResults(results);
+ })
+ .always(function() {
+ $body.removeClass('search-loading');
+ }), 1000);
+ }
+
+ function closeSearch() {
+ $body.removeClass('with-search');
+ $bookSearchResults.removeClass('open');
+ }
+
+ function launchSearchFromQueryString() {
+ var q = getParameterByName('q');
+ if (q && q.length > 0) {
+ // Update search input
+ $searchInput.val(q);
+
+ // Launch search
+ launchSearch(q);
+ }
+ }
+
+ function bindSearch() {
+ // Bind DOM
+ $searchInput = $('#book-search-input input');
+ $bookSearchResults = $('#book-search-results');
+ $searchList = $bookSearchResults.find('.search-results-list');
+ $searchTitle = $bookSearchResults.find('.search-results-title');
+ $searchResultsCount = $searchTitle.find('.search-results-count');
+ $searchQuery = $searchTitle.find('.search-query');
+
+ // Launch query based on input content
+ function handleUpdate() {
+ var q = $searchInput.val();
+
+ if (q.length == 0) {
+ closeSearch();
+ }
+ else {
+ launchSearch(q);
+ }
+ }
+
+ // Detect true content change in search input
+ // Workaround for IE < 9
+ var propertyChangeUnbound = false;
+ $searchInput.on('propertychange', function(e) {
+ if (e.originalEvent.propertyName == 'value') {
+ handleUpdate();
+ }
+ });
+
+ // HTML5 (IE9 & others)
+ $searchInput.on('input', function(e) {
+ // Unbind propertychange event for IE9+
+ if (!propertyChangeUnbound) {
+ $(this).unbind('propertychange');
+ propertyChangeUnbound = true;
+ }
+
+ handleUpdate();
+ });
+
+ // Push to history on blur
+ $searchInput.on('blur', function(e) {
+ // Update history state
+ if (usePushState) {
+ var uri = updateQueryString('q', $(this).val());
+ history.pushState({ path: uri }, null, uri);
+ }
+ });
+ }
+
+ gitbook.events.on('page.change', function() {
+ bindSearch();
+ closeSearch();
+
+ // Launch search based on query parameter
+ if (gitbook.search.isInitialized()) {
+ launchSearchFromQueryString();
+ }
+ });
+
+ gitbook.events.on('search.ready', function() {
+ bindSearch();
+
+ // Launch search from query param at start
+ launchSearchFromQueryString();
+ });
+
+ function getParameterByName(name) {
+ var url = window.location.href;
+ name = name.replace(/[\[\]]/g, '\\$&');
+ var regex = new RegExp('[?&]' + name + '(=([^]*)|&|#|$)', 'i'),
+ results = regex.exec(url);
+ if (!results) return null;
+ if (!results[2]) return '';
+ return decodeURIComponent(results[2].replace(/\+/g, ' '));
+ }
+
+ function updateQueryString(key, value) {
+ value = encodeURIComponent(value);
+
+ var url = window.location.href;
+ var re = new RegExp('([?&])' + key + '=.*?(&|#|$)(.*)', 'gi'),
+ hash;
+
+ if (re.test(url)) {
+ if (typeof value !== 'undefined' && value !== null)
+ return url.replace(re, '$1' + key + '=' + value + '$2$3');
+ else {
+ hash = url.split('#');
+ url = hash[0].replace(re, '$1$3').replace(/(&|\?)$/, '');
+ if (typeof hash[1] !== 'undefined' && hash[1] !== null)
+ url += '#' + hash[1];
+ return url;
+ }
+ }
+ else {
+ if (typeof value !== 'undefined' && value !== null) {
+ var separator = url.indexOf('?') !== -1 ? '&' : '?';
+ hash = url.split('#');
+ url = hash[0] + separator + key + '=' + value;
+ if (typeof hash[1] !== 'undefined' && hash[1] !== null)
+ url += '#' + hash[1];
+ return url;
+ }
+ else
+ return url;
+ }
+ }
+});
diff --git a/docs/_book/gitbook/gitbook-plugin-sharing/buttons.js b/docs/_book/gitbook/gitbook-plugin-sharing/buttons.js
new file mode 100644
index 00000000..709a4e4c
--- /dev/null
+++ b/docs/_book/gitbook/gitbook-plugin-sharing/buttons.js
@@ -0,0 +1,90 @@
+require(['gitbook', 'jquery'], function(gitbook, $) {
+ var SITES = {
+ 'facebook': {
+ 'label': 'Facebook',
+ 'icon': 'fa fa-facebook',
+ 'onClick': function(e) {
+ e.preventDefault();
+ window.open('http://www.facebook.com/sharer/sharer.php?s=100&p[url]='+encodeURIComponent(location.href));
+ }
+ },
+ 'twitter': {
+ 'label': 'Twitter',
+ 'icon': 'fa fa-twitter',
+ 'onClick': function(e) {
+ e.preventDefault();
+ window.open('http://twitter.com/home?status='+encodeURIComponent(document.title+' '+location.href));
+ }
+ },
+ 'google': {
+ 'label': 'Google+',
+ 'icon': 'fa fa-google-plus',
+ 'onClick': function(e) {
+ e.preventDefault();
+ window.open('https://plus.google.com/share?url='+encodeURIComponent(location.href));
+ }
+ },
+ 'weibo': {
+ 'label': 'Weibo',
+ 'icon': 'fa fa-weibo',
+ 'onClick': function(e) {
+ e.preventDefault();
+ window.open('http://service.weibo.com/share/share.php?content=utf-8&url='+encodeURIComponent(location.href)+'&title='+encodeURIComponent(document.title));
+ }
+ },
+ 'instapaper': {
+ 'label': 'Instapaper',
+ 'icon': 'fa fa-instapaper',
+ 'onClick': function(e) {
+ e.preventDefault();
+ window.open('http://www.instapaper.com/text?u='+encodeURIComponent(location.href));
+ }
+ },
+ 'vk': {
+ 'label': 'VK',
+ 'icon': 'fa fa-vk',
+ 'onClick': function(e) {
+ e.preventDefault();
+ window.open('http://vkontakte.ru/share.php?url='+encodeURIComponent(location.href));
+ }
+ }
+ };
+
+
+
+ gitbook.events.bind('start', function(e, config) {
+ var opts = config.sharing;
+
+ // Create dropdown menu
+ var menu = $.map(opts.all, function(id) {
+ var site = SITES[id];
+
+ return {
+ text: site.label,
+ onClick: site.onClick
+ };
+ });
+
+ // Create main button with dropdown
+ if (menu.length > 0) {
+ gitbook.toolbar.createButton({
+ icon: 'fa fa-share-alt',
+ label: 'Share',
+ position: 'right',
+ dropdown: [menu]
+ });
+ }
+
+ // Direct actions to share
+ $.each(SITES, function(sideId, site) {
+ if (!opts[sideId]) return;
+
+ gitbook.toolbar.createButton({
+ icon: site.icon,
+ label: site.text,
+ position: 'right',
+ onClick: site.onClick
+ });
+ });
+ });
+});
diff --git a/docs/_book/gitbook/gitbook.js b/docs/_book/gitbook/gitbook.js
new file mode 100644
index 00000000..13077b45
--- /dev/null
+++ b/docs/_book/gitbook/gitbook.js
@@ -0,0 +1,4 @@
+!function e(t,n,r){function o(s,a){if(!n[s]){if(!t[s]){var u="function"==typeof require&&require;if(!a&&u)return u(s,!0);if(i)return i(s,!0);var c=new Error("Cannot find module '"+s+"'");throw c.code="MODULE_NOT_FOUND",c}var l=n[s]={exports:{}};t[s][0].call(l.exports,function(e){var n=t[s][1][e];return o(n?n:e)},l,l.exports,e,t,n,r)}return n[s].exports}for(var i="function"==typeof require&&require,s=0;s =0&&n-1)o&&o.push(i);else if(c=de.contains(i.ownerDocument,i),s=v(f.appendChild(i),"script"),c&&y(s),n)for(l=0;i=s[l++];)Ve.test(i.type||"")&&n.push(i);return f}function b(){return!0}function w(){return!1}function T(){try{return te.activeElement}catch(e){}}function C(e,t,n,r,o,i){var s,a;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(a in t)C(e,a,n,r,t[a],i);return e}if(null==r&&null==o?(o=n,r=n=void 0):null==o&&("string"==typeof n?(o=r,r=void 0):(o=r,r=n,n=void 0)),o===!1)o=w;else if(!o)return e;return 1===i&&(s=o,o=function(e){return de().off(e),s.apply(this,arguments)},o.guid=s.guid||(s.guid=de.guid++)),e.each(function(){de.event.add(this,t,o,r,n)})}function j(e,t){return de.nodeName(e,"table")&&de.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e:e}function k(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function E(e){var t=rt.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function S(e,t){var n,r,o,i,s,a,u,c;if(1===t.nodeType){if(Fe.hasData(e)&&(i=Fe.access(e),s=Fe.set(t,i),c=i.events)){delete s.handle,s.events={};for(o in c)for(n=0,r=c[o].length;n=s&&(r!==u&&(c=void 0,l=[e]),n.rejectWith(c,l))}};t?p():(de.Deferred.getStackHook&&(p.stackTrace=de.Deferred.getStackHook()),e.setTimeout(p))}}var s=0;return de.Deferred(function(e){n[0][3].add(i(0,e,de.isFunction(o)?o:a,e.notifyWith)),n[1][3].add(i(0,e,de.isFunction(t)?t:a)),n[2][3].add(i(0,e,de.isFunction(r)?r:u))}).promise()},promise:function(e){return null!=e?de.extend(e,o):o}},i={};return de.each(n,function(e,t){var s=t[2],a=t[5];o[t[1]]=s.add,a&&s.add(function(){r=a},n[3-e][2].disable,n[0][2].lock),s.add(t[3].fire),i[t[0]]=function(){return i[t[0]+"With"](this===i?void 0:this,arguments),this},i[t[0]+"With"]=s.fireWith}),o.promise(i),t&&t.call(i,i),i},when:function(e){var t=arguments.length,n=t,r=Array(n),o=re.call(arguments),i=de.Deferred(),s=function(e){return function(n){r[e]=this,o[e]=arguments.length>1?re.call(arguments):n,--t||i.resolveWith(r,o)}};if(t<=1&&(c(e,i.done(s(n)).resolve,i.reject),"pending"===i.state()||de.isFunction(o[n]&&o[n].then)))return i.then();for(;n--;)c(o[n],s(n),i.reject);return i.promise()}});var De=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;de.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&De.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},de.readyException=function(t){e.setTimeout(function(){throw t})};var Oe=de.Deferred();de.fn.ready=function(e){return Oe.then(e).catch(function(e){de.readyException(e)}),this},de.extend({isReady:!1,readyWait:1,holdReady:function(e){e?de.readyWait++:de.ready(!0)},ready:function(e){(e===!0?--de.readyWait:de.isReady)||(de.isReady=!0,e!==!0&&--de.readyWait>0||Oe.resolveWith(te,[de]))}}),de.ready.then=Oe.then,"complete"===te.readyState||"loading"!==te.readyState&&!te.documentElement.doScroll?e.setTimeout(de.ready):(te.addEventListener("DOMContentLoaded",l),e.addEventListener("load",l));var Le=function(e,t,n,r,o,i,s){var a=0,u=e.length,c=null==n;if("object"===de.type(n)){o=!0;for(a in n)Le(e,t,a,n[a],!0,i,s)}else if(void 0!==r&&(o=!0,de.isFunction(r)||(s=!0),c&&(s?(t.call(e,r),t=null):(c=t,t=function(e,t,n){return c.call(de(e),n)})),t))for(;a1,null,!0)},removeData:function(e){return this.each(function(){Re.remove(this,e)})}}),de.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=Fe.get(e,t),n&&(!r||de.isArray(n)?r=Fe.access(e,t,de.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=de.queue(e,t),r=n.length,o=n.shift(),i=de._queueHooks(e,t),s=function(){de.dequeue(e,t)};"inprogress"===o&&(o=n.shift(),r--),o&&("fx"===t&&n.unshift("inprogress"),delete i.stop,o.call(e,s,i)),!r&&i&&i.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return Fe.get(e,n)||Fe.access(e,n,{empty:de.Callbacks("once memory").add(function(){Fe.remove(e,[t+"queue",n])})})}}),de.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length","
"],col:[2,"
"],tr:[2,"","
"],td:[3,"
"],_default:[0,"",""]};Ge.optgroup=Ge.option,Ge.tbody=Ge.tfoot=Ge.colgroup=Ge.caption=Ge.thead,Ge.th=Ge.td;var Ye=/<|?\w+;/;!function(){var e=te.createDocumentFragment(),t=e.appendChild(te.createElement("div")),n=te.createElement("input");n.setAttribute("type","radio"),n.setAttribute("checked","checked"),n.setAttribute("name","t"),t.appendChild(n),pe.checkClone=t.cloneNode(!0).cloneNode(!0).lastChild.checked,t.innerHTML="",pe.noCloneChecked=!!t.cloneNode(!0).lastChild.defaultValue}();var Qe=te.documentElement,Je=/^key/,Ke=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ze=/^([^.]*)(?:\.(.+)|)/;de.event={global:{},add:function(e,t,n,r,o){var i,s,a,u,c,l,f,p,h,d,g,m=Fe.get(e);if(m)for(n.handler&&(i=n,n=i.handler,o=i.selector),o&&de.find.matchesSelector(Qe,o),n.guid||(n.guid=de.guid++),(u=m.events)||(u=m.events={}),(s=m.handle)||(s=m.handle=function(t){return"undefined"!=typeof de&&de.event.triggered!==t.type?de.event.dispatch.apply(e,arguments):void 0}),t=(t||"").match(qe)||[""],c=t.length;c--;)a=Ze.exec(t[c])||[],h=g=a[1],d=(a[2]||"").split(".").sort(),h&&(f=de.event.special[h]||{},h=(o?f.delegateType:f.bindType)||h,f=de.event.special[h]||{},l=de.extend({type:h,origType:g,data:r,handler:n,guid:n.guid,selector:o,needsContext:o&&de.expr.match.needsContext.test(o),namespace:d.join(".")},i),(p=u[h])||(p=u[h]=[],p.delegateCount=0,f.setup&&f.setup.call(e,r,d,s)!==!1||e.addEventListener&&e.addEventListener(h,s)),f.add&&(f.add.call(e,l),l.handler.guid||(l.handler.guid=n.guid)),o?p.splice(p.delegateCount++,0,l):p.push(l),de.event.global[h]=!0)},remove:function(e,t,n,r,o){var i,s,a,u,c,l,f,p,h,d,g,m=Fe.hasData(e)&&Fe.get(e);if(m&&(u=m.events)){for(t=(t||"").match(qe)||[""],c=t.length;c--;)if(a=Ze.exec(t[c])||[],h=g=a[1],d=(a[2]||"").split(".").sort(),h){for(f=de.event.special[h]||{},h=(r?f.delegateType:f.bindType)||h,p=u[h]||[],a=a[2]&&new RegExp("(^|\\.)"+d.join("\\.(?:.*\\.|)")+"(\\.|$)"),s=i=p.length;i--;)l=p[i],!o&&g!==l.origType||n&&n.guid!==l.guid||a&&!a.test(l.namespace)||r&&r!==l.selector&&("**"!==r||!l.selector)||(p.splice(i,1),l.selector&&p.delegateCount--,f.remove&&f.remove.call(e,l));s&&!p.length&&(f.teardown&&f.teardown.call(e,d,m.handle)!==!1||de.removeEvent(e,h,m.handle),delete u[h])}else for(h in u)de.event.remove(e,h+t[c],n,r,!0);de.isEmptyObject(u)&&Fe.remove(e,"handle events")}},dispatch:function(e){var t,n,r,o,i,s,a=de.event.fix(e),u=new Array(arguments.length),c=(Fe.get(this,"events")||{})[a.type]||[],l=de.event.special[a.type]||{};for(u[0]=a,t=1;t","