Перенос в микроприложение

This commit is contained in:
RedHood 2020-09-16 03:14:44 +10:00
commit dc5b301adf
9 changed files with 1586 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
vendor/
dumps/
runtime/

22
README.md Normal file
View File

@ -0,0 +1,22 @@
# YII Dumper
***
Parsing HTML pages at a given depth
```sh
$ yii dump https://domain.zone/foo $DEPTH $BUFFER $FORCE $EXTERNAL $ANOTHER_PATH
```
- The larger the buffer size, the faster the program runs
- Logs are stored in the directory /runtime/logs
## Sample
```sh
$ yii dump https://domain.zone/foo 2 50
```
## Requirements
* **PHP ^7.2**
* **CURL**
## TODO
1. $searchExternal to $depthExterntal
2. Fix processing of the link: zakupki.gov.ru/data/common-info.html?regNumber=0816500000619001511
3. The construction <base> needs to be modified to check an existing tag

15
composer.json Normal file
View File

@ -0,0 +1,15 @@
{
"require": {
"php": "^7.2",
"ext-curl": "*",
"yiisoft/yii2": "~2.0.0",
"linslin/yii2-curl": "*",
"omnilight/yii2-scheduling": "*"
},
"repositories": [
{
"type": "composer",
"url": "https://asset-packagist.org"
}
]
}

641
composer.lock generated Normal file
View File

