拆分java.util.stream.Stream

13

我有一个文本文件,其中包含URL和电子邮件。我需要从文件中提取它们。每个URL和电子邮件可能会出现多次,但结果不应包含重复项。 我可以使用以下代码提取所有URL:

Files.lines(filePath).
    .map(urlPattern::matcher)
    .filter(Matcher::find)
    .map(Matcher::group)
    .distinct();

我可以使用以下代码提取所有电子邮件:

Files.lines(filePath).
    .map(emailPattern::matcher)
    .filter(Matcher::find)
    .map(Matcher::group)
    .distinct();

我能否只读取一次由Files.lines(filePath)返回的流并提取所有URL和电子邮件,类似将行流拆分为URL流和电子邮件流。


Stream<String> fileStream = Files.lines(Paths.get("test")); fileStream.//匹配电子邮件 fileStream.//匹配URL 当你的问题是不想创建两个流时,我能想到的唯一解决方案。 - Loki
2
我猜,将这些行存储到List中并遍历两次不算是一个合格的解决方案,对吗? - Tagir Valeev
6
洛基,你无法两次穿越同一条河流。 - Tagir Valeev
不知道这个。但是我们都学到了一些东西 ;) - Loki
1
@TagirValeev 在这种情况下,我肯定会将数据存储在一个List中,并进行两次遍历,这似乎是我能想到的最好的解决方案。 - Voidpaw
4个回答

10

你可以使用partitioningBy收集器,虽然它仍然不是非常优雅的解决方案。

Map<Boolean, List<String>> map = Files.lines(filePath)
        .filter(str -> urlPattern.matcher(str).matches() ||
                       emailPattern.matcher(str).matches())
        .distinct()
        .collect(Collectors.partitioningBy(str -> urlPattern.matcher(str).matches()));
List<String> urls = map.get(true);
List<String> emails = map.get(false);

如果不想两次应用正则表达式,可以使用中间的键值对对象(例如,SimpleEntry)来实现:

public static String classify(String str) {
    return urlPattern.matcher(str).matches() ? "url" : 
        emailPattern.matcher(str).matches() ? "email" : null;
}

Map<String, Set<String>> map = Files.lines(filePath)
        .map(str -> new AbstractMap.SimpleEntry<>(classify(str), str))
        .filter(e -> e.getKey() != null)
        .collect(Collectors.groupingBy(e -> e.getKey(),
            Collectors.mapping(e -> e.getValue(), Collectors.toSet())));

使用我的免费StreamEx库,最后一步会更短:

Map<String, Set<String>> map = StreamEx.of(Files.lines(filePath))
        .mapToEntry(str -> classify(str), Function.identity())
        .nonNullKeys()
        .grouping(Collectors.toSet());

1
该问题在过滤后使用了.distinct(),这表明收集到Set而不是List更为合适。通常情况下,classify方法是一个好主意,使得使用现有的Collector比实现自定义的Collector更容易(就像我所做的那样)。 - Holger
1
@york.beta:只要使用matches,就没有使用group(1)的意义,因为这意味着整个String都匹配。如果您使用find,那么情况就不同了,因为它意味着在同一行中可能会有两个模式匹配... - Holger
1
@york.beta:这听起来你应该使用 find 而不是 regexp,正如所说的,这意味着你应该考虑如果两个模式在同一行中匹配或者超过一个匹配时该怎么做... ^.*regexp.*$ 显然是一种反模式。 - Holger
1
@york.beta: 邮箱和URL能不能同时出现在同一行呢?如果有多个URL或邮箱该怎么办? - Tagir Valeev
1
@york.beta:如果这只是一个“例子”,为什么你坚持要求解决方案调用group(1),即使底层模式未知?无论实际应用程序做什么,如果它将matchesgroup(n)结合使用,很可能是错误的。即使保证最多只有一个匹配,使用实际模式而不是带有.*等的find()group()(没有数字)是正确的工具。 - Holger
显示剩余5条评论

4
您可以在 Collector 中执行匹配操作:
Map<String,Set<String>> map=Files.lines(filePath)
    .collect(HashMap::new,
        (hm,line)-> {
            Matcher m=emailPattern.matcher(line);
            if(m.matches())
              hm.computeIfAbsent("mail", x->new HashSet<>()).add(line);
            else if(m.usePattern(urlPattern).matches())
              hm.computeIfAbsent("url", x->new HashSet<>()).add(line);
        },
        (m1,m2)-> m2.forEach((k,v)->m1.merge(k, v,
                                     (s1,s2)->{s1.addAll(s2); return s1;}))
    );
Set<String> mail=map.get("mail"), url=map.get("url");

请注意,这可以轻松地适应在一行内查找多个匹配项。
Map<String,Set<String>> map=Files.lines(filePath)
    .collect(HashMap::new,
        (hm,line)-> {
            Matcher m=emailPattern.matcher(line);
            while(m.find())
              hm.computeIfAbsent("mail", x->new HashSet<>()).add(m.group());
            m.usePattern(urlPattern).reset();
            while(m.find())
              hm.computeIfAbsent("url", x->new HashSet<>()).add(m.group());
        },
        (m1,m2)-> m2.forEach((k,v)->m1.merge(k, v,
                                     (s1,s2)->{s1.addAll(s2); return s1;}))
    );

1

由于无法重复使用流,我认为唯一的选择就是“手动操作”。

File.lines(filePath).forEach(s -> /** match and sort into two lists */ );

如果有其他解决方案,我很乐意了解!

是的,我考虑过了,我很好奇是否还有其他解决方案,所以回答了这个问题。 - york.beta

0

总的问题应该是:为什么你只想要流一次?

提取URL和提取电子邮件是不同的操作,因此应该在它们自己的流操作中处理。即使底层流源包含数十万条记录,迭代时间与映射和过滤操作相比可以忽略不计。

唯一需要考虑的可能是IO操作的性能问题。因此,最清晰的解决方案是只读取文件一次,然后在结果集上进行两次流操作:

List<String> allLines = Files.readAllLines(filePath);
allLines.stream() ... // here do the URLs
allLines.stream() ... // here do the emails

当然,这需要一些内存。


有时候一次性完成可能是合理的。例如,输入文件包含数百万行,其中只有一小部分符合正则表达式。 - Tagir Valeev
提取URL和电子邮件只是一个例子,我想从几个大文件中提取另一些数据。因此,将它们读入内存或多次读取它们都不是解决方案。 - york.beta

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接