正则表达式学习笔记

正则表达式学习笔记

最近做了一道算法题,题目是提供一个函数来去除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功能来看。


;