@ -0,0 +1,641 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6556bad2e96cc66bf6aed5a9fdd8cd57",
"packages": [
{
"name": "bower-asset/inputmask",
"version": "3.3.11",
"source": {
"type": "git",
"url": "https://github.com/RobinHerbots/Inputmask.git",
"reference": "5e670ad62f50c738388d4dcec78d2888505ad77b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/RobinHerbots/Inputmask/zipball/5e670ad62f50c738388d4dcec78d2888505ad77b",
"reference": "5e670ad62f50c738388d4dcec78d2888505ad77b"
},
"require": {
"bower-asset/jquery": ">=1.7"
},
"type": "bower-asset",
"license": [
"http://opensource.org/licenses/mit-license.php"
]
},
{
"name": "bower-asset/jquery",
"version": "3.5.1",
"source": {
"type": "git",
"url": "git@github.com:jquery/jquery-dist.git",
"reference": "4c0e4becb8263bb5b3e6dadc448d8e7305ef8215"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jquery/jquery-dist/zipball/4c0e4becb8263bb5b3e6dadc448d8e7305ef8215",
"reference": "4c0e4becb8263bb5b3e6dadc448d8e7305ef8215"
},
"type": "bower-asset",
"license": [
"MIT"
]
},
{
"name": "bower-asset/punycode",
"version": "v1.3.2",
"source": {
"type": "git",
"url": "git@github.com:bestiejs/punycode.js.git",
"reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bestiejs/punycode.js/zipball/38c8d3131a82567bfef18da09f7f4db68c84f8a3",
"reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3"
},
"type": "bower-asset"
},
{
"name": "bower-asset/yii2-pjax",
"version": "2.0.7.1",
"source": {
"type": "git",
"url": "https://github.com/yiisoft/jquery-pjax.git",
"reference": "aef7b953107264f00234902a3880eb50dafc48be"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/aef7b953107264f00234902a3880eb50dafc48be",
"reference": "aef7b953107264f00234902a3880eb50dafc48be"
},
"require": {
"bower-asset/jquery": ">=1.8"
},
"type": "bower-asset",
"license": [
"MIT"
]
},
{
"name": "cebe/markdown",
"version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/cebe/markdown.git",
"reference": "9bac5e971dd391e2802dca5400bbeacbaea9eb86"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cebe/markdown/zipball/9bac5e971dd391e2802dca5400bbeacbaea9eb86",
"reference": "9bac5e971dd391e2802dca5400bbeacbaea9eb86",
"shasum": ""
},
"require": {
"lib-pcre": "*",
"php": ">=5.4.0"
},
"require-dev": {
"cebe/indent": "*",
"facebook/xhprof": "*@dev",
"phpunit/phpunit": "4.1.*"
},
"bin": [
"bin/markdown"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2.x-dev"
}
},
"autoload": {
"psr-4": {
"cebe\\markdown\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Carsten Brandt",
"email": "mail@cebe.cc",
"homepage": "http://cebe.cc/",
"role": "Creator"
}
],
"description": "A super fast, highly extensible markdown parser for PHP",
"homepage": "https://github.com/cebe/markdown#readme",
"keywords": [
"extensible",
"fast",
"gfm",
"markdown",
"markdown-extra"
],
"time": "2018-03-26T11:24:36+00:00"
},
{
"name": "dragonmantank/cron-expression",
"version": "v1.2.1",
"source": {
"type": "git",
"url": "https://github.com/dragonmantank/cron-expression.git",
"reference": "9504fa9ea681b586028adaaa0877db4aecf32bad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/9504fa9ea681b586028adaaa0877db4aecf32bad",
"reference": "9504fa9ea681b586028adaaa0877db4aecf32bad",
"shasum": ""
},
"require": {
"php": ">=5.3.2"
},
"require-dev": {
"phpunit/phpunit": "~4.0|~5.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Cron\\": "src/Cron/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
"keywords": [
"cron",
"schedule"
],
"time": "2017-01-23T04:29:33+00:00"
},
{
"name": "ezyang/htmlpurifier",
"version": "v4.13.0",
"source": {
"type": "git",
"url": "https://github.com/ezyang/htmlpurifier.git",
"reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/08e27c97e4c6ed02f37c5b2b20488046c8d90d75",
"reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75",
"shasum": ""
},
"require": {
"php": ">=5.2"
},
"require-dev": {
"simpletest/simpletest": "dev-master#72de02a7b80c6bb8864ef9bf66d41d2f58f826bd"
},
"type": "library",
"autoload": {
"psr-0": {
"HTMLPurifier": "library/"
},
"files": [
"library/HTMLPurifier.composer.php"
],
"exclude-from-classmap": [
"/library/HTMLPurifier/Language/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "Edward Z. Yang",
"email": "admin@htmlpurifier.org",
"homepage": "http://ezyang.com"
}
],
"description": "Standards compliant HTML filter written in PHP",
"homepage": "http://htmlpurifier.org/",
"keywords": [
"html"
],
"time": "2020-06-29T00:56:53+00:00"
},
{
"name": "linslin/yii2-curl",
"version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/linslin/Yii2-Curl.git",
"reference": "38f2c28efb4c200b7ad1c2decb262806bb6b5cea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/linslin/Yii2-Curl/zipball/38f2c28efb4c200b7ad1c2decb262806bb6b5cea",
"reference": "38f2c28efb4c200b7ad1c2decb262806bb6b5cea",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"yidas/yii2-bower-asset": "^2.0.13.1",
"yiisoft/yii2": "^2.0.0"
},
"require-dev": {
"codeception/base": "^2.2.3",
"codeception/specify": "~0.4.3",
"codeception/verify": "~0.3.1",
"codeclimate/php-test-reporter": "dev-master",
"guzzlehttp/guzzle": ">=4.1.4 <7.0",
"mcustiel/phiremock-codeception-extension": "1.2.4"
},
"type": "yii2-extension",
"autoload": {
"psr-4": {
"linslin\\yii2\\curl\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nils Gajsek",
"email": "info@linslin.org"
}
],
"description": "Easy and nice cURL extension with RESTful support for Yii2",
"keywords": [
" curl",
"extension",
"restful",
"yii2"
],
"time": "2020-05-03T11:44:34+00:00"
},
{
"name": "omnilight/yii2-scheduling",
"version": "1.1.4",
"source": {
"type": "git",
"url": "https://github.com/omnilight/yii2-scheduling.git",
"reference": "c7ffcd3b26143d47455ddd8e0cdf1611dbd9b74c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/omnilight/yii2-scheduling/zipball/c7ffcd3b26143d47455ddd8e0cdf1611dbd9b74c",
"reference": "c7ffcd3b26143d47455ddd8e0cdf1611dbd9b74c",
"shasum": ""
},
"require": {
"dragonmantank/cron-expression": "1.*",
"php": ">=5.4.0",
"symfony/process": "2.6.* || 3.* || 4.*",
"yiisoft/yii2": "2.0.*"
},
"require-dev": {
"phpunit/phpunit": "4.8.36"
},
"suggest": {
"guzzlehttp/guzzle": "Required to use the thenPing method on schedules (~5.0)."
},
"type": "yii2-extension",
"extra": {
"bootstrap": "omnilight\\scheduling\\Bootstrap"
},
"autoload": {
"psr-4": {
"omnilight\\scheduling\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"authors": [
{
"name": "Pavel Agalecky",
"email": "pavel.agalecky@gmail.com"
}
],
"description": "Scheduling extension for Yii2 framework",
"keywords": [
"cron",
"scheduling",
"yii"
],
"time": "2020-03-31T21:02:32+00:00"
},
{
"name": "symfony/process",
"version": "v4.4.13",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "65e70bab62f3da7089a8d4591fb23fbacacb3479"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/65e70bab62f3da7089a8d4591fb23fbacacb3479",
"reference": "65e70bab62f3da7089a8d4591fb23fbacacb3479",
"shasum": ""
},
"require": {
"php": ">=7.1.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.4-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Process\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Process Component",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-07-23T08:31:43+00:00"
},
{
"name": "yidas/yii2-bower-asset",
"version": "2.0.13.1",
"source": {
"type": "git",
"url": "https://github.com/yidas/yii2-bower-asset.git",
"reference": "056dd55087e0b945c01c91c99eb346ef6e28a42e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/yidas/yii2-bower-asset/zipball/056dd55087e0b945c01c91c99eb346ef6e28a42e",
"reference": "056dd55087e0b945c01c91c99eb346ef6e28a42e",
"shasum": ""
},
"provide": {
"bower-asset/bootstrap": "*",
"bower-asset/inputmask": "*",
"bower-asset/jquery": "*",
"bower-asset/punycode": "*",
"bower-asset/typeahead.js": "*",
"bower-asset/yii2-pjax": "*"
},
"type": "yii2-extension",
"autoload": {
"psr-4": {
"yidas\\yii2BowerAsset\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Bower Assets for Yii 2 app provided via Composer repository",
"keywords": [
"bower",
"bower asset",
"framework",
"yii2"
],
"time": "2019-09-19T11:33:03+00:00"
},
{
"name": "yiisoft/yii2",
"version": "2.0.38",
"source": {
"type": "git",
"url": "https://github.com/yiisoft/yii2-framework.git",
"reference": "fd01e747cc66a049ec105048f0ab8dfbdf60bf4b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/yiisoft/yii2-framework/zipball/fd01e747cc66a049ec105048f0ab8dfbdf60bf4b",
"reference": "fd01e747cc66a049ec105048f0ab8dfbdf60bf4b",
"shasum": ""
},
"require": {
"bower-asset/inputmask": "~3.2.2 | ~3.3.5",
"bower-asset/jquery": "3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable",
"bower-asset/punycode": "1.3.*",
"bower-asset/yii2-pjax": "~2.0.1",
"cebe/markdown": "~1.0.0 | ~1.1.0 | ~1.2.0",
"ext-ctype": "*",
"ext-mbstring": "*",
"ezyang/htmlpurifier": "~4.6",
"lib-pcre": "*",
"php": ">=5.4.0",
"yiisoft/yii2-composer": "~2.0.4"
},
"bin": [
"yii"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"yii\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Qiang Xue",
"email": "qiang.xue@gmail.com",
"homepage": "http://www.yiiframework.com/",
"role": "Founder and project lead"
},
{
"name": "Alexander Makarov",
"email": "sam@rmcreative.ru",
"homepage": "http://rmcreative.ru/",
"role": "Core framework development"
},
{
"name": "Maurizio Domba",
"homepage": "http://mdomba.info/",
"role": "Core framework development"
},
{
"name": "Carsten Brandt",
"email": "mail@cebe.cc",
"homepage": "http://cebe.cc/",
"role": "Core framework development"
},
{
"name": "Timur Ruziev",
"email": "resurtm@gmail.com",
"homepage": "http://resurtm.com/",
"role": "Core framework development"
},
{
"name": "Paul Klimov",
"email": "klimov.paul@gmail.com",
"role": "Core framework development"
},
{
"name": "Dmitry Naumenko",
"email": "d.naumenko.a@gmail.com",
"role": "Core framework development"
},
{
"name": "Boudewijn Vahrmeijer",
"email": "info@dynasource.eu",
"homepage": "http://dynasource.eu",
"role": "Core framework development"
}
],
"description": "Yii PHP Framework Version 2",
"homepage": "http://www.yiiframework.com/",
"keywords": [
"framework",
"yii2"
],
"funding": [
{
"url": "https://github.com/yiisoft",
"type": "github"
},
{
"url": "https://opencollective.com/yiisoft",
"type": "open_collective"
},
{
"url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2",
"type": "tidelift"
}
],
"time": "2020-09-14T21:52:10+00:00"
},
{
"name": "yiisoft/yii2-composer",
"version": "2.0.10",
"source": {
"type": "git",
"url": "https://github.com/yiisoft/yii2-composer.git",
"reference": "94bb3f66e779e2774f8776d6e1bdeab402940510"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/yiisoft/yii2-composer/zipball/94bb3f66e779e2774f8776d6e1bdeab402940510",
"reference": "94bb3f66e779e2774f8776d6e1bdeab402940510",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.0 | ^2.0"
},
"require-dev": {
"composer/composer": "^1.0 | ^2.0@dev",
"phpunit/phpunit": "<7"
},
"type": "composer-plugin",
"extra": {
"class": "yii\\composer\\Plugin",
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"yii\\composer\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Qiang Xue",
"email": "qiang.xue@gmail.com"
},
{
"name": "Carsten Brandt",
"email": "mail@cebe.cc"
}
],
"description": "The composer plugin for Yii extension installer",
"keywords": [
"composer",
"extension installer",
"yii2"
],
"funding": [
{
"url": "https://github.com/yiisoft",
"type": "github"
},
{
"url": "https://opencollective.com/yiisoft",
"type": "open_collective"
},
{
"url": "https://tidelift.com/funding/github/packagist/yiisoft/yii2-composer",
"type": "tidelift"
}
],
"time": "2020-06-24T00:04:01+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "^7.2",
"ext-curl": "*"
},
"platform-dev": [],
"plugin-api-version": "1.1.0"
}

