从 Guava CharMatcher 切换到 Regex

Switch from Guava CharMatcher to Regex

我目前正在使用以下 CharMatcher 算法在包含 1000 万条推文的文件中解析出推特状态中的所有 @Mentions。它似乎正在消耗大量内存。 运行 Netbeans 分析器,它似乎创建了很多 char[] 数组,我只能假设它们来自我实施的 CharMatcher 解决方案。

任何人都可以推荐更有效的 CharMatcher/Strings 方法或正则表达式解决方案(我认为这在对象创建方面会更有效)?速度不是我最关心的问题....

@Override
public boolean filter(Tweet msg) {

    List<String> statusList = Splitter.on(CharMatcher.BREAKING_WHITESPACE).trimResults().omitEmptyStrings().splitToList(msg.getStatusText());

    for (int i = 0; i < statusList.size(); i++) {
        if (statusList.get(i).contains("@")) {
            insertTwitterLegalUsernames(statusList.get(i), msg);
        }
    }

    if (msg.hasAtMentions()) {
        Statistics.increaseNumTweetsWithAtMentions();
    }

    statusList = null;
    return msg.hasAtMentions();
}

private void insertTwitterLegalUsernames(String token, Tweet msg) {
    token = token.substring(token.indexOf("@"), token.length());
    List<String> splitList = Splitter.on(CharMatcher.inRange('0', '9').or(CharMatcher.inRange('a', 'z')).or(CharMatcher.inRange('A', 'Z')).or(CharMatcher.anyOf("_@")).negate()).splitToList(token);
    for (int j = 0; j < splitList.size(); j++) {
        if (splitList.get(j).length() > 1 && splitList.get(j).contains("@")) {
            String finalToken = splitList.get(j).substring(splitList.get(j).lastIndexOf("@") + 1, splitList.get(j).length());
            if (!finalToken.equalsIgnoreCase(msg.getUserScreenNameString())) {
                msg.addAtMentions(finalToken);
            }
        }
    }

}

预期的输入可以是任何包含用户名的内容。我想提取被认为合法的用户名,以“@”开头,后跟任意数量的数字或字符 'a' - 'z', 'A' - 'Z', 0 -9 和 '_',以 '@' 开头。

如果紧跟在“@”之后有任何非法字符,我们将忽略,但我们希望提取其他合法用户名或非法字符之前或之后的用户名。

例如输入:

"!@@@Mike,#Java@Nancy_2,this this on for size"

应该return:

Mike

Nancy_2

答案应适用于 Java。

来自你的解释:

The expected input could be anything with username's throughout it. I want to extract the username which is legal with any character 'a' - 'z', 'A' - 'Z', 0-9 and '_', beginning with an '@'. Should there be any illegal characters immediately following the '@', we would disregard, however we would expect to extract usernames that are either before or after either other legal usernames or illegal characters

我们似乎正在搜索 [\w][a-zA-Z0-9_] 的 shorthand),它紧接在 @ 之前。这在正则表达式中非常简单,主要担心消除回溯和几乎匹配的成本。

模式:

(?<=@)[\w]++

会完全按照您的要求去做。

打破模式:

  • (?<=@) 是一个积极的回顾断言,检查 @ 是否先于此匹配
  • [\w]++ 所有格匹配名称本身,它必须至少包含一个字符。

首先,全局声明 Pattern 。它是线程安全的,应该被重用。

private static final Pattern TWITTER_NAME = Pattern.compile("(?<=@)[\w]++")

然后您可以使用类似这种方法来提取(唯一)用户名:

public static Set<String> findNames(final String input) {
    final Matcher matcher = TWITTER_NAME.matcher(input);
    final Set<String> names = new HashSet<>();
    while (matcher.find()) {
        names.add(matcher.group());
    }
    return names;
}

请注意,您还可以将 Matcherreset(String) 重用,但 Matcher 而非 线程安全的 - 您可以考虑使用 ThreadLocal 匹配器实例以在必要时提高性能。如果不使用多线程,那么你也可以使用全局 Matcher

正在使用您的输入进行测试:

public static void main(final String[] args) throws Exception {
    System.out.println(findNames("!@@@Mike,#Java@Nancy_2,this this on for size"));
}

产量:

[Mike, Nancy_2]

作为旁注,您在所有 List 上按索引循环。这是一个非常糟糕的主意——尤其是当你不知道 List Splitter.splitToList return 的类型时。如果它恰好是 LinkedList 那么按索引访问是 O(n) 所以在这个循环中:

for(final String s : myList) {
    System.out.println(s);
}

明明是O(n),同样循环按索引:

for(int i = 0; i < myList.size(); ++i) {
    System.out.println(myList.get(i));
}

很容易成为O(n^2)。这是毫无理由的巨大性能损失。

TL;DR:永远不要使用索引循环,除非你:

  1. 知道你的 ListRandomAccess;和
  2. 出于某种原因确实需要索引。

进一步补充,如果你想成为 Java 8-y,你可以使用以下代码将 Matcher 包装在 Spliterator 中:

public class MatcherSpliterator extends AbstractSpliterator<MatchResult> {

    private final Matcher m;

    public MatcherSpliterator(final Matcher m) {
        super(Long.MAX_VALUE, ORDERED | NONNULL | IMMUTABLE);
        this.m = m;
    }

    @Override
    public boolean tryAdvance(Consumer<? super MatchResult> action) {
        if (!m.find()) {
            return false;
        }
        action.accept(m.toMatchResult());
        return true;
    }
}

然后一个简单的方法 return 匹配结果 Stream:

public static Stream<MatchResult> extractMatches(final Pattern pattern, final String input) {
    return StreamSupport.stream(new MatcherSpliterator(pattern.matcher(input)), false);
}

现在你的方法变成了:

public static Set<String> findNames(final String input) {
    return extractMatches(TWITTER_NAME, input)
            .map(MatchResult::group)
            .collect(toSet());        
}

灵感来自this SO answer