使用正则表达式缩小/压缩CSS


Minify/compress CSS with regex?

在PHP你可以压缩/缩小CSS与regex (PCRE)?

(作为regex中的理论。我相信有一些库在这方面做得很好。

背景说明:在花了几个小时写了一个已删除(半垃圾)问题的答案后,我想我应该发布一部分潜在问题并自己回答它。

简单正则表达式CSS压缩器

(好吧,这可能不是太简单,但很直接。)

需求

这个答案假设需求是:

  • 删除评论
  • 将大于1个空格的空格组合替换为单个空格
  • 删除元字符周围的所有空白:{, }, ;, ,, >, ~, +, -
  • 删除!important周围的空格
  • 删除:周围的空格,除了在选择器(你必须在它之前保持一个空格)
  • 删除$=
  • 等操作符周围的空格
  • 删除(/[右侧和)/]左侧的所有空格
  • 删除字符串
  • 开头和结尾的所有空格
  • 删除块中的最后一个;
  • 不要修改字符串
  • 不需要处理无效的CSS

注意,这里的要求不包括将CSS属性转换为更短的版本(比如使用速记属性而不是几个完整长度的属性,删除不需要的引号)。这是一般情况下regex无法解决的问题。

解决方案

通过两步解决这个问题更容易:首先删除注释,然后删除其他所有内容。

应该可以在一次传递中完成,但随后您必须将所有's替换为匹配空格和注释的表达式(以及其他一些修改)。

删除注释的第一个传递表达式:

(?xs)
  # quotes
  (
    "(?:[^"'']++|''.)*+"
  | '(?:[^''']++|''.)*+'
  )
|
  # comments
  /'* (?> .*? '*/ )

$1代替

要删除其他所有内容,可以使用:

(?six)
  # quotes
  (
    "(?:[^"'']++|''.)*+"
  | '(?:[^''']++|''.)*+'
  )
|
  # ; before } (and the spaces after it while we're here)
  's*+ ; 's*+ ( } ) 's*+
|
  # all spaces around meta chars/operators
  's*+ ( [*$~^|]?+= | [{};,>~+-] | !important'b ) 's*+
|
  # spaces right of ( [ :
  ( [[(:] ) 's++
|
  # spaces left of ) ]
  's++ ( [])] )
|
  # spaces left (and right) of :
  's++ ( : ) 's*+
  # but not in selectors: not followed by a {
  (?!
    (?>
      [^{}"']++
    | "(?:[^"'']++|''.)*+"
    | '(?:[^''']++|''.)*+' 
    )*+
    {
  )
|
  # spaces at beginning/end of string
  ^ 's++ | 's++ 'z
|
  # double spaces to single
  ('s)'s+

替换为$1$2$3$4$5$6$7 .

与正确的解析器相比,选择器检查是否删除:之前的空格(负向前看)可能会减慢此速度。解析器已经知道它们是否在选择器中,并且不需要做额外的搜索来检查。

PHP中的示例实现

function minify_css($str){
    # remove comments first (simplifies the other regex)
    $re1 = <<<'EOS'
(?sx)
  # quotes
  (
    "(?:[^"'']++|''.)*+"
  | '(?:[^''']++|''.)*+'
  )
|
  # comments
  /'* (?> .*? '*/ )
EOS;
    $re2 = <<<'EOS'
(?six)
  # quotes
  (
    "(?:[^"'']++|''.)*+"
  | '(?:[^''']++|''.)*+'
  )
|
  # ; before } (and the spaces after it while we're here)
  's*+ ; 's*+ ( } ) 's*+
|
  # all spaces around meta chars/operators
  's*+ ( [*$~^|]?+= | [{};,>~+-] | !important'b ) 's*+
|
  # spaces right of ( [ :
  ( [[(:] ) 's++
|
  # spaces left of ) ]
  's++ ( [])] )
|
  # spaces left (and right) of :
  's++ ( : ) 's*+
  # but not in selectors: not followed by a {
  (?!
    (?>
      [^{}"']++
    | "(?:[^"'']++|''.)*+"
    | '(?:[^''']++|''.)*+' 
    )*+
    {
  )
|
  # spaces at beginning/end of string
  ^ 's++ | 's++ 'z
|
  # double spaces to single
  ('s)'s+
EOS;
    $str = preg_replace("%$re1%", '$1', $str);
    return preg_replace("%$re2%", '$1$2$3$4$5$6$7', $str);
}

快速测试

可以在ideone.com找到:

