从Guava CharMatcher切换到Regex

时间:2015-03-26 17:18:33

标签: java regex guava

我目前正在使用以下CharMatcher算法在一个1000万条推文的文件中解析twitter状态中的所有@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,这个尺寸”

应该返回:

  迈克

     

Nancy_2

答案应该适用于Java。

1 个答案:

答案 0 :(得分:5)

从你的解释:

  

预期输入可以是包含用户名的任何内容。我想提取与任何字符'a' - 'z','A' - 'Z',0-9和'_'合法的用户名,以'@'开头。如果在'@'之后立即出现任何非法字符,我们会忽略,但我们希望提取其他合法用户名或非法字符之前或之后的用户名

我们似乎正在搜索[\w]的{​​{1}}(这是[a-zA-Z0-9_]的简写),紧接着是@。这在Regex中非常简单,主要的担心是消除回溯和几乎匹配的成本。

模式:

(?<=@)[\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返回什么类型。如果恰好是LinkedList,那么访问by-index为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 :除非您:

,否则切勿使用by-index循环
  1. 知道您的ListRandomAccess;和
  2. 出于某种原因确实需要索引。

  3. 进一步的补充,如果你想成为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;
        }
    }
    

    然后,返回匹配的简单方法会产生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

    的灵感