最近接到了一个很有意思的任务:统计下我们团队的 JS 代码中各种 CSS 选择器的使用频率。接到任务之后还是很兴奋的。平时就很喜欢折腾正则,最近项目比较忙,调试 IE 的各种样式 Bug 调的昏天黑地的,有机会换换口味,写点自己喜欢的代码,也算是很好的消遣了。但真的写起来之后却碰到了一点小麻烦。
首先,这个问题的解决不能简单粗暴的采用正则表达式抓取代码中 长得像 CSS 表达式的文本的方式来解决。因为 JS 代码中长得像 CSS 表达式的实在太多了,比如: c = a + b , a + b 是一个标准的兄弟选择器; if(a>b) , a > b 是合法的子代选择;而 var a, b, c, d 这种情况就更常见了,同样的 a,b, c, d 也是合法的选择器。所以如果粗暴的用正则去抓取像选择器的文本是没有意义的,得到的数据质量太低。因此我换了一种思路来处理:找到支持选择器的函数,然后用正则抓取对应的参数。这样精度就高了许多。比如如果我们要统计使用 jQuery 的 JS 代码中的 CSS 选择器的使用频率,那么我们只需要统计 jQuery 中支持选择器的几个函数就可以了,比如我们可以统计符合这几种格式的字符串: $('selector') , .find('selector'), .parents('selector')等等
方法找到了就可以动手做了。不过刚动手就遇到了一个当初不曾预料到的一个问题: 这个正则太大了,而且有太多重复的内容 。我们就以 jQuery 中的$函数来举例说明。jQuery 中$函数的参数格式是:$(selecotr [, context]) ,有 4 种可能的参数组合是我们所需要的:$('selecotr', 'selector') 、 $('selecotr') 、 $('selector',not_selector) 、 $(not_selector, 'selecotr') 。selecotr 的字符串可能是单引号也可能是双引号。因此为了匹配这样一个结构我们需要类似如下的一个表达式:
reg = %r{
[^\w$] # $不能是其他变量的结尾
\$ \s* \( \s* (?:
(?: # 匹配格式$('selector' [, 'selector'])
(?<prop_1>
' (?: [^'\n\\] | \\' )*? '
| " (?: [^"\n\\] | \\" )*? "
)
(?:
(?<comma> \s* , \s*)
(?<prop_2>
' (?: [^'\n\\] | \\. )*? '
| " (?: [^"\n\\] | \\. )*? "
)
)?
)
| (?: # 匹配格式$('selector', not-selecotr)
(?<prop_1>
' (?: [^'\n\\] | \\. )*? '
| " (?: [^"\n\\] | \\. )*? "
)
(?<comma> \s* , \s*)
(?: [\w$.]+ )
)
| (?: # 匹配格式$(not-selecotr, 'selector')
(?: [\w$.]+ )
(?<comma> \s* , \s* )
(?<prop_2>
' (?: [^'\n\\] | \\. )*? '
| " (?: [^"\n\\] | \\. )*? "
)
)
) \s* \)
}x
为了说明方便 not_selector 部分与项目中使用大代码相比做了适当的简化,不考虑用方括号引用属性比如:object[prop1][prop2].prop3。 不知道大家看到这个表达式有何感受,我的一个同事看到之后直接被吓住了。我自己写这个表达式的时候也很费劲,太长了,稍不注意就会造成括号不匹配的错误。而这只匹配了众多函数中的一个,还有好几种可能的结构需要匹配。那么最终的的表达式将比这个还要长,还要恐怖很多。而且可以看到这个表达式中有很多内容是重复出现的,比如匹配字符串、匹配逗号等。因此我需要一种方式将表达式拆分成一些小的单位,然后将他们组合在一起。但正则的语法中没有变量之类的方案帮我们做这些事情,所以需要我们自己去想办法了。
最容易想的方案就是试下能不能使用#{}在正则中引入变量。试了下,比较幸运,是可以的。而且还有一个好消息是 Regexp 对象的 to_s 方法会返回一个转义之后正则表达式字符串,而通过#{}引入的变量会自动调用对象的 to_s 方法,因此我们可以利用这些特性将我们的大的正则表达式拆分成小块然后组合起来。看下我们拆分之后的代码
def get_scan_reg
not_selector = /[\w$.]+/
not_name = /[^\w$]/
comma = / \s* , \s* /x
lb = / \s* \( \s* /x
rb = / \s* \)/x
str = %r{
(?:
' (?: [^'\n\\] | \\. )*? '
| " (?: [^"\n\\] | \\. )*? "
)
}x
return %r{
#{not_name} \$ #{lb} (?:
(?:
(?<prop_1> #{str})
(?: #{comma} (?<prop_2> #{str}) )?
)
| (?: (?<prop_1> #{str}) #{comma} #{not_selector} )
| (?: #{not_selector} #{comma} (?<prop_2> #{str}) )
) #{rb}
}x
end
虽然还是有些复杂,但与原来相比好理解很多了。至此已经满足我的需求了,基本可以打住了,但使用 puts 输出下生成的正则后发现,还是有点瑕疵。每一个变量的外层被包了一个括号变成了一个非捕获分组,这样多少会对性能有所影响,因此我们动手把这个细节修复下。
def def_reg_part(reg, asGroup = false)
source = reg.to_s
inst = Object.new
inst.define_singleton_method(:to_s) do
return asGroup ? source : source.gsub(/^\(.*?:|\)$/, '')
end
return inst
end
def get_scan_reg
not_selector = def_reg_part(/[\w$.]+/)
not_name = def_reg_part(/[^\w$]/)
comma = def_reg_part(/ \s* , \s* /x)
lb = def_reg_part(/ \s* \( \s* /x)
rb = def_reg_part(/ \s* \) /x)
str = def_reg_part %r{
(?:
' (?: [^'\n\\] | \\. )*? '
| " (?: [^"\n\\] | \\. )*? "
)
}x
return %r{
#{not_name} \$ #{lb} (?:
(?:
(?<prop_1> #{str})
(?: #{comma} (?<prop_2> #{str}) )?
)
| (?: (?<prop_1> #{str}) #{comma} #{not_selector} )
| (?: #{not_selector} #{comma} (?<prop_2> #{str}) )
) #{rb}
}x
end
这样就满足我的需求了,这是目前为止我能找到的最好方案了,如果大家有好的方案希望能够指点一二