正则表达式学习笔记
最近做了一道算法题,题目是提供一个函数来去除Java源文件中所有注释。
拿到题目开始觉得挺简单的,不就是多行注释和单行注释么,一行一行分别处理一下就行了:
public String removeComments(String sourceCode){
String lines = sourceCode.split("\\n");
String result = "";
String trimLine = "";
boolean multiLine = false;
for(String line:lines){
trimLine = line.replaceAll("\\s","");
if(multiLine){
if(line.endsWith("*/")){
multiLine = false;
}
continue;
}
if(line.startsWith("/*")){
multiLine = true;
continue;
}
if(trimLine.startsWith("//")){
continue;
}
if(trimLine.length()>0){
result+="\\n";
}
result += line;
}
return result;
}
分析
稍微分析一下就发现这段代码错漏百出:
- /*xxxxx*/这样的注释就会导致multiLine永远为true
- 在正常的代码行末尾也能加上//xxxxx这样的注释,无法处理
- 另外也需要考虑出现在字符串中的注释标识,比如System.out.println(“//xxxxx/*xxxx*/”)就不应该被去除
- 其他bug…
分析到这里已经知道这题可以给自己0分了,还是先去研究清楚Java中一共会出现那些注释情况,已经那些可能被误伤的字符串。
/**
* 多行注释
* @param args
*/
if(a==0/*单行的多行注释*/)
//普通单行注释
int i=0; //行尾的单行注释
String str1 = "https://www.qq.com"; //可能被误伤的字符串1
String str2 = "/*******/"; //可能被误伤的字符串2
另外经过查阅文档和在IDE里面尝试,得知:
在多行注释的开头/*和结尾*/之间不允许出现其他的*/。
这么分析下来,这看似简单的题目还挺复杂的。逻辑弄清楚了,用if/else总能堆出来。不过,能不能用正则来实现呢?自己的正则水平实在是水得很,只能去google了。很快就在Stack Overflow找到答案,手动尝试了下确实满足所有的情况了。简单把帖子里面的正则用java写出来如下:
public String removeComments(String sourceCode){
return sourceCode==null?null:sourceCode.replaceAll("((['\"])(?:(?!\\2|\\\\).|\\\\.)*\\2)|//[^\\n]*|/\\*(?:[^*]|\\*(?!/))*\\*/","$1");
}
自己写不出来这么优秀的正则,至少也要弄清楚这里面每一段的意思。
正则分析
下面是StackOverflow中正则的逻辑结构图
可以看到,第一条规则是用来匹配代码中的字符串常量的,第二条是用来匹配单行注释//的,第三条是用来匹配多行注释/**/的。
难易度第二条<第三条<第一条,从易到难开始分析。
匹配单行注释
//[^\\n]*
这个很简单
匹配两个/以及任意后面跟着出现的非换行字符。
匹配多行注释
/\\*(?:[^*]|\\*(?!/))*\\*/
这个用到了非捕获匹配?:expr和负向前瞻?!expr
- (?:expr) 非捕获匹配,匹配expr但是不捕获匹配结果
- (?=expr) 正向前瞻,断言紧跟着当前位置的的字符串匹配expr,不捕获匹配结果
- (?<=expr) 正向后顾,断言当前位置之前的字符串匹配expr,不捕获匹配结果
- (?!expr) 负向前瞻,断言紧跟着当前位置的的字符串不匹配expr,不捕获匹配结果
- (?<!expr) 负向后顾,断言当前位置之前的字符串不匹配expr,不捕获匹配结果
了解了上述正则知识点以后,就可以解读这段正则了:
/\*匹配/*
[^*]匹配任意不包含*的字符串
\*(?!/)匹配*后面不是/的字符串
\*/匹配*/
匹配代码中的字符串
((['\"])(?:(?!\\2|\\\\).|\\\\.)*\\2)
这里用到了分组\2,也就是([‘\”])中匹配的内容,经过解读后如下:
(([‘\”])(?:(?!\2|\\).|\\.)*\2) 整段匹配分组为1
([‘\”]) 匹配’或”,分组为2
(?!\2|\\). 匹配当前位置往后不是单个出现的\或者分组2最近匹配到的结果
\\. 匹配\x,x为任意字符
\2 匹配最近在分组2最近匹配到的结果
这三段组合起来最后用分组1替换,就是正好去除单行和多行注释,同时保留了代码中的字符串。
详细的正则的每一步匹配过程可以用regex101的debug功能来看。