【问题标题】:Scanner.findAll() and Matcher.results() work differently for same input text and patternScanner.findAll() 和 Matcher.results() 对于相同的输入文本和模式的工作方式不同
【发布时间】:2020-10-02 01:19:20
【问题描述】:

我在使用正则表达式拆分属性字符串的过程中看到了这个有趣的事情。我无法找到根本原因。

我有一个字符串,其中包含属性键=值对等文本。 我有一个正则表达式,它根据 = 位置将字符串拆分为键/值。它将第一个 = 视为分割点。值也可以包含 =。

我尝试在 Java 中使用两种不同的方式来实现它。

  1. 使用Scanner.findAll()方法

    这与预期不符。它应该根据模式提取和打印所有键。但我发现它的行为很奇怪。我有一个键值对如下

    SectionError.ErrorMessage=errorlevel=Warning {HelpMessage:This is very important message This is very important .....}

应该提取的键是 SectionError.ErrorMessage= 但它也将 errorlevel= 视为键。

有趣的一点是,如果我从 String 传递的属性中删除一个字符,它会表现良好并且只提取 SectionError.ErrorMessage= 键。

  1. 使用 Matcher.results() 方法

    这很好用。我们在属性字符串中放什么都没问题。

我尝试过的示例代码:

import java.util.Scanner;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;

import static java.util.regex.Pattern.MULTILINE;

public class MessageSplitTest {

    static final Pattern pattern = Pattern.compile("^[a-zA-Z0-9._]+=", MULTILINE);

    public static void main(String[] args) {
        final String properties =
                "SectionOne.KeyOne=first value\n" + // removing one char from here would make the scanner method print expected keys
                        "SectionOne.KeyTwo=second value\n" +
                        "SectionTwo.UUIDOne=379d827d-cf54-4a41-a3f7-1ca71568a0fa\n" +
                        "SectionTwo.UUIDTwo=384eef1f-b579-4913-a40c-2ba22c96edf0\n" +
                        "SectionTwo.UUIDThree=c10f1bb7-d984-422f-81ef-254023e32e5c\n" +
                        "SectionTwo.KeyFive=hello-world-sample\n" +
                        "SectionThree.KeyOne=first value\n" +
                        "SectionThree.KeyTwo=second value additional text just to increase the length of the text in this value still not enough adding more strings here n there\n" +
                        "SectionError.ErrorMessage=errorlevel=Warning {HelpMessage:This is very important message This is very important message This is very important messageThis is very important message This is very important message This is very important message This is very important message This is very important message This is very important message This is very important message This is very important messageThis is very important message This is very important message This is very important message This is very important message This is very important message}\n" +
                        "SectionFour.KeyOne=sixth value\n" +
                        "SectionLast.KeyOne=Country";

        printKeyValuesFromPropertiesUsingScanner(properties);
        System.out.println();
        printKeyValuesFromPropertiesUsingMatcher(properties);
    }

    private static void printKeyValuesFromPropertiesUsingScanner(String properties) {
        System.out.println("===Using Scanner===");
        try (Scanner scanner = new Scanner(properties)) {
            scanner
                    .findAll(pattern)
                    .map(MatchResult::group)
                    .forEach(System.out::println);
        }
    }

    private static void printKeyValuesFromPropertiesUsingMatcher(String properties) {
        System.out.println("===Using Matcher===");
        pattern.matcher(properties).results()
                .map(MatchResult::group)
                .forEach(System.out::println);

    }
}

打印输出:

===Using Scanner===
SectionOne.KeyOne=
SectionOne.KeyTwo=
SectionTwo.UUIDOne=
SectionTwo.UUIDTwo=
SectionTwo.UUIDThree=
SectionTwo.KeyFive=
SectionThree.KeyOne=
SectionThree.KeyTwo=
SectionError.ErrorMessage=
errorlevel=
SectionFour.KeyOne=
SectionLast.KeyOne=

===Using Matcher===
SectionOne.KeyOne=
SectionOne.KeyTwo=
SectionTwo.UUIDOne=
SectionTwo.UUIDTwo=
SectionTwo.UUIDThree=
SectionTwo.KeyFive=
SectionThree.KeyOne=
SectionThree.KeyTwo=
SectionError.ErrorMessage=
SectionFour.KeyOne=
SectionLast.KeyOne=

这可能是什么根本原因?扫描仪的 findAllma​​tcher 的工作方式是否不同?

如果需要更多信息,请告诉我。

【问题讨论】:

  • @Sweeper 我已经更新了标签。由于拼写错误,它被添加了。感谢您的关注。

标签: java regex pattern-matching java.util.scanner java-9


【解决方案1】:

Scanner 的文档经常提到“缓冲区”这个词。这表明Scanner 不知道它正在读取的整个字符串,并且一次只在缓冲区中保存一小部分。这是有道理的,因为Scanners 也被设计为从流中读取,从流中读取所有内容可能需要很长时间(或永远!)并占用大量内存。

