如何从Java8流(如grep)中获取匹配前后的行?
我有一个文本文件,里面有很多字符串行。如果我想在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 -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));
});
}