【问题标题】:How to match a comment unless it's in a quoted string?除非它在带引号的字符串中,否则如何匹配评论?
【发布时间】:2010-02-17 21:36:27
【问题描述】:

所以我有一些字符串:

//Blah blah blach
// sdfkjlasdf
"Another //thing"

我正在使用 java regex 替换所有带有双斜杠的行,如下所示:

theString = Pattern.compile("//(.*?)\\n", Pattern.DOTALL).matcher(theString).replaceAll("");

它在大多数情况下都有效,但问题是它会删除所有出现的事件,我需要找到一种方法让它不删除引用的事件。我该怎么做呢?

【问题讨论】:

  • "有些人在遇到问题时会想:'我知道,我会使用正则表达式。'现在他们有两个问题。”
  • 能否详细说明解析器部分?
  • @Bears 会吃掉你,“有些人在遇到正则表达式时会想“我知道,我会使用我记得的引人入胜的引语”。现在他们没有在讨论中添加任何内容。” ` -- 托马拉克 `
  • 哈,以前从未见过这个问题/评论 (stackoverflow.com/questions/1098296/…)。无论如何,这只是我对人们滥用正则表达式的直觉反应。
  • "您能详细说明一下解析器部分吗?" - 我推荐 javacc。

标签: java regex parsing regex-negation


【解决方案1】:

与其使用解析整个 Java 源文件的解析器,或者自己编写只解析您感兴趣的部分的东西,不如使用一些 3rd 方工具,例如 ANTLR。

ANTLR 能够仅定义您感兴趣的那些标记(当然还有那些可能会弄乱您的标记流的标记,例如多行 cmets 和字符串和字符文字)。因此,您只需要定义一个能够正确处理这些标记的词法分析器(tokenizer 的另一个词)。

这称为语法。在 ANTLR 中,这样的语法可能如下所示:

lexer grammar FuzzyJavaLexer;

options{filter=true;}

SingleLineComment
  :  '//' ~( '\r' | '\n' )*
  ;

MultiLineComment
  :  '/*' .* '*/'
  ;

StringLiteral
  :  '"' ( '\\' . | ~( '"' | '\\' ) )* '"'
  ;

CharLiteral
  :  '\'' ( '\\' . | ~( '\'' | '\\' ) )* '\''
  ;

将以上内容保存在名为FuzzyJavaLexer.g 的文件中。现在将download ANTLR 3.2 here 保存在与FuzzyJavaLexer.g 文件相同的文件夹中。

执行以下命令:

java -cp antlr-3.2.jar org.antlr.Tool FuzzyJavaLexer.g

这将创建一个FuzzyJavaLexer.java 源类。

当然,您需要测试词法分析器,您可以通过创建一个名为 FuzzyJavaLexerTest.java 的文件并将以下代码复制到其中来完成:

import org.antlr.runtime.*;

public class FuzzyJavaLexerTest {
    public static void main(String[] args) throws Exception {
        String source = 
            "class Test {                                 \n"+
            "  String s = \" ... \\\" // no comment \";   \n"+
            "  /*                                         \n"+
            "   * also no comment: // foo                 \n"+
            "   */                                        \n"+
            "  char quote = '\"';                         \n"+
            "  // yes, a comment, finally!!!              \n"+
            "  int i = 0; // another comment              \n"+
            "}                                            \n";
        System.out.println("===== source =====");
        System.out.println(source);
        System.out.println("==================");
        ANTLRStringStream in = new ANTLRStringStream(source);
        FuzzyJavaLexer lexer = new FuzzyJavaLexer(in);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        for(Object obj : tokens.getTokens()) {
            Token token = (Token)obj;
            if(token.getType() == FuzzyJavaLexer.SingleLineComment) {
                System.out.println("Found a SingleLineComment on line "+token.getLine()+
                        ", starting at column "+token.getCharPositionInLine()+
                        ", text: "+token.getText());
            }
        }
    }
}

接下来,编译您的FuzzyJavaLexer.javaFuzzyJavaLexerTest.java

javac -cp .:antlr-3.2.jar *.java

最后执行FuzzyJavaLexerTest.class文件:

// *nix/MacOS
java -cp .:antlr-3.2.jar FuzzyJavaLexerTest

或:

// Windows
java -cp .;antlr-3.2.jar FuzzyJavaLexerTest

之后,您将看到以下内容打印到您的控制台:

===== source =====
class Test {                                 
  String s = " ... \" // no comment ";   
  /*                                         
   * also no comment: // foo                 
   */                                        
  char quote = '"';                         
  // yes, a comment, finally!!!              
  int i = 0; // another comment              
}                                            

==================
Found a SingleLineComment on line 7, starting at column 2, text: // yes, a comment, finally!!!              
Found a SingleLineComment on line 8, starting at column 13, text: // another comment  

很简单,嗯? :)

【讨论】:

  • ANTLR 一个解析器生成器。
  • @KennyTM,错了,我知道。但是 ANTLR 可用于仅创建词法分析器(无需解析器),甚至可以创建仅对您感兴趣的部分进行词法分析的词法分析器(使编写语法更加容易:您不需要解析整个源文件)。抱歉问了,但你有没有看我的回复?
  • 不错的ANTLR小教程!在我需要 ANTLR 之类的极少数情况下,我似乎永远找不到这种东西。
  • 谢谢艾伦。是的,尤其是使用带有options{filter=true;} 的词法分析器语法,它可以让您只指定您感兴趣的那些标记,这并不是 ANTLR 的一个众所周知的功能。我已经用它来突出我创建的一个小文本编辑器的语法。它使添加新的语言荧光笔变得轻而易举(当然,在对 ANTLR 语法有一定程度熟悉的情况下)。
  • +1 很好的例子!我只是还没有花时间学习 ANTLR,这会有所帮助。但我仍然对 RE 很满意(请参阅我的回答),尤其是 Perl 5.10 中更强大的实现,所以要进行切换将是一场斗争。
【解决方案2】:

使用解析器,逐个字符地确定它。

启动示例:

StringBuilder builder = new StringBuilder();
boolean quoted = false;

for (String line : string.split("\\n")) {
    for (int i = 0; i < line.length(); i++) {
        char c = line.charAt(i);
        if (c == '"') {
            quoted = !quoted;
        }
        if (!quoted && c == '/' && i + 1 < line.length() && line.charAt(i + 1) == '/') {
            break;
        } else {
            builder.append(c);
        }
    }
    builder.append("\n");
}

String parsed = builder.toString();
System.out.println(parsed);

【讨论】:

  • @BalusC,这可能会导致 OP 认为这个问题有点太容易了...... @Confused,想想当你遇到` \ . If you encounter a \ ` 和然后是",你还应该翻转quoted 标志吗?想想//(或引号)何时位于多行注释块内。
  • @Bart K.:这只是一个开始的例子:)
【解决方案3】:

(这是对@finnw 在his answer 下的评论中提出的问题的回答。与其说是对 OP 问题的回答,不如说是对为什么正则表达式是错误工具的扩展解释。)

这是我的测试代码:

String r0 = "(?m)^((?:[^\"]|\"(?:[^\"]|\\\")*\")*)//.*$";
String r1 = "(?m)^((?:[^\"\r\n]|\"(?:[^\"\r\n]|\\\")*\")*)//.*$";
String r2 = "(?m)^((?:[^\"\r\n]|\"(?:[^\"\r\n\\\\]|\\\\\")*\")*)//.*$";

String test = 
    "class Test {                                 \n"+
    "  String s = \" ... \\\" // no comment \";   \n"+
    "  /*                                         \n"+
    "   * also no comment: // but no harm         \n"+
    "   */                                        \n"+
    "  /* no comment: // much harm  */            \n"+
    "  char quote = '\"';  // comment             \n"+
    "  // another comment                         \n"+
    "  int i = 0; // and another                  \n"+
    "}                                            \n"
    .replaceAll(" +$", "");
System.out.printf("%n%s%n", test);

System.out.printf("%n%s%n", test.replaceAll(r0, "$1"));
System.out.printf("%n%s%n", test.replaceAll(r1, "$1"));
System.out.printf("%n%s%n", test.replaceAll(r2, "$1"));