Scanner的源码中,确实有一个CharBuffer

// Internal buffer used to hold input
private CharBuffer buf;

由于字符串的长度和内容,扫描程序决定将所有内容加载到...

SectionError.ErrorMessage=errorlevel=Warning {HelpMessage:This is very...
                          ^
                    somewhere here
(It could be anywhere in the word "errorlevel")

...进入缓冲区。然后,在读取字符串的那一半之后,字符串的另一半开始如下:

errorlevel=Warning {HelpMessage:This is very...

errorLevel= 现在是字符串的开头,导致模式匹配。

Related Bug?

Matcher 不使用缓冲区。它将与之匹配的整个字符串存储在字段中:

/**
 * The original string being matched.
 */
CharSequence text;

所以Matcher 中没有观察到这种行为。

【讨论】:

  • 错误报告是关于跨越缓冲区边界的分隔符。但是findAll 不使用分隔符。
  • @Holger 我也不是 100% 确定,这就是为什么我放一个“?”那里:) 我怀疑那个错误的原因和这个错误是一样的。 nextfindAll 都涉及寻找模式,并且该模式跨越缓冲区边界。
  • 是的,它们似乎是相关的。当出现这些场景失败的一般模式时,就会提出是否所有操作都受到影响的问题。它还与性能问题重叠,Scanner(String) 构造函数不应该使用StringReader,因为它可以轻松地使用CharBuffer.wrap(…) 来构造一个状态,就好像已经读取了整个字符串,而无需复制任何数据。当然,对于所有其他构造函数,该错误仍然存​​在……
  • 我添加了一个答案来扩充这个答案,提供一些具有更简单模式的测试代码,以演示这个问题以及如何解决这个问题。
【解决方案2】:

Sweepers answer 没看错,这是Scanner 的缓冲区不包含整个字符串的问题。我们可以简化示例来具体触发问题:

static final Pattern pattern = Pattern.compile("^ABC.", Pattern.MULTILINE);
public static void main(String[] args) {
    String testString = "\nABC1\nXYZ ABC2\nABC3ABC4\nABC4";
    String properties = "X".repeat(1024 - testString.indexOf("ABC4")) + testString;

    String s1 = usingScanner(properties);
    System.out.println("Using Scanner: "+s1);
    String m = usingMatcher(properties);
    System.out.println("Using Matcher: "+m);

    if(!s1.equals(m)) System.out.println("mismatch");
    if(s1.equals(usingScannerNoStream(properties)))
        System.out.println("Not a stream issue");
}
private static String usingScanner(String source) {
    return new Scanner(source)
        .findAll(pattern)
        .map(MatchResult::group)
        .collect(Collectors.joining(" + "));
}
private static String usingScannerNoStream(String source) {
    Scanner s = new Scanner(source);
    StringJoiner sj = new StringJoiner(" + ");
    for(;;) {
        String match = s.findWithinHorizon(pattern, 0);
        if(match == null) return sj.toString();
        sj.add(match);
    }
}
private static String usingMatcher(String source) {
    return pattern.matcher(source).results()
        .map(MatchResult::group)
        .collect(Collectors.joining(" + "));
}

哪个打印:

Using Scanner: ABC1 + ABC3 + ABC4 + ABC4
Using Matcher: ABC1 + ABC3 + ABC4
mismatch
Not a stream issue

这个例子在前缀前面加上 X 字符,以便将误报匹配的开头与缓冲区的大小对齐。 Scanner 的初始缓冲区大小为 1024,但在需要时可能会扩大。

由于findAll 忽略了扫描器的分隔符,就像findWithinHorizon 一样,这段代码还表明手动循环使用findWithinHorizon 表现出相同的行为,换句话说,这不是所使用的Stream API 的问题。

由于Scanner 会在需要时扩大缓冲区,我们可以通过使用匹配操作来解决此问题,该操作在执行预期的匹配操作之前强制将整个内容读取到缓冲区中,例如

private static String usingScanner(String source) {
    Scanner s = new Scanner(source);
    s.useDelimiter("(?s).*").hasNext();
    return s
        .findAll(pattern)
        .map(MatchResult::group)
        .collect(Collectors.joining(" + "));
}

这个特定的hasNext() 带有一个占用整个字符串的定界符,将强制对字符串进行完全缓冲,而不会使位置提前。随后的findAll() 操作忽略了分隔符和hasNext() 检查的结果,但由于缓冲区已完全填满,因此不再出现此问题。

当然,这会破坏Scanner 在解析实际流时的优势。

【讨论】:

    猜你喜欢
    • 2013-12-09
    • 1970-01-01
    • 1970-01-01
    • 2017-06-23
    • 1970-01-01
    • 2011-10-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多