Упаковщик Javascript: зачистка js-файла
версия для печатиЗадача: удалить однострочные/многострочные комментарии, tab-отступы, пустые строки в js-скрипте. Так же нужно учесть, что в коде возможны экранированные back-slash и кавычки. После всего удалить переносы строк, чтобы получится весь код в одной строке. Разбить код на строки заданной длины.
Я не знаю, зачем последнее действие, но все "обфускаторы" так делают =-). Приведенный в статье скрипт я считаю упаковщиком, потому что он уменьшает вес конечного файла, а еще - мне так больше нравится :) Зачем все это нужно, я уже писал здесь. В двух словах: этот скрипт чистит js-файлы для загрузки на сервер.
Код получился большой (для статьи), поэтому разделен на две части, пояснения под кодом. Прежде чем писать статью, я трижды его переделывал, каждый раз думая, что теперь все правильно работает %). Если опять что-то не так будет, укажу здесь дату изменений (см. строку 1).
//Версия v1.4b, 22 октября 2012
$fn='/home/site/js/script.js'
$text=file_get_contents($fn);
$text=preg_replace("#\r\n|\r#","\n",$text);//Приводим концы строк к одному виду - Linux окончания
//---блок_1----
$new='';
//Ищем одно из пяти "контрольных" выражений.
while (preg_match('#.*(//|/\*|[\'"/])#Us',$text,$match,PREG_OFFSET_CAPTURE))
{
$case=$match[1][0]; //какое именно выражение нашла функция.
$pos=$match[1][1]+1;//позиция, следующая за найденным выражением. Значение важно только для кавычек.
if ($case=='//') $text=preg_replace('#\/\/.*(\n?)$#m', "$1", $text, 1);//Вырезаем один комментарий, начинающийся с //
elseif($case=='/*') $text=preg_replace('#\/\*.*\*\/#Us', '', $text, 1);//Вырезаем один комментарий, заключенный в "/*...*/"
else{
$new.=$match[0][0];//Вхождение всего шаблона, описанного в условии цикла, т.е. весь текущий $text до открывающей кавычки включительно.
//Ищем не экранированную закрывающую кавычку, одинарную или двойную, в зависимости от открывающей (значение $case).
//Шаблон: выбрать все до кавычки включительно (не жадный поиск). Все бэк-слеши перед ней выделить отдельно.
$ptn="#.*?(\\\*)($case)#";
$flag=1;
//Гоняем цикл, пока не найдем НЕ экранированную закрывающую кавычку или пока не возникнет ошибка.
while ($flag==1){
$flag=preg_match($ptn, $text, $match, PREG_OFFSET_CAPTURE, $pos);
//Если количество бэк-слешей перед кавычкой - нечетное, значит она экранированная.
if(strlen($match[1][0])%2 == 1)
$pos=$match[2][1]+1; //Позиция для продолжения поиска - со следующего символа после экранированной кавычки.
else $flag='break'; //Нашли закрывающую кавычку, больше не гонять цикл.
$new.=$match[0][0]; //Вхождение всего шаблона. Здесь весь текст до кавычки (включительно), найденой в любом состоянии экрана.
}
if(!$flag) die('ERROR: Не найдена закрывающая кавычка. Прерываем зачистку.');
$text=substr($text,$match[2][1]+1);//Остаток текста после очередной закрывающей кавычки.
}
}
$new.=$text; //отстаток текста без каких-либо кавычек или комментариев.
$text=preg_replace("#\s+\n+#","\n",$new);//Заменяем все пробелы в конце строки и множественные переносы строк на один перенос строки (Linux).
//---блок_1. Конец----
Пояснения: блок 1
В этом блоке все основано на последних двух параметрах функции preg_match($pattern, $subject, $matches, $FLAGS, $OFFSET).
Смысл: ищем вхождение одного из "выражений": // ИЛИ /* ИЛИ ' ИЛИ " ИЛИ /. Слеши должны
быть именно в такой последовательности. В итоге есть 5 вариантов развития событий:
1.
однострочный комментарий - регуляркой удаляем все до конца строки (что бы там не было, все это - комментарий). Если есть перенос строки, пишем его. Переноса не будет, только если комментарий написан в конце файла. Это частный случай, но его нужно учитывать.
2. многострочный комментарий - регулярка для удаления такого коммента.
В первых двух случаях изменения происходят с исходным текстом ($text), поэтому в $new ничего не пишется, а $text укорачивается регулярками.
3. и 4. кавычка или двойная кавычка: все до открывающей кавычки (вместе с ней) пишем в $new, потом начиная с позиции +1 от нее ищем не экранированную закрывающую кавычку. Все, что найдем между кавычками (вместе с закрывающей), так же дописываем в $new. После этого отбрасываем часть исходного текста до найденой закрывающей кавычки включительно, т.к. эта часть проверена и больше не нужна в анализе. В конце всего дописываем в $new то, что осталось от исходного текста.
5. Одиночные слеши в js используются для описания регулярного выражения, типа '/pattern/gmi'. Упаковщик должен воспринимать такие выражения, как текст в кавычках, т.е. ничего внутри не меняя. Поэтому обработка такая же, как в случаях 3 и 4.
Сейчас в переменной $text - код скрипта без комментариев. Продолжение:
//Удаление форматирования
$text=preg_replace('#\t#','',$text); //Убираем tab-отступы.
$text=preg_replace("#\n+\s+#","\n",$text);//Убираем все пробелы в начале строк.
//$text=preg_replace("#\n#",'',$text); //Убираем переносы строк. Закомментил, т.к. ниже будем делить весь текст на куски.
//---блок_2----
$from=$offset=0;
$new='';
$len=strlen($text);
//Ищем ближайший перенос строки сразу за смещением в NN символов. NN=450. Здесь же следим за краем текста.
//Если оставшийся кусок будет меньше, то уже ничего не ищем, будем вырезать все, что осталось.
do {
$offset=$from+450;
if($offset<$len) $pos=strpos($text,"\n",$offset); else $pos=$len;
if($pos===false) $pos=$len;
$sub=substr($text,$from,$pos-$from); //Вырезаем очередной кусок текста
$new.=preg_replace("#\n#",'',$sub)."\n";//Убираем внутри него переносы строк, в конце куска наоборот ставим один перенос строки.
$from=$pos+1;
} while ($pos!=$len);
$text=substr($new,0,-1);//Уберем последний перенос строки. Штрих красоты ;)
//---блок_2. Конец----
file_put_contents($fn,$text);//Пишем итоговый текст в файл.
Пояснения: блок 2
Делим на строки по признаку переноса строки "\n". Почему именно он? Потому что этот признак гарантированно исключает деление строки в неподходящих местах, например посреди текстового значения переменной.
Есть масса вариантов поделить текст кода на строки, начиная от последовательного удаления каждого переноса (считая длину), до заполнения массива кусками текста с последующей склейкой символом "\n". Я выбрал не самый мутный и не самый долгий вариант :) Итоговые куски текста могут получиться короче заявленных NN символов, т.к. из них удаляется несколько переносов строк.
Если блок 2 исключить из скрипта, то в итоговом файле сохранится разбиение на строки. Если же нужен весь код в одной строке, то кроме исключения блока 2 нужно раскомментировать строку (4).
P.S.: Почему нельзя считать данное решение обфускацией? Как сказал в одной статье Крис Касперски "обфускация - это сложная инженерная задача". В википедии есть определение: "Обфускация (от лат. obfuscare — затенять, затемнять; и англ. obfuscate — делать неочевидным, запутанным, сбивать с толку) или запутывание кода — приведение исходного текста или исполняемого кода программы к виду, сохраняющему ее функциональность, но затрудняющему анализ, понимание алгоритмов работы и модификацию при декомпиляции". Поставленная здесь задача - всего лишь чистка исходного кода с целью уменьшения веса файла. Конечно, удаление комментариев делает понимание сложнее, но не настолько, чтоб заявлять об обфускации.
[UPD0] Раньше в этой статье я высказал идею использовать PHP-функцию php_strip_whitespace() для очистки js-скрипта. Я был уверен, что функция будет работать, почему что: синтакис javascript относительно комментариев такой же, как у PHP; экранирование символов в текстовых значениях переменных такое же, а ограничений на эти значения еще больше (в javascript нельзя записать текстовое значение в несколько строк в паре кавычек).
Я ошибался. Синтаксис регулярных выражений js не совпадает с PHP, а именно возможность описать шаблон типа 'prtn=/patten/g'. Такие описания могут привести к неправильной очистке js-кода через php_strip_whitespace(). Поэтому ее использовать нельзя.
[1oo%, EoF]Похожие материалы: Упаковщик CSS: зачистка css-файла
Понравилась статья? Расскажите о ней друзьям: