你可以用这个:
$pattern = <<<'LOD'
~
(?(DEFINE)
(?<quoted_content>
(["']) (?>[^"'\\]++ | \\{2} | \\. | (?!\g{-1})["'] )*+ \g{-1}
)
(?<comment> /\* .*? \*/ )
(?<url_skip> (?: https?: | data: ) [^"'\s)}]*+ )
(?<other_content>
(?> [^u}/"']++ | \g<quoted_content> | \g<comment>
| \Bu | u(?!rl\s*+\() | /(?!\*)
| \g<url_start> \g<url_skip> ["']?+
)++
)
(?<anchor> \G(?<!^) ["']?+ | @font-face \s*+ { )
(?<url_start> url\( \s*+ ["']?+ )
)
\g<comment> (*SKIP)(*FAIL) |
\g<anchor> \g<other_content>?+ \g<url_start> \K [./]*+
( [^"'\s)}]*+ ) # url
~xs
LOD;
$result = preg_replace($pattern, 'http://cdn.test.com/fonts/$8', $data);
print_r($result);
测试字符串
$data = <<<'LOD'
@font-face {
font-family: 'FontAwesome';
src: url("fonts/fontawesome-webfont.eot?v=4.0.3");
src: url(fonts/fontawesome-webfont.eot?#iefix&v=4.0.3) format("embedded-opentype"),
/*url("fonts/fontawesome-webfont.woff?v=4.0.3") format("woff"),*/
url("http://domain.com/fonts/fontawesome-webfont.ttf?v=4.0.3") format("truetype"),
url('fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format("svg");
font-weight: normal;
font-style: normal;
}
/*
@font-face {
font-family: 'Font1';
src: url("fonts/font1.eot");
} */
@font-face {
font-family: 'Fon\'t2';
src: url("fonts/font2.eot");
}
@font-face {
font-family: 'Font3';
src: url("../fonts/font3.eot");
}
LOD;
主要思想:
为了提高可读性,该模式被划分为命名的子模式。 (?(DEFINE)...) 不匹配任何内容,它只是一个定义部分。
此模式的主要技巧是使用\G 锚,这意味着:字符串的开始或与先例匹配。我在(?<!^) 后面添加了一个否定的lookbehind 来避免这个定义的第一部分。
<anchor> 命名子模式是最重要的,因为它仅在找到 @font-face { 或紧跟在 url 结尾之后才允许匹配(这就是您可以看到 ["']?+ 的原因)。
<other_content> 表示所有不是 url 部分但与必须跳过的 url 部分匹配的所有内容(以“http:”、“data:”开头的 url)。这个子模式的重要细节是它不能匹配@font-face 的右花括号。
<url_start>的使命只是匹配url("。
\K从匹配结果中重置所有之前匹配过的子串。
([^"'\s)}]*+) 匹配 url (匹配结果中唯一保留前导 ./../ 的内容)
由于 <other_content> 和 url 子模式无法匹配 }(即在引用或注释部分之外),您肯定永远不会匹配 @font-face 定义之外的内容,第二个后果是模式总是在最后一个 url 之后失败。因此,在下一次尝试时,“连续分支”将失败,直到下一个 @font-face。
另一个技巧:
主要模式以\g<comment> (*SKIP)(*FAIL) | 开头,以跳过cmets /*....*/ 中的所有内容。 \g<comment> 指的是描述评论外观的基本子模式。 (*SKIP) 禁止重试之前匹配过的子字符串(在他的左边,由g<comment>),如果模式在他的右边失败。 (*FAIL) 强制模式失败。
使用这个技巧,cmets 被跳过并且不是匹配结果(因为模式失败)。
子模式详情:
quoted_content:
在<other_content> 中使用它来避免匹配引号内的url( 或/*。
(["']) # capture group: the opening quote
(?> # atomic group: all possible content between quotes
[^"'\\]++ # all that is not a quote or a backslash
| # OR
\\{2} # two backslashes: (two \ doesn't escape anything)
| # OR
\\. # any escaped character
| # OR
(?!\g{-1})["'] # the other quote (this one that is not in the capture group)
)*+ # repeat zero or more time the atomic group
\g{-1} # backreference to the last capturing group
other_content: 所有不是右花括号,或者没有http:或data:的url
(?> # open an atomic group
[^u}/"']++ # all character that are not problematic!
|
\g<quoted_content> # string inside quotes
|
\g<comment> # string inside comments
|
\Bu # "u" not preceded by a word boundary
|
u(?!rl\s*+\() # "u" not followed by "rl(" (not the start of an url definition)
|
/(?!\*) # "/" not followed by "*" (not the start of a comment)
|
\g<url_start> # match the url that begins with "http:"
\g<url_skip> ["']?+ # until the possible quote
)++ # repeat the atomic group one or more times
锚点
\G(?<!^) ["']?+ # contiguous to a precedent match with a possible closing quote
| # OR
@font-face \s*+ { # start of the @font-face definition
注意:
您可以改进主要模式:
在 @font-face 的最后一个 url 之后,正则表达式引擎尝试匹配 <anchor> 的“连续分支”并匹配所有字符,直到导致模式失败的 }。然后,在每个相同的字符上,正则表达式引擎必须尝试两个分支或<anchor>(在} 之前总是会失败。
为了避免这些无用的尝试,您可以将主要模式更改为:
\g<comment> (*SKIP)(*FAIL) |
\g<anchor> \g<other_content>?+
(?>
\g<url_start> \K [./]*+ ([^"'\s)}]*+)
|
} (*SKIP)(*FAIL)
)
在这个新场景中,最后一个 url 之后的第一个字符由“连续分支”匹配,\g<other_content> 匹配所有字符,直到}、\g<url_start> 立即失败,} 匹配并且@987654360 @ 使模式失败并禁止重试这些字符。