18
config/console.php Normal file
View File

@ -0,0 +1,18 @@
<?php
return [
'id' => 'Dumper',
'controllerNamespace' => 'app\controllers',
'basePath' => dirname(__DIR__),
'params' => [
'basePath' => dirname(__DIR__) . '/dumps',
'pagesPath' => '/pages',
'docsPath' => '/data',
'imgPath' => '/img',
'cssPath' => '/css',
'jsPath' => '/js',
'externalLinksPath' => '/sites',
'timezone' => 'Asia/Vladivostok',
'useragent' => 'Dumper',
'regBlackList' => '(instagram|whatsapp|appdv)'
]
];

6
config/schedule.php Normal file
View File

@ -0,0 +1,6 @@
<?php
/**
* @var \omnilight\scheduling\Schedule $schedule
*/
$schedule->command('dump http://expertise.27pro.ru// 5 50')->dailyAt('03:00');

View File

@ -0,0 +1,16 @@
<?php
namespace app\controllers;
use \Yii;
use \app\models\Dump;
class DumpController extends \yii\console\Controller
{
public function actionIndex($link, $depth = 0, $buffer = 0, $force = false, $searchExternal = false, $path = '')
{
(new Dump)->download($link, $depth, $buffer, $force, $searchExternal, $path);
return 0;
}
}

851
models/Dump.php Normal file
View File