r0 是您答案中编辑后的正则表达式;它只删除最后的评论 (// and another),因为其他所有内容都在 group(1) 中匹配。设置多行模式 ((?m)) 是 ^$ 正常工作所必需的,但它不能解决这个问题,因为您的字符类仍然可以匹配换行符。

r1 处理换行问题,但它仍然错误地匹配字符串文字中的// no comment,原因有两个:您没有在(?:[^\"\r\n]|\\\") 的第一部分包含反斜杠;并且您只使用了其中两个来匹配第二部分中的反斜杠。

r2 修复了这个问题,但它不会尝试处理 char 文字中的引号或多行 cmets 中的单行 cmets。它们可能也可以处理,但是这个正则表达式已经是 Baby Godzilla;你真的想看到它长大吗?

【讨论】:

  • 这个问题没有说明多行 cmets,所以我没有将它们包含在我的正则表达式中。
  • 你是对的,OP没有说它是Java源代码,只是有cmets和引用的字符串——事实上,他甚至没有提到转义的引号。无论如何,我使用它来演示纯正则表达式解决方案在需求蔓延时会多快变成泥潭。而且您的正则表达式中的错误很常见,因此剖析它们似乎是值得的。
【解决方案4】:

以下内容来自我几年前(在 Perl 中)编写的一个类似 grep 的程序。它可以选择在处理文件之前剥离 java cmets:

# ============================================================================
# ============================================================================
#
# strip_java_comments
# -------------------
#
# Strip the comments from a Java-like file.  Multi-line comments are
# replaced with the equivalent number of blank lines so that all text
# left behind stays on the same line.
#
# Comments are replaced by at least one space .
#
# The text for an entire file is assumed to be in $_ and is returned
# in $_
#
# ============================================================================
# ============================================================================

sub strip_java_comments
{
      s!(  (?: \" [^\"\\]*   (?:  \\.  [^\"\\]* )*  \" )
         | (?: \' [^\'\\]*   (?:  \\.  [^\'\\]* )*  \' )
         | (?: \/\/  [^\n] *)
         | (?: \/\*  .*? \*\/)
       )
       !
         my $x = $1;
         my $first = substr($x, 0, 1);
         if ($first eq '/')
         {
             "\n" x ($x =~ tr/\n//);
         }
         else
         {
             $x;
         }
       !esxg;
}

此代码确实可以正常工作,并且不会被棘手的注释/引用组合所迷惑。它可能会被 unicode 转义(\u0022 等)所愚弄,但如果您愿意,您可以轻松地先处理这些转义。

因为它是 Perl,而不是 java,所以替换代码必须改变。我将快速制作等效的 java。待命...

编辑:我刚刚整理了一下。可能需要工作:

// The trick is to search for both comments and quoted strings.
// That way we won't notice a (partial or full) comment withing a quoted string
// or a (partial or full) quoted-string within a comment.
// (I may not have translated the back-slashes accurately.  You'll figure it out)

Pattern p = Pattern.compile(
       "(  (?: \" [^\"\\\\]*   (?:  \\\\.  [^\"\\\\]* )*  \" )" +  //    " ... "
       "  | (?: ' [^'\\\\]*    (?:  \\\\.  [^'\\\\]*  )*  '  )" +  // or ' ... '
       "  | (?: //  [^\\n] *    )" +                               // or // ...
       "  | (?: /\\*  .*? \\* / )" +                               // or /* ... */
       ")",
       Pattern.DOTALL  | Pattern.COMMENTS
);

Matcher m = p.matcher(entireInputFileAsAString);

StringBuilder output = new StringBuilder();

while (m.find())
{
    if (m.group(1).startsWith("/"))
    {
        // This is a comment. Replace it with a space...
        m.appendReplacement(output, " ");

        // ... or replace it with an equivalent number of newlines
        // (exercise for reader)
    }
    else
    {
        // We matched a quoted string.  Put it back
        m.appendReplacement(output, "$1");
    }
}

m.appendTail(output);
return output.toString();

【讨论】:

    【解决方案5】:

    您无法使用正则表达式判断您是否在双引号字符串中。最后,正则表达式只是一个状态机(有时是扩展的)。我会使用 BalusC 或 this one 提供的解析器。

    如果您想知道为什么正则表达式受到限制,请阅读形式语法。维基百科article 是一个好的开始。

    【讨论】:

      猜你喜欢
      • 2015-03-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-08
      • 1970-01-01
      • 1970-01-01
      • 2023-03-27
      相关资源
      最近更新 更多