PHPの正規表現でHTML・CSS・JSを圧縮 (minify) する方法

PHPのpreg_replace関数を使用して、HTML・CSS・JS各コードの無駄な空白文字列 (半角スペース・タブスペース・改行) を取り除いて圧縮 (minify) する正規表現を紹介します。

WordPressなどのPHPを使用したCMSでも使用できます。

HTMLコード内には、script要素でJSコード、style要素でCSSコードが直接記述されている場合があるので、JSコードの圧縮、CSSコードの圧縮、そして、それらを使用したHTMLコードの圧縮の順番で紹介します。

JSコードを圧縮するためのPHPコードと正規表現

// なんらかの方法で取得したJSコード
$js = '/*! JSコード */…';

// 置換用の配列を生成
$js_replaces = [];

// (1) JSの正規表現前後の空白文字列の除去
$js_replaces[ '/([(+=])\s*(\/(?:(?!(?<!\\\)\/).)+\/[dgimsuy]*)\s*([)+,.;])/s' ] = '${1}${2}${3}';

// (2) コメントの除去
$js_replaces[ '/(\/\*[!@].*?\*\/|[(+=]\/(?:(?!(?<!\\\)\/).)+\/[dgimsuy]*[)+,.;]|\"(?:(?!(?<!\\\)\").)*\"|\'(?:(?!(?<!\\\)\').)*\'|\`(?:(?!(?<!\\\)\`).)*\`)|\/\*.*?\*\/|\/\/[^\r\n]+[\r\n]/s' ] = '${1}';

// (3) 1つ以上連続する空白文字列の置換
$js_replaces[ '/(\/\*[!@].*?\*\/|[(+=]\/(?:(?!(?<!\\\)\/).)+\/[dgimsuy]*[)+,.;]|\"(?:(?!(?<!\\\)\").)*\"|\'(?:(?!(?<!\\\)\').)*\'|\`(?:(?!(?<!\\\)\`).)*\`)\s*|\s+/s' ] = '${1} ';

// (4) 記号前後の半角スペースの除去
$js_replaces[ '/(\/\*[!@].*?\*\/|[(+=]\/(?:(?!(?<!\\\)\/).)+\/[dgimsuy]*[)+,.;]|\"(?:(?!(?<!\\\)\").)*\"|\'(?:(?!(?<!\\\)\').)*\'|\`(?:(?!(?<!\\\)\`).)*\`) | ([!#$%&)*+,\-.\/:;<=>?@\]^_|}~]) | ([!#$%&)*,.\/:;<=>?@\]^_|}~]|\+(?!\+)|-(?!-)|\z)|([!#$%&()*+,\-.\/:;<=>?@\[\]^_{|}~]|\A) /s' ] = '${1}${2}${3}${4}';

// 一括置換
$js = preg_replace( array_keys( $js_replaces ), array_values( $js_replaces ), $js );

「(1) JSの正規表現前後の空白文字列の除去」の解説

JSコード内の正規表現部分を圧縮の対象から除外するために、正規表現前後の記号を含めた正規表現部分の余計な空白文字列を除去します。