@ -0,0 +1,851 @@
<?php
namespace app\models;
use \Exception;
use \DateTime;
use \Yii;
use \linslin\yii2\curl\Curl;
/**
* Модель для выгрузки страниц в автономное хранилище
*
* Позволяет скачивать, сохранять и оптимизировать под нужную архитектуру
*
* @method download(string $link, int $depth = 0, int $buffer = 0, bool $searchExternal = false) Скачивает страницу
* @method save() Сохраняет скачанные страницы на диск
* @method saveStatistics() Сохранение статистики
* @method convertLinks(array $links = null) Конвертер ссылок
* @method convertFiles(array $files = null) Конвертер страниц
* @method filterLink(string $link) Фильтрация ссылки
*
* @author Арсен Мирзаев red@hood.su
*
* @todo
* 1. $searchExternal переделать в $depthExterntal
* 2. Исправить обработку ссылки: zakupki.gov.ru/data/common-info.html?regNumber=0816500000619001511
* 4. Конструкцию <base> надо доработать на проверку уже существующего тега
*/
class Dump extends \yii\base\Component
{
/**
* Глубина поиска страниц относительно первичной
*/
protected $depth = 0;
/**
* Буфер страниц для скачивания
*/
protected $buffer;
/**
* Флаг форсированного выполнения (перезаписи файлов)
*/
protected $force;
/**
* Путь для сохранения файлов
*/
protected $path;
/**
* Буфер страниц для скачивания
*/
protected $searchExternal;
/**
* Регистр обработанных ссылок
*
* 'Ссылка' => [
* [0] => 'URN файла'
* [1] => 'URL файла'
* [2] => 'URI файла для конвертации страниц'
* ]
*/
protected $links = [];
/**
* Буфер скачанных файлов
*
* 'Файл (URN)' => [
* [0] => 'Тип'
* [1] => 'Данные'
* ]
*
* Типы:
* [0] - HTML страница
* [1] - документ ('.css', '.js', '.png'...)
*/
protected $filesBuffer = [];
/**
* Регистр сохранённых файлов
*
* 'Файл (URN)' => [
* [0] => 'Тип'
* [1] => 'Данные'
* ]
*
* Типы:
* [0] - HTML страница
* [1] - документ ('.css', '.js', '.png'...)
*/
protected $files = [];
/**
* Блокировка циклов
*
* Указывает работает основное скачивание или рекурсивное
*/
protected $subdownload = false;
/**
* Количество новых найденных ссылок
*/
protected $linksNew = 0;
/**
* Запрашиваемая ссылка
*/
protected $target;
/**
* SCHEME/PROTOCOL запроса
*/
public $connectionProtocol;
/**
* HOST запроса
*/
public $connectionHost;
/**
* Собранная информация о выполнении
*
* [0] => 'Запрошенный URI'
* [1] => 'Статус выполнения (завершен или ошибка)'
* [2] => [
* [0] => 'Количество найденных ссылок'
* [1] => 'Количество найденных ссылок без дубликатов'
* [2] => 'Количество обработанных ссылок'
* ]
* [3] => [
* [0] => 'Количество найденных HTML страниц'
* [1] => 'Количество конвертированных страниц'
* ]
* [4] => [
* [0] => 'Количество найденных документов (.png, .css, .pdf)'
* [1] => 'удалено'
* [2] => 'Найдено: Изображения',
* [3] => 'Найдено: Видеозаписи',
* [4] => 'Найдено: Аудиозаписи',
* [5] => 'Найдено: CSS',
* [6] => 'Найдено: JS',
* [7] => 'Найдено: Не опознано',
* ]
*/
public $statistics = [
'',
1,
[
0,
0,
0
],
[
0,
0,
0
],
[
0,
0,
0,
0,
0,
0,
0,
0,
]
];
/**
* Время начала выполнения скрипта
*
* Используется для вычисления времени выполнения и записи в статистику
*/
public $timeStart;
public function __construct()
{
// Начало отсчёта синтетического теста времени выполнеия для записи в статистику
$this->timeStart = microtime(true);
// if (YII_DEBUG) {
register_shutdown_function(array(&$this, 'saveStatistics'));
// }
}
/**
* Скачивание страницы
*
* @param string $link Ссылка
* @param int $depth Глубина скачивания вложенных ссылок
* @param int $buffer Буфер файлов
* @param bool $force Флаг форсированного выполнения (с перезаписью существующих файлов)
* @param string $path Свой путь для сохранения
* @param bool $searchExternal Флаг поиска ссылок во внешних сайтах
*
* @return Dump
*/
public function download($link, $depth = 0, $buffer = 0, $force = false, $searchExternal = false, $path = '')
{
if (!$link = $this->filterLink($link)) {
// Если ссылка не прошла фильтрацию
return;
}
// Инициализация свойств
if (!isset($this->buffer)) {
$this->buffer = $buffer;
}
if (!isset($this->force)) {
$this->force = $force;
}
if (!isset(Yii::$app->params['basePath'])) {
Yii::$app->params['basePath'] = $path;
}
if (!isset($this->searchExternal)) {
$this->searchExternal = $searchExternal;
}
if (!isset($this->connectionProtocol)) {
// Проверка наличия domain.zone в ссылке на подобие: 'https://domain.zone/foo/bar'
preg_match_all('/(.*)?:?(\/\/|\\\\)(.*)((\/|\\\|$).*$)/U', $link, $linkMatch);
$this->connectionProtocol = $linkMatch[1][0] ?? $_SERVER['REQUEST_SCHEME'] ?? $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? 'http';
}
if (!isset($this->connectionHost)) {
// Проверка наличия domain.zone в ссылке на подобие: 'https://domain.zone/foo/bar'
preg_match_all('/(.*)?:?(\/\/|\\\\)(.*)((\/|\\\|$).*$)/U', $link, $linkMatch);
// Проверка наличия domain.zone в ссылке на подобие: 'domain.zone/' или 'domain.zone' или 'subdomain.domain.zone'
preg_match_all('/^([^\\\|\/|\\s]+\.[^\\\|\/|\\s]+)(\\\|\/)?$/', $link, $domainMatch);
$this->connectionHost = $linkMatch[3][0] ?? $domainMatch[1][0] ?? $_SERVER['REQUEST_HOST'] ?? $_SERVER['HTTP_HOST'] ?? '127.0.0.1';
}
// Обработка буфера (это весь его код)
if (count($this->filesBuffer) >= $this->buffer) {
$this->save();
}
// Если скачивание является первым (ручной запрос)
if (!isset($this->target)) {
// Инициализация стартовой ссылки
$this->target = $link;
// Добавление стартовой ссылки в регистр
array_unshift($this->links, $link);
// Инициализация цели в статистике
// if (YII_DEBUG) {
$this->statistics[0] = $this->target;
// }
// Прибавление стартовой ссылки для вывода в статистике
// if (YII_DEBUG) {
// if (!isset($this->statistics[2][1])) {
// $this->statistics[2][1] = 0;
// }
// $this->statistics[2][1]++;
$this->statistics[2][1] = 1;
// }
} else if ($this->subdownload && $depth === 0) {
// Иначе если это дополнительное скачивание и глубина равна нулю
$this->linksNew++;
// Добавление стартовой ссылки в регистр
array_unshift($this->links, $link);
}
// Обработка стартовой ссылки
if (isset($this->target)) {
// Внимание: $targetMatch не очищается и используется в коде ниже
preg_match_all('/(.*)?:?(\/\/|\\\\)(.*)((\/|\\\|$).*$)/U', $this->target, $targetMatch);
}
if (preg_match('/^[^\/|\\\]+\..+$/', $link)) {
// Паттерн: 'domain.zone', 'domain.zone/foo/bar/index.html'
$request = ($this->connectionProtocol ?? 'http') . '://' . $link;
} else if (preg_match_all('/(^http.*(\/\/|\\\\)|^(\/\/|\\\\))(.*)(\/|\\\|$)(.*$)/iU', $link, $match)) {
// Паттерн: 'https://domain.zone/foo/index.html', '//domain.zone/foo'
$request = ($this->connectionProtocol ?? 'http') . '://' . $match[4][0] . '/' . $match[6][0];
} else if (preg_match('/(^\/[^\/\\\\\s]+[^\s]*$|^\\\[^\/\\\\\s]+[^\\s]*$|^\/$|^\\\$)/', $link)) {
// Паттерн: '/', '/foo/bar', '/foo/bar/index.html'
$request = ($this->connectionProtocol ?? 'http') . '://' . ($targetMatch[3][0] ?? $this->connectionHost) . $link;
} else if (preg_match('/(^[^\/\\\\\s\.]+(\/|\\\|$)([^\/\\\\\s]*$|[^\/\\\\\s]+(\/|\\\).*))/', $link)) {
// Паттерн: 'foo/index.html', 'foo/bar/index.html', 'foo', 'foo/'
// Не помню почему регулярное выражение такое сложное, разбираться сейчас не стал, так как главное, что работает
$request = ($this->connectionProtocol ?? 'http') . '://' . ($targetMatch[3][0] ?? $this->connectionHost) . '/' . $link;
} else {
unset($this->links[$link]);
// throw new Exception('Не удалось идентифицировать запрос');
return $this;
}
unset($match); // Очистка на всякий случай, так как переменные остаются
// Выполнение запроса
$this->filesBuffer[$link][1] = (new Curl())->setOption(CURLOPT_RETURNTRANSFER, true)
->setOption(CURLOPT_FOLLOWLOCATION, true)
->setOption(CURLOPT_SSL_VERIFYPEER, true)
->setOption(CURLOPT_USERAGENT, Yii::$app->params['useragent'])
->get($request);
if (preg_match('/(https?:(\/\/|\\\\).+(\/|\\\).+|^(\/|\\\).+)(\.(?!php|htm)[A-z0-9]+)[^\/\\\s]*$/i', $link)) {
// Если ссылка является документом ('.css', '.js', '.png'...)
$this->filesBuffer[$link][0] = 1;
} else {
// Иначе расценивается как HTML страница для продолжения поиска
preg_match_all('/(.*)?:?(\/\/|\\\\)(.*)((\/|\\\|$).*$)/U', $link, $linkMatch);
// Если это внутренний URL или domain.zone цели сходится с domain.zone обрабатываемой ссылки или есть разрешение на проход внешних ссылок
if (empty($linkMatch[3][0]) || $targetMatch[3][0] === $linkMatch[3][0] || $this->searchExternal === true) {
$this->filesBuffer[$link][0] = 0;
$file = $this->filesBuffer[$link][1];
} else {
$this->filesBuffer[$link][0] = 0;
}
}
unset($targetMatch, $linkMatch, $link); // Очистка на всякий случай, так как переменные остаются
// Извлечение ссылок из страницы по свойствам href='' и src=''
// Единственное место где добавляются найденные ссылки
if (!empty($file) && $depth > 0 && preg_match_all('/(href|src)\\s?=\\s*[\"\']?((?!(\"|\'))(?!tel)(?!mailto)[^\"\']+)[\"\']/i', $file, $match)) {
// Если файл скачан, глубина больше нуля и были найдены ссылки
// Прибавление количества новых ссылок для обработки
$this->linksNew += count($match[2]);
// Прибавление количества новых ссылок для вывода в статистике
// if (YII_DEBUG) {
$this->statistics[2][0] += count($match[2]);
// }
// Добавление ссылок в общий регистр
foreach ($match[2] as $link) {
if (!$link = $this->filterLink($link)) {
// Если ссылка не прошла фильтрацию
continue;
}
array_unshift($this->links, $link);
}
}
unset($link, $file, $match); // Очистка на всякий случай, так как переменные остаются
// Конвертация ссылок
if (isset($this->links) && $this->linksNew) {
// Если глубина больше ноля, ссылки существуют и счетчик новых ссылок больше ноля
$this->convertLinks();
}
if (!$this->subdownload) {
// Если это не дополнительное скачивание и текущая глубина не равна нулю
// Устанавливается для того, чтобы работать в цикле
$this->depth = $depth;
// Скачиваем найденные страницы по установленной глубине поиска
while ($this->depth-- > 0) {
foreach ($this->links as $link => $type) {
$this->subdownload = true;
$this->download($link, $this->depth);
$this->subdownload = false;
}
$this->convertFiles($this->save(), true);
}
unset($link, $type);
}
// Сохранение остатков ссылок после обработки
$this->save();
// Конвертация сохранённых файлов
$this->convertFiles();
return $this;
}
/**
* Сохранение страницы
*
* Получает на вход файлы из буфера и сохраняет на диске
* На выходе будут перенесены в массив $this->files
*
* @return array
*/
public function save($files = null)
{
if (empty($files)) {
$files = &$this->filesBuffer;
}
if (isset($this->links)) {
$this->convertLinks();
}
$savedFiles = [];
foreach ($files as $link => $file) {
if (!empty($this->links[$link][1]) && !preg_match('/^(\/|\\\)/', $this->links[$link][1])) {
// Если в начале ссылки нет слеша и ссылка не, то добавить слеш
$this->links[$link][1] = '/' . $this->links[$link][1];
}
// if (!isset($this->links[$link])) {
// $this->convertLinks();
// }
// Проверка существования каталога и его создание
if (/** isset($this->links[$link] && */ !file_exists(Yii::$app->params['basePath'] . $this->links[$link][1])) {
if (!mkdir(Yii::$app->params['basePath'] . $this->links[$link][1], 0755, true)) {
// throw new Exception('Не удалось создать каталог');
}
}
// Сохранение файла
if (!file_exists(Yii::$app->params['basePath'] . $this->links[$link][1] . $this->links[$link][0]) || $this->force) {
if (file_put_contents(Yii::$app->params['basePath'] . $this->links[$link][1] . $this->links[$link][0], $file[1])) {
$this->files[$link][0] = $file[0];
$this->files[$link][1] = $file[1];
} else {
// throw new Exception('Не удалось сохранить файл');
}
}
$savedFiles[$link] = $files[$link];
unset($files[$link]);
}
unset($link, $file); // Очистка на всякий случай, так как переменные остаются
// Указание сборщику статистики, что парсер успешно завершил свою работу
// if (YII_DEBUG) {
$this->statistics[1] = 0;
// }
return $savedFiles;
}
/**
* Сохранить статистику
*
* Возвращает статус сохранения (true/false)
*
* @return bool
*/
public function saveStatistics()
{
// Запись времени окончания работы скрипта
$timeFinish = microtime(true);
$i = new DateTime(Yii::$app->params['timezone'] ?? 'Europe/Moscow');
$date = date_format($i, 'Y-m-d');
$dateFull = date_format($i, 'Y.m.d H:i:s');
$request = $this->statistics[0] ?? 'Ошибка';
$time = ($timeFinish - $this->timeStart) ?? 'Ошибка';
$status = $this->statistics[1] === 0 ? 'Успех' : 'Ошибка';
$linksCount = $this->statistics[2][0] ?? 'Ошибка';
$linksProcessed = $this->statistics[2][1] ?? 'Ошибка';
$linksProcessedReal = $this->statistics[2][2] ?? 'Ошибка';
$pagesCount = $this->statistics[3][0] ?? 'Ошибка';
$pagesProcessed = $this->statistics[3][1] ?? 'Ошибка';
$filesCount = $this->statistics[4][0] ?? 'Ошибка';
$imagesCount = $this->statistics[4][2] ?? 'Ошибка';
$videosCount = $this->statistics[4][3] ?? 'Ошибка';
$audiosCount = $this->statistics[4][4] ?? 'Ошибка';
$cssCount = $this->statistics[4][5] ?? 'Ошибка';
$jsCount = $this->statistics[4][6] ?? 'Ошибка';
$unidentifiedCount = $this->statistics[4][7] ?? 'Ошибка';
if (!file_exists(Yii::getAlias('@runtime/logs'))) {
if (!mkdir(Yii::getAlias('@runtime/logs'), 0755, true)) {
// throw new Exception('Не удалось создать каталог');
}
}
$file = fopen(Yii::getAlias('@runtime/logs') . '/' . $date . uniqid('_DUMPER_', true) . '.log', 'a+');
if (!fwrite($file, <<<EOT
///////////////////////////////////////////////////////////
/// Статистика выполнения ///
///////////////////////////////////////////////////////////
Дата: $dateFull
Запрос: $request
Время выполнения: $time секунд
Статус выполнения: $status
///////////////////////////////////////////////////////////
Найдено ссылок: $linksCount
Обработано ссылок: $linksProcessed
Из них без дубликатов: $linksProcessedReal
//////////////////////////////////////////////////////////
Найдено страниц: $pagesCount
Конвертировано страниц: $pagesProcessed
///////////////////////////////////////////////////////////
Найдено документов: $filesCount
Изображения: $imagesCount
Видеозаписи: $videosCount
Аудиозаписи: $audiosCount
CSS скрипты: $cssCount
JS скрипты: $jsCount
Не опознано: $unidentifiedCount
///////////////////////////////////////////////////////////
EOT
)) {
// throw new Exception('Не удалось записать статистику');
}
fclose($file);
unset($i, $date, $dateFull, $file); // Очистка на всякий случай, так как переменные остаются
// return true;
}
/**
* Конвертер ссылок
*
* Приводит ссылки к заданному архитектурой виду
*
* @return array
*/
private function convertLinks($links = null)
{
if (empty($links)) {
$links = &$this->links;
}
while ($this->linksNew >= 0) {
// Инициализация ссылки и копии для будущего поиска в файлах
if (!array_key_exists($this->linksNew, $links)) {
// Подготовка к следующей итерации цикла
$this->linksNew--;
continue;
}
$link = $rawLink = $links[$this->linksNew];
if (is_array($link)) {
// Если это уже конвертированная ссылка
continue;
}
$uri = $this->initLink($link);
// !!!!!!!!!!!!!!!!!!!!!!!
preg_match_all('/\/\/(.*)((\/|$).*$)/U', $uri, $uriMatch);
preg_match_all('/\/\/(.*)((\/|$).*$)/U', $this->target, $targetMatch);
if ($targetMatch[1][0] === $uriMatch[1][0]) {
$location = '';
} else {
$location = Yii::$app->params['externalLinksPath'] . '/' . $uriMatch[1][0];
}
unset($uriMatch, $targetMatch);
// Инициализация ссылки
if ($uri === $this->target || $uri . '/' === $this->target || $uri === $this->target . '/' || $uri . '\\' === $this->target || $uri === $this->target . '\\' || $uri === '/' || $uri === '\\') {
// Если это первый запуск (запрошенная, главная ссылка)
// Создание ссылки
$links[$rawLink][0] = '/index.html';
$links[$rawLink][1] = '';
$links[$rawLink][2] = '/index.html';
} else if (preg_match('/\\.css/i', $uri)) {
// Если это CSS файл
// Получение последнего каталога (имени файла с расширением), например: '/index.html'
if (preg_match_all('/[^\/\\\\\s]+$/', $uri, $file)) {
// Создание ссылки
$links[$rawLink][0] = '/' . $file[0][0];
$links[$rawLink][1] = $location . Yii::$app->params['cssPath'];
$links[$rawLink][2] = $links[$rawLink][1] . $links[$rawLink][0];
}
// Обновление статистики
// if (YII_DEBUG) {
$this->statistics[4][0]++;
$this->statistics[4][5]++;
// }
} else if (preg_match('/\\.js/i', $uri)) {
// Если это JS файл
// Получение последнего каталога (имени файла с расширением), например: '/index.html'
if (preg_match_all('/[^\/\\\\\s]+$/', $uri, $file)) {
// Создание ссылки
$links[$rawLink][0] = '/' . $file[0][0];
$links[$rawLink][1] = $location . Yii::$app->params['jsPath'];
$links[$rawLink][2] = $links[$rawLink][1] . $links[$rawLink][0];
}
// Обновление статистики
// if (YII_DEBUG) {
$this->statistics[4][0]++;
$this->statistics[4][6]++;
// }
} else if (preg_match('/(\\.png|\\.jpeg|\\.jpg|\\.webp|\\.gif|\\.svg|\\.ico)/i', $uri)) {
// Если это изображение
// Получение последнего каталога (имени файла с расширением), например: '/index.html'
if (preg_match_all('/[^\/\\\\\s]+$/', $uri, $file)) {
// Создание ссылки
$links[$rawLink][0] = '/' . $file[0][0];
$links[$rawLink][1] = $location . Yii::$app->params['imgPath'];
$links[$rawLink][2] = $links[$rawLink][1] . $links[$rawLink][0];
}
// Обновление статистики
// if (YII_DEBUG) {
$this->statistics[4][0]++;
$this->statistics[4][2]++;
// }
} else if (preg_match('/(https?:(\/\/|\\\\).+(\/|\\\).+|^(\/|\\\).+)(\.(?!php|htm)[A-z0-9]+)[^\/\\\s]*$/i', $uri)) {
// Если это неопознанный документ (очень затратное выражение, но по другому никак)
// Получение последнего каталога (имени файла с расширением), например: '/index.html'
if (preg_match_all('/[^\/\\\\\s]+$/', $uri, $file)) {
// Создание ссылки
$links[$rawLink][0] = '/' . $file[0][0];
$links[$rawLink][1] = $location . Yii::$app->params['docsPath'];
$links[$rawLink][2] = $links[$rawLink][1] . $links[$rawLink][0];
}
// Обновление статистики
// if (YII_DEBUG) {
$this->statistics[4][0]++;
$this->statistics[4][7]++;
// }
} else if (preg_match_all('/(\/\/|\\\\)(.*)((\/|\\|$).*$)/U', $uri, $uriMatch)) {
// Иначе, если это обрабатывается универсально или как HTML документ
if (isset($uriMatch[3][0])) {
// Если есть путь к файлу, например 'https://domain.zone/это/обязательно/index.html'
if (preg_match_all('/^([^\/\\\\\s\.]*[^\\s\.]+)([^\/\\\]*\.html|[^\/\\\]*\.php|[^\/\\\]*\.htm)?$/U', $uriMatch[3][0], $uriSplit)) {
// Если в URI не найден URN (файл с расширением, например: 'index.php')
if (empty($uriSplit[2][0])) {
$uriSplit[2][0] = '/index.html';
}
// Создание ссылки
$links[$rawLink][0] = $uriSplit[2][0];
$links[$rawLink][1] = $location . Yii::$app->params['pagesPath'] . $uriSplit[1][0];
$links[$rawLink][2] = $links[$rawLink][1] . $links[$rawLink][0];
}
} else {
// Иначе обрабатывается как пустая ссылка, например 'https://domain.zone'
// Создание ссылки
$links[$rawLink][0] = '/index.html';
$links[$rawLink][1] = $location . Yii::$app->params['pagesPath'] . '/' . $uriMatch[1][0];
$links[$rawLink][2] = $links[$rawLink][1] . '/index.html';
}
// Прибавление количеству найденных страниц
// if (YII_DEBUG) {
$this->statistics[3][0]++;
// }
} else {
// Иначе, ссылку не удалось инициализировать, пропуск
// throw new Exception('Не удалось идентифицировать сохранённую ссылку: '.$link)
}
// Удаление обработанной ссылки и оставшихся переменных
unset($links[$this->linksNew], $rawLink, $location, $uriSplit, $match, $file, $uri);
// Прибавление количеству обработанных ссылок
// if (YII_DEBUG) {
$this->statistics[2][1]++;
// }
// Подготовка к следующей итерации цикла
$this->linksNew--;
}
// Количество обработанных ссылок без дубликатов
// if (YII_DEBUG) {
// Количество обработанных ссылок без дубликатов
$this->statistics[2][2] = count($this->links);
// }
return $links;
}
/**
* Конвертер страниц
*
* Преобразует ссылки в тексте (HTML документе)
* Возвращает массив не найденных файлов
*
* @param array $files Файлы для конвертации
*
* @return array
*/
private function convertFiles($files = null)
{
if (empty($files)) {
$files = &$this->files;
}
foreach ($files as $link => &$file) {
if ($file[0] === 0 && isset($this->links[$link]) && file_exists(Yii::$app->params['basePath'] . $this->links[$link][1] . $this->links[$link][0]) && $content = file_get_contents(Yii::$app->params['basePath'] . $this->links[$link][1] . $this->links[$link][0])) {
// Если метаданные файла указывают, что он является HTML документом
if (preg_match_all('/(href|src)\\s?=\\s*[\"\']?((?!(\"|\'))(?!tel)(?!mailto)[^\"\']+)[\"\']/i', $content, $match)) {
// Если найдены ссылки
// Конвертация
foreach ($match[2] as $rawLink) {
if (!$rawLink = $this->filterLink($rawLink)) {
// Если ссылка не прошла фильтрацию
continue;
}
if (!array_key_exists($rawLink, $this->links)) {
continue;
}
$content = preg_replace('/(\"|\')' . preg_quote($rawLink, '/') . '(\"|\')/', '".' . $this->links[$rawLink][2] . '"', $content);
}
// Инъекция тега <base> в страницу, чтобы работали относительные пути
if (preg_match_all('/(.*<head>)(.*)/si', $content, $contentMatch)) {
// Если удалось найти <head> в странице
// Определяем вложенность страницы
if (preg_match_all('/([^\\\|\/|\\s]+)/', $this->links[$link][1], $urlMatch)) {
$catalogsDepth = count($urlMatch[1]);
} else {
// throw new Exception('Ошибка при инъекции <base>: не удалось посчитать вложенность страницы');
}
$content = $contentMatch[1][0] . "\n<base href=\"" . str_repeat('../', $catalogsDepth ?? 0) . '">'. $contentMatch[2][0];
}
// Сохранение файла
if (file_put_contents(Yii::$app->params['basePath'] . $this->links[$link][1] . $this->links[$link][0], $content)) {
unset($this->files[$link]);
// Прибавление количеству конвертированных страниц
// if (YII_DEBUG) {
$this->statistics[3][1]++;
// }
} else {
// throw new Exception('Не удалось сохранить файл');
}
}
} else if ($file[0] !== 0) {
// Если файл не является HTML документом
unset($this->files[$link]);
} else {
// Иначе воспринимается как не HTML документ, который не требует конвертацию
continue;
}
}
unset($link, $file); // Очистка на всякий случай, так как переменные остаются
return $files;
}
/**
* Фильтрация ссылки
*
* Перед инициализацией ссылка проверяется фильтрами
*
* @return ?string
*/
private function filterLink($link)
{
// Проверка существования URL в чёрном списке
if (!empty(Yii::$app->params['regBlackList']) && preg_match('/' . Yii::$app->params['regBlackList'] . '/', $link)) {
return null;
}
// Преобразование слешей в Unix стиль, унификация
$link = preg_replace('/\\\/', '/', $link);
return $link;
}
/**
* Инициализация ссылки
*
* Подготовка к конвертации
*
* @return string
*/
private function initLink($link)
{
// Подготовка ссылок перед обработкой
// Разбиение ссылки на каталоги: 'https://domain.zone/foo/bar/index.html' на 'https:', 'domain.zone', 'foo', 'bar', 'index.html'
if (preg_match_all('/([^\\\|\/|\\s]+)/', $link, $uriMatch)) {
// Определение того, что URI является полноценным и имеет протокол подключения, например: 'https:', 'ssh:', 'mail:'
if (preg_match('/^.*:$/', $uriMatch[0][0])) {
$uri = $uriMatch[0][0] . '//' . $uriMatch[0][1];
$uriMatch[0] = array_slice($uriMatch[0], 2);
} else {
$uri = $this->connectionProtocol . ':' . '//' . $this->connectionHost;
}
// Замена всех фрагментов URI от символов, которые Windows не даёт записывать в именах файлов и каталогов на '@'
// На данный момент достаточно заменять все символы на один, так как обратная конвертация не потребуется, а шанс конфликта имён минимален
foreach ($uriMatch[0] as &$piece) {
$piece = preg_replace('/(\\\|\/|\:|\*|\?|\"|\<|\>|\|)/', '@', $piece);
}
unset($piece);
// Сборка новой ссылки из фрагментов оригинальной
foreach ($uriMatch[0] as $piece) {
$uri .= '/' . $piece;
}
unset($piece);
} else if ($link === '/' || $link === '\\') {
// Иначе, если ссылка ведёт на главную страницу сайта
// [!!!] Это надо переработать, так как ссылка '/' можеть быть и на стороннем хосте [!!!]
$uri = $this->connectionProtocol . ':' . '//' . $this->connectionHost;
// Иначе всё обрабатывается как ссылка на текущего хоста
} else {
if (!preg_match('/^(\/|\\\)/', $link)) {
$link = '/' . $link;
}
$link = preg_replace('/(\/|\\\)$/', '', $link);
$uri = $this->connectionProtocol . ':' . '//' . $this->connectionHost . $link;
}
unset($link, $uriMatch, $site);
return $uri;
}
}

14
yii Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env php
<?php
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');
require __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/vendor/yiisoft/yii2/Yii.php';
$config = require __DIR__ . '/config/console.php';
$application = new yii\console\Application($config);
$exitCode = $application->run();
exit($exitCode);