$in = <<<'EOS'
p * i ,  html   
/* remove spaces */
/* " comments have no escapes '*/
body/* keep */ /* space */p,
p  [ remove ~= " spaces  " ]  :nth-child( 3 + 2n )  >  b span   i  ,   div::after
{
  /* comment */
    background :  url(  "  /* string */  " )   blue  !important ;
        content  :  " escapes '" allowed ''" ;
      width: calc( 100% - 3em + 5px ) ;
  margin-top : 0;
  margin-bottom : 0;
  margin-left : 10px;
  margin-right : 10px;
}
EOS;

$out = minify_css($in);
echo "input:'n";
var_dump($in);
echo "'n'n";
echo "output:'n";
var_dump($out);
输出:

input:
string(435) "
p * i ,  html   
/* remove spaces */
/* " comments have no escapes '*/
body/* keep */ /* space */p,
p  [ remove ~= " spaces  " ]  :nth-child( 3 + 2n )  >  b span   i  ,   div::after
{
  /* comment */
    background :  url(  "  /* string */  " )   blue  !important ;
    content  :  " escapes '" allowed ''" ;
      width: calc( 100% - 3em + 5px ) ;
  margin-top : 0;
  margin-bottom : 0;
  margin-left : 10px;
  margin-right : 10px;
}
"

output:
string(251) "p * i,html body p,p [remove~=" spaces  "] :nth-child(3+2n)>b span i,div::after{background:url("  /* string */  ") blue!important;content:" escapes '" allowed ''";width:calc(100%-3em+5px);margin-top:0;margin-bottom:0;margin-left:10px;margin-right:10px}"

cssminifier.com

对于相同输入的cssminifier.com的结果:

p * i,html /*'*/body/**/p,p [remove ~= " spaces  "] :nth-child(3+2n)>b span i,div::after{background:url("  /* string */  ") blue;content:" escapes '" allowed ''";width:calc(100% - 3em+5px);margin-top:0;margin-bottom:0;margin-left:10px;margin-right:10px}

长度263字节。比上面的正则表达式的输出长12个字节。

cssminifier.com与这个正则表达式迷你器相比有一些缺点:

  • 它留下了部分评论。(这可能是有原因的。也许需要一些CSS技巧)
  • 不删除某些表达式中操作符周围的空格

CSSTidy

在预设的最高压缩级别下输出CSSTidy 1.3(通过codebeautifier.com):

p * i,html /* remove spaces */
/* " comments have no escapes '*/
body/* keep */ /* space */p,p [ remove ~= " spaces " ] :nth-child( 3 + 2n ) > b span i,div::after{background:url("  /* string */  ") blue!important;content:" escapes '" allowed ''";width:calc(100%-3em+5px);margin:0 10px;}

长度286字节。比正则表达式的输出长35字节。

CSSTidy不删除某些选择器中的注释或空格。但它确实简化为速记属性。后者可能会帮助压缩普通CSS更多。

并排比较

对于相同的输入,从不同的缩小器缩小输出,如上面的例子。(剩余的换行符用空格代替)

this answern    (251): p * i,html body p,p [remove~=" spaces  "] :nth-child(3+2n)>b span i,div::after{background:url("  /* string */  ") blue!important;content:" escapes '" allowed ''";width:calc(100%-3em+5px);margin-top:0;margin-bottom:0;margin-left:10px;margin-right:10px}
cssminifier.com (263): p * i,html /*'*/body/**/p,p [remove ~= " spaces  "] :nth-child(3+2n)>b span i,div::after{background:url("  /* string */  ") blue!important;content:" escapes '" allowed ''";width:calc(100% - 3em+5px);margin-top:0;margin-bottom:0;margin-left:10px;margin-right:10px}
CSSTidy 1.3     (286): p * i,html /* remove spaces */ /* " comments have no escapes '*/ body/* keep */ /* space */p,p [ remove ~= " spaces " ] :nth-child( 3 + 2n ) > b span i,div::after{background:url("  /* string */  ") blue!important;content:" escapes '" allowed ''";width:calc(100%-3em+5px);margin:0 10px;}

对于普通的CSS, CSSTidy可能是最好的,因为它可以转换为简短的属性。

我认为有其他的缩小器(如YUI压缩器)应该在这方面做得更好,并且给出比这个regex缩小器更短的结果。

这是@Qtax的答案的一个稍微修改的版本,它解决了calc()的问题,这要归功于@matthiasmullie的miniify库中的一个替代正则表达式。

function minify_css( $string = '' ) {
    $comments = <<<'EOS'
(?sx)
    # don't change anything inside of quotes
    ( "(?:[^"'']++|''.)*+" | '(?:[^''']++|''.)*+' )
|
    # comments
    /'* (?> .*? '*/ )
EOS;
    $everything_else = <<<'EOS'
(?six)
    # don't change anything inside of quotes
    ( "(?:[^"'']++|''.)*+" | '(?:[^''']++|''.)*+' )
|
    # spaces before and after ; and }
    's*+ ; 's*+ ( } ) 's*+
|
    # all spaces around meta chars/operators (excluding + and -)
    's*+ ( [*$~^|]?+= | [{};,>~] | !important'b ) 's*+
|
    # all spaces around + and - (in selectors only!)
    's*([+-])'s*(?=[^}]*{)
|
    # spaces right of ( [ :
    ( [[(:] ) 's++
|
    # spaces left of ) ]
    's++ ( [])] )
|
    # spaces left (and right) of : (but not in selectors)!
    's+(:)(?![^'}]*'{)
|
    # spaces at beginning/end of string
    ^ 's++ | 's++ 'z
|
    # double spaces to single
    ('s)'s+
EOS;
    $search_patterns  = array( "%{$comments}%", "%{$everything_else}%" );
    $replace_patterns = array( '$1', '$1$2$3$4$5$6$7$8' );
    return preg_replace( $search_patterns, $replace_patterns, $string );
}

这个问题是专门关于PHP的,但由于这篇文章在我搜索"最小化css regex"时位于结果的顶部,所以我在这里发布了一个Python改编版:

#!/usr/bin/env python
# These regexes were adapted from PCRE patterns by Dustin "lots0logs" Falgout,
# Matthias Mullie (https://stackoverflow.com/a/15195752/299196), and Andreas
# "Qtax" Zetterlund (https://stackoverflow.com/a/44350195/299196).
import re
CSS_COMMENT_STRIPPING_REGEX = re.compile(r"""
    # Quoted strings
    ( "(?:[^"'']+|''.)*" | '(?:[^''']+|''.)*' )
    |
    # Comments
    /'* ( .*? '*/ )
    """,
    re.DOTALL | re.VERBOSE
)
CSS_MINIFICATION_REGEX = re.compile(r"""
    # Quoted strings
    ( "(?:[^"'']+|''.)*" | '(?:[^''']+|''.)*' )
    |
    # Spaces before and after ";" and "}"
    's* ; 's* ( } ) 's*
    |
    # Spaces around meta characters and operators excluding "+" and "-"
    's* ( [*$~^|]?= | [{};,>~] | !important'b ) 's*
    |
    # Spaces around "+" and "-" in selectors only
    's*([+-])'s*(?=[^}]*{)
    |
    # Spaces to the right of "(", "[" and ":"
    ( [[(:] ) 's+
    |
    # Spaces to the left of ")" and "]"
    's+ ( [])] )
    |
    # Spaces around ":" outside of selectors
    's+(:)(?![^'}]*'{)
    |
    # Spaces at the beginning and end of the string
    ^ 's+ | 's+ 'z
    |
    # Collapse concurrent spaces
    ('s)'s+
    """,
    re.DOTALL | re.IGNORECASE | re.VERBOSE
)
def minify_css(css):
    return CSS_MINIFICATION_REGEX.sub(r"'1'2'3'4'5'6'7'8",
        CSS_COMMENT_STRIPPING_REGEX.sub(r"'1", css))

可能与PHP+PCRE版本不完全相同。由于Python的正则表达式库不支持PCRE所支持的许多结构,因此我不得不修改PCRE模式。我删除的修饰符提高了性能,但可能会增强regex抵御恶意输入的能力,因此在不可信的输入上使用它可能不是一个好主意。

这里是我如何做的一个简洁的源代码。与压缩。如果你在源代码中修改了一些东西,你也不必在意。

实际上'//comments'在css中是不允许的。

ob_start('ob_handler');
if(!file_exists('style/style-min.css) 
or filemtime('style/style.css') > filemtime('style/style-min.css')){
  $css=file_get_contents('style/style.css');    
  //you need to escape some more charactes if pattern is an external string.
  $from=array('@''s*/''*.*''*/''s*@sU', '/''s{2,}/');
  $to=  array(''                      , ' ');
  $css=preg_replace($from,$to,$css); 
  $css=preg_replace('@'s*([':;,."''{}()])'s*@',"$1",$css);  
  $css=preg_replace('@;}@','}',$css);
  header('Content-type: text/css');
  echo $css;
  file_put_contents('style/style-min.css',$css);
  //etag- modified- cache-control- header
  }
else{
  //exit if not modified?
  //etag- modified- cache-control- header
  header('Content-type: text/css');
  readfile('style/style-min.css');
  }   
ob_end_flush();

PS谁在我准备打字之前给了我减号?QTax—有一小段时间我忘记转义$ form数组中的反斜杠。PSS。只有新版本的PHP才能理解'U'参数,它使正则表达式不贪婪。