正規表現のデリミタ (開始と終了を表す記号) であるスラッシュ (/) は、コメント (/*…*/ or //…) やURL (https://…) などに、頻繁に使用されます。

そのため、単純にスラッシュからスラッシュまでを正規表現部分として扱うと、予期しない圧縮が行われる可能性があるため、正規表現前後の記号も含めています。

「(2) コメントの除去」の解説

特殊なコメント (/*!…*/ or /*@…@*/)、正規表現部分、引用符で括られた内容 ("…" or '…' or `…`)、これらを以降、JSの置換対象外部分と呼ぶことにします。

ここでは、JSの置換対象外部分の外側にあるコメントを除去します。

「(3) 1つ以上連続する空白文字列の置換」の解説

JSの置換対象外部分の外側にある、1つ以上連続する空白文字列 (半角スペース・タブスペース・改行) を1つの半角スペースに置き換えます。

「(4) 記号前後の半角スペースの除去」の解説

JSの置換対象外部分の外側にある、記号の前後の半角スペースを除去します。

記号はコード上、特殊な役割を持つため、前後の半角スペースが除去できる記号、前の半角スペースが除去できる記号、後の半角スペースが除去できる記号、の3つに分けています。

ポイント

JSの置換対象外部分を先にマッチさせることで、その後に続く対象のみを置換することができます。余計な空白文字列の除去だけならば簡単ですね。

置換対象外部分を設定する上で重要になるのがエスケープされた文字の存在です。例えば二重引用符の場合、二重引用符から始まり、エスケープされた二重引用符 (\") を含む可能性がある文字列があって、二重引用符で終わるというように設定しないと、エスケープされた二重引用符までしかマッチしなくなってしまいます。

CSSコードを圧縮するためのPHPコードと正規表現

// なんらかの方法で取得したCSSコード
$css = '/*! CSSコード */…';

// 置換用の配列を生成
$css_replaces = [];

// (1) @charsetの除去
$css_replaces[ '/@charset \"(utf|UTF)-8\";/' ] = '';

// (2) コメントの除去
$css_replaces[ '/(\/\*!.*?\*\/|\"(?:(?!(?<!\\\)\").)*\"|\'(?:(?!(?<!\\\)\').)*\')|\/\*.*?\*\//s' ] = '${1}';

// (3) 1つ以上連続する空白文字列の置換
$css_replaces[ '/(\/\*!.*?\*\/|\"(?:(?!(?<!\\\)\").)*\"|\'(?:(?!(?<!\\\)\').)*\')\s*|\s+/s' ] = '${1} ';

// (4) 一部の演算記号を除く記号前後の半角スペースの除去
$css_replaces[ '/(\/\*!.*?\*\/|\"(?:(?!(?<!\\\)\").)*\"|\'(?:(?!(?<!\\\)\').)*\')| ([!#$%&,.:;<=>?@^{|}~]) |([!#$&(,.:;<=>?@\[^{|}~]|\A) | ([$%&),;<=>?@\]^{|}~]|\z)/s' ] = '${1}${2}${3}${4}';

// (5) 演算記号前後の半角スペースの除去
$css_replaces[ '/(\/\*!.*?\*\/|\"(?:(?!(?<!\\\)\").)*\"|\'(?:(?!(?<!\\\)\').)*\'|\([^;{}]+\))| ([+\-\/]) |([+\-\/]) | ([+\/])/s' ] = '${1}${2}${3}${4}';

// 一括置換
$css = preg_replace( array_keys( $css_replaces ), array_values( $css_replaces ), $css );

「(1) @charsetの除去」の解説

現在、一般的に使用されている文字エンコーディングは「UTF-8」です。また、@charsetがない場合は「UTF-8」として扱われます。「UTF-8」を定義した@charsetを記述するメリットはありませんので除去します。

「(2) コメントの除去」の解説

特殊なコメント (/*!…*/)、引用符で括られた内容 ("…" or '…')、これらを以降、CSSの置換対象外部分と呼ぶことにします。

ここでは、CSSの置換対象外部分の外側にあるコメントを除去します。

「(3) 1つ以上連続する空白文字列の置換」の解説

CSSの置換対象外部分の外側にある、1つ以上連続する空白文字列 (半角スペース・タブスペース・改行) を1つの半角スペースに置き換えます。

「(4) 一部の演算記号を除く記号前後の半角スペースの除去」の解説

CSSの置換対象外部分の外側にある、一部の演算記号 (+ or - or /) を除く記号前後の半角スペースを除去します。

JSコードと同様に、記号はコード上、特殊な役割を持つため、前後の半角スペースが除去できる記号、前の半角スペースが除去できる記号、後の半角スペースが除去できる記号、の3つに分けています。

特に、一部の演算記号は擬似クラス (:nth-child(), :nth-last-child(), :nth-of-type(), :nth-last-of-type()) や関数値 (calc(), min(), max(), clamp()) で使用される際、前後の半角スペースが重要な意味を持つ場合があるので、次の「(5) 演算記号前後の半角スペースの除去」で別途除去します。

「(5) 演算記号前後の半角スペースの除去」の解説

CSSの置換対象外部分に丸括弧で括られた内容 ((…)) を加えて、これらの外側にある一部の演算記号 (+ or - or /) 前後の半角スペースを除去します。

注意点としては、演算記号の乗算を意味するアスタリスク (*) だけは前後の半角スペースを除去しません。アスタリスクは全称セレクタとして利用されるためです。

ポイント

JSコードと同様に、CSSの置換対象外部分を先にマッチさせることで、その後に続く対象のみを置換することができます。

CSSの場合、特定の記号がセレクタとプロパティなど、異なる記述ルールで使用されることもあるため、置換対象と置換対象外の文字列を厳密に区別する必要があります。

例えば、セレクタの擬似クラス・疑似要素を表すコロン (:) の前にある空白文字列は最低1つ残しておかなければいけません。p:first-childp :first-child ではプロパティの適用対象が異なるためです。

一方、プロパティ名とプロパティ値を区切るコロン (:) の前後にある空白文字列は取り除いても問題ありません。

HTMLコードを圧縮するためのPHPコードと正規表現

// なんらかの方法で取得したHTMLコード
$html = '<!--[HTMLコード]-->...';

// 置換用の配列を生成
$html_replaces = [];

// (1) script要素内のJSコードの圧縮
if( preg_match_all( '[<script(?: [^>]+)?>(.+?)</script>]is', $html, $matches, PREG_SET_ORDER ) ) {
  foreach( $matches as $match ) {
    $js = $match[ 1 ];

    // 「JSコードを圧縮するためのPHPコードと正規表現」をここに記述

    $html_replaces[ $match[ 1 ] ] = $js;
  }
}

// (2) style要素内のCSSコードの圧縮
if( preg_match_all( '[<style(?: [^>]+)?>(.+?)</style>]is', $html, $matches, PREG_SET_ORDER ) ) {
  foreach( $matches as $match ) {
    $css = $match[ 1 ];

    // 「CSSコードを圧縮するためのPHPコードと正規表現」をここに記述

    $html_replaces[ $match[ 1 ] ] = $css;
  }
}

// (3) pre要素内とtextarea要素内の改行の置換
if( preg_match_all( '[<(?:pre|textarea)(?: [^>]+)?>(.+?)</(?:pre|textarea)>]is', $html, $matches, PREG_SET_ORDER ) ) {
  foreach( $matches as $match ) {
    $html_replaces[ $match[ 1 ] ] = str_replace( "\n", '&#xA;', $match[ 1 ] );
  }
}

// script要素内、style要素内、pre要素内、textarea要素内を一括置換
if( !empty( $replaces ) ) {
  $html = str_replace( array_keys( $html_replaces ), array_values( $html_replaces ), $html );
}

// 置換用の配列を生成
$html_replaces = [];

// (4) コメントの除去
$html_replaces = [ '[<!--(?![<>\[\]]).*?(?<![<>\[\]])-->]s' ] = '';

// (5) 空白文字列の除去
$html_replaces = [ '[\n\s*(\S)|\A\s+|\s+\z]s' ] = '${1}';

// 一括置換
$html = preg_replace( array_keys( $html_replaces ), array_values( $html_replaces ), $html );

「(1) script要素内のJSコードの圧縮」の解説

HTMLコード内のscript要素を使用して直書きされたJSコードを、「JSコードを圧縮するためのPHPコードと正規表現」で紹介したPHPコードで圧縮します。

「(2) style要素内のCSSコードの圧縮」の解説

HTMLコード内のstyle要素を使用して直書きされたCSSコードを、「CSSコードを圧縮するためのPHPコードと正規表現」で紹介したPHPコードで圧縮します。

「(3) pre要素内とtextarea要素内の改行の置換」の解説

HTMLコード中の改行がそのまま表示されるpre要素とtextarea要素の改行を文字参照 (&#xA;) に置き換えます。

「(4) コメントの除去」の解説

まだ使用しているかもしれない「条件付きコメント」などの特殊なコメントを除外して、それ以外のコメントを除去します。

「(5) 空白文字列の除去」の解説

改行とその後に続く0個以上の空白文字列、HTMLコードの最初と最後にある空白文字列を除去します。

ポイント

改行とその後に続く空白文字列にマッチさせることで、文章内の単語の区切りなどに使用される半角スペースを置換対象外にすることができます。

また、pre要素内やtextarea要素内の改行は、あらかじめ文字参照に置き換えてあるので、空白文字列の除去の影響を受けません。

おまけ: WordPressで出力直前のHTMLコードを取得する方法

add_action( 'wp', function() {
  ob_start( function( $html ) {

    // 「HTMLコードを圧縮するためのPHPコードと正規表現」をここに記述

    return $html;
  } );
} );

正規表現は複雑なので、時間が経つと自分が書いたコードでさえ「?」となってしまいます。そんな自分のための備忘録でした。

2021年12月28日に大幅に加筆修正しました。不具合等を見つけたら是非教えてください。

関連記事