如何从Java8流(如grep)中获取匹配前后的行?

如何从Java8流(如grep)中获取匹配前后的行?,java,java-8,java-stream,Java,Java 8,Java Stream,我有一个文本文件,里面有很多字符串行。如果我想在grep中查找匹配前后的行,我会这样做: grep -A 10 -B 10 "ABC" myfile.txt 如何使用stream在Java 8中实现等效功能?由于现有方法不提供对流中元素邻居的访问,因此stream API不支持这种情况。在不创建自定义迭代器/拆分器和第三方库调用的情况下,我能想到的最接近的解决方案是将输入文件读入列表,然后使用索引流: List<String> input = Files.readAllLines(

我有一个文本文件,里面有很多字符串行。如果我想在grep中查找匹配前后的行,我会这样做:

grep -A 10 -B 10 "ABC" myfile.txt

如何使用stream在Java 8中实现等效功能?

由于现有方法不提供对流中元素邻居的访问,因此stream API不支持这种情况。在不创建自定义迭代器/拆分器和第三方库调用的情况下,我能想到的最接近的解决方案是将输入文件读入
列表
,然后使用索引流:

List<String> input = Files.readAllLines(Paths.get(fileName));
Predicate<String> pred = str -> str.contains("ABC");
int contextLength = 10;

IntStream.range(0, input.size()) // line numbers
    // filter them leaving only numbers of lines satisfying the predicate
    .filter(idx -> pred.test(input.get(idx))) 
    // add nearby numbers
    .flatMap(idx -> IntStream.rangeClosed(idx-contextLength, idx+contextLength))
    // remove numbers which are out of the input range
    .filter(idx -> idx >= 0 && idx < input.size())
    // sort numbers and remove duplicates
    .distinct().sorted()
    // map to the lines themselves
    .mapToObj(input::get)
    // output
    .forEachOrdered(System.out::println);

正如Tagir Valeev所指出的,streams API并没有很好地支持这种问题。如果您希望以增量方式从输入中读取行,并打印出与上下文匹配的行,则必须引入有状态管道阶段(或自定义收集器或拆分器),这会增加相当多的复杂性

如果您愿意将所有行读取到内存中,那么
位集
是处理匹配组的有用表示法。这与Tagir的解决方案有些相似,但它不是使用整数范围来表示要打印的行,而是在
位集中使用1位。
位集
的一些优点是,它具有许多内置的批量操作,并且具有紧凑的内部表示。它还可以生成一个1位的索引流,这对于这个问题非常有用

首先,让我们首先创建一个
位集
,它的每一行对应一个1位谓词:

void contextMatch(Predicate<String> pred, int before, int after, List<String> input) {
    int len = input.size();
    BitSet matches = IntStream.range(0, len)
                              .filter(i -> pred.test(input.get(i)))
                              .collect(BitSet::new, BitSet::set, BitSet::or);
如果我们只想打印所有行,包括上下文,我们可以这样做:

    context.stream()
           .forEachOrdered(i -> System.out.println(input.get(i)));
实际的
grep-aa-bb
命令在每组上下文行之间打印一个分隔符。为了确定何时打印分隔符,我们查看上下文位集中的每个1位。如果在它前面有一个0位,或者如果它在一开始,我们在结果中设置一位。这在每组上下文行的开头提供了一个1位:

    BitSet separators = context.stream()
                               .filter(i -> i == 0 || !context.get(i-1))
                               .collect(BitSet::new, BitSet::set, BitSet::or);
我们不希望在每组上下文行之前打印分隔符;我们想在每组之间打印它。这意味着我们必须清除第一个1位(如果有):

现在,我们可以打印出结果行。但在打印每行之前,我们先检查是否应打印分隔符:

    context.stream()
           .forEachOrdered(i -> {
               if (separators.get(i)) {
                   System.out.println("--");
               }
               System.out.println(input.get(i));
           });
}

如果您愿意使用第三方库并且不需要并行性,那么提供如下SQL风格的窗口函数

Seq.Seq(Files.readAllLines(path.get(新文件(“/path/to/Example.java”).toURI()))
.window(-1,1)
.filter(w->w.value().contains(“ABC”))
.forEach(w->{
System.out.println(“-1:+w.lag().orElse(”);
System.out.println(“0:+w.value());
System.out.println(“+1:”+w.lead().orElse(“”));
//ABC:只是检查一下
});
屈服

-1:。窗口(-1,1)
0:筛选器(w->w.value().包含(“ABC”))
+1:.forEach(w->{
-1:System.out.println(“+1:+w.lead().orElse(”));
0://ABC:只是检查一下
+1:       });
lead()
函数从窗口按遍历顺序访问下一个值,
lag()
函数访问前一行


免责声明:我为jOOλ背后的公司工作

有趣的方法,投票表决。另一种选择是将前两个步骤合并在一起,从我的解决方案中选择
IntStream.range(..).filter(..).flatMap(..).filter(..)
步骤,然后选择
.collect(BitSet::new,BitSet::set,BitSet::or)
而不是
.distinct().sorted()
。这将保持内存效率,同时看起来更“流畅”。顺便说一下
i>0&!context.get(i-1)| i==0
可以缩短为
i==0 | |!获取(i-1)
。我简化了中间步骤。我希望你不介意我直接编辑它;它看起来太复杂了,我无法评论它,而它的上下文很容易理解。@TagirValeev在你的“顺便说一句”中给出了一个很好的建议。我在后面添加了
I==0
大小写以拾取边缘大小写,但我没有注意到可以进行的简化。编辑。@Holger编辑得很好,谢谢。我还更新了附近的一些文本,以删除不再需要的描述。不幸的是,即时可用的流API不支持这一点,但您需要的是所谓的“滑动窗口”。
    BitSet separators = context.stream()
                               .filter(i -> i == 0 || !context.get(i-1))
                               .collect(BitSet::new, BitSet::set, BitSet::or);
    // clear the first bit
    int first = separators.nextSetBit(0);
    if (first >= 0) {
        separators.clear(first);
    }
    context.stream()
           .forEachOrdered(i -> {
               if (separators.get(i)) {
                   System.out.println("--");
               }
               System.out.println(input.get(i));
           });
}