我有一个初始列表,每个科目都有主题和短语。
public class Subject {
private String subject_name;
private List<Phrase> phrases;
}
public class Phrase {
private String phrase_name;
}
我需要过滤初始主题列表(应该得到另一个列表),条件是短语名称应该匹配输入文本中的单词。 所以,如果我有输入List:
subjects :
[
{
subject_name : "black",
phrases :
[
phrase_name : "one",
phrase_name : "two",
phrase_name : "three"
]
},
{
subject_name : "white",
phrases :
[
phrase_name : "qw",
phrase_name : "as",
phrase_name : "do",
phrase_name : "oopopop"
]
},
{
subject_name : "green",
phrases :
[
phrase_name : "rrr",
phrase_name : "ppo"
]
}
]
我输入text = "one year today some rrr"
,最后我需要获取以下列表
subjects :
[
{
subject_name : "black",
phrases :
[
phrase_name : "one"
]
},
{
subject_name : "green",
phrases :
[
phrase_name : "rrr"
]
}
]
下面的代码工作正常,我得到了理想的结果,但是当我需要过滤例如20000&#34; text&#34;对于那些带我一些〜5分钟的受试者,取决于文字大小。
private List<Subject> filterSubjects(List<Subject> subjects, String text) {
List<Subject> result = new ArrayList<Subject>();
for (Subject subject : subjects) {
List<Phrase> p = new ArrayList<Phrase>();
for (Phrase phrase : subject.getPhrases()) {
String regex = "\\b(" + replaceSpecialChars(phrase.getName()).toLowerCase() + ")\\b";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
if (matcher.find()) {
p.add(phrase);
}
}
if (!p.isEmpty()) {
result.add(new Subject.SubjectBuilder(subject.getSubjectId(), subject.getName())
.setWeight(subject.getWeight()).setColor(subject.getColor())
.setOntologyId(subject.getOntologyId()).setCreatedBy(subject.getCreatedBy())
.setUpdatedBy(subject.getUpdatedBy()).setPhrases(p).build());
}
}
return result;
}
我也尝试使用流,但这对我不起作用,因为我不想过滤初始主题列表,但需要新的一个:
subjects = subjects.stream()
.filter(s -> s.getPhrases().parallelStream()
.anyMatch(p -> text.matches(".*\\b" + replaceSpecialChars(p.getName().toLowerCase()) + "\\b.*")))
.collect(Collectors.toList());
subjects.parallelStream()
.forEach(s -> s.getPhrases().removeIf(p -> !text.matches(".*\\b"
+ replaceSpecialChars(p.getName().toLowerCase())
+ "\\b.*")));
修改
这是分析的结果
答案 0 :(得分:3)
正如评论中所建议的那样,您应该进行介绍。正确使用,分析器应该给你更多的细节,而不是“在该方法中消耗的整个时间”。您应该能够看到Pattern.compile()
,Matcher.find()
,ArrayList.add()
以及所有其他方法花费了多少时间,无论它们是您的还是JDK方法。
你这样做是绝对至关重要的,否则你就会因盲目工作而浪费精力。例如,也许ArrayList.add()
正在花费时间。你可以用各种方式解决它。但是,除非你有确凿的证据证明这就是问题所在,否则为什么要花时间呢?
您还可以应用提取方法重构,以便您拥有更多自己的方法供分析器报告。这样做的一个好处是编译器和运行时非常适合优化小方法。
当您找到花费时间的方法时,您需要:
如果它在replaceSpecialChars()
花了很多时间,你应该看看它,并提高它的表现。
根据其复杂性,编译正则表达式可能需要一些时间。如果replaceSpecialChars()
中包含Pattern.compile()
,请将其移动到仅调用一次的地方(静态初始值设定项,构造函数等)。如果它使用正则表达式并且没有Pattern.compile()
,请考虑引入一个。
您的修改显示大部分时间都花费在您向我们展示的代码调用的Pattern.compile()
中。
由于您向我们展示的代码中的regex
是使用数据中的字符串构建的,因此您不能只调用Pattern.compile()
一次。但是,您可能会从重复短语的备忘中受益 - 这取决于数据中的重复次数。
Map<String, Pattern> patterns = new HashMap<>();
Pattern pattern(String s) {
Pattern pattern = patterns.get(s);
if(pattern == null) {
pattern = Pattern.compile("\\b" + s + "\\b");
patterns.put(s,pattern);
}
return pattern;
}
(注意这不是线程安全的 - 有更好的缓存类,例如在Guava中)
您可以通过准备(每次输入一次)使文本内的查找更容易:
现在您只需要preparedText.contains(" " + phrase.getName() + " ")
。这避免了完全编译正则表达式。您可以使用正则表达式来准备文本,但这只需要执行一次(如果您有多个文本,则可以重用已编译的Pattern
。
但如果你这样做,你也可以将文本处理成Set
,搜索速度比字符串更快:
Set<String> wordSet = new HashSet<>(Arrays.asList(preparedText.split(" ")));
对于足够大的文本, wordSet.contains(phrase.getName())
应该比preparedText.contains(phrase.getName())
更快。
它也可能 - 再次,取决于数据 - 更快地迭代text
中的标记,寻找集合中的单词,而不是循环遍历单词。这可能会以不同的顺序返回项目 - 这是否重要取决于您的要求。
Set<String> lookingFor = collectWordsToFind(subject);
StringTokenizer tokens = new StringTokenizer(text);
for(String token : tokens) {
if(lookingFor.contains(token)) { // or if(lookingFor.remove(token))
outputlist.add(token);
}
}
这可以避免多次扫描每个text
。
最后,踩到后面,我会考虑先预处理Subject
数据,制作phrase_name
到Subject
的地图。也许你已经从外部资源中读取数据了 - 如果是这样的话,一定要在你阅读的时候建立这个地图(也许不是你现在拥有的List):
Map<String,Set<Subject>> map = new HashMap<>();
for(Subject subject : subjects) {
for(String phrase : subject.phrases()) {
String name = phrase.name();
Set<Subject> subjectsForName = map.get(name);
if(subjectsForName == null) {
subjectsForName = new HashSet<>();
map.put(name, subjectsForName);
}
subjectsForName.add(subject);
}
}
现在,对于输入text
中的每个字词,您可以快速获得包含该phrase_name Set<Subjects> subjectsForThisWord = map.get(word)
的一组主题。
Map<T,Collection<U>>
是一种相当常见的模式,但是像Guava和Apache Commons这样的第三方集合库提供了MultiMap
,它使用更干净的API做同样的事情。
答案 1 :(得分:1)
正如您所提到的那样,您尝试了没有运气的流,这是我尝试将您的功能转换为流(警告:未经测试!):
subjects.parallelStream()
.map(subject -> {
List<Phrase> filteredPhrases = subject.getPhrases().parallelStream()
.filter(p -> text.matches(".*\\b" + replaceSpecialChars(p.getName().toLowerCase()) + "\\b.*"))
.collect(Collectors.toList());
return new AbstractMap.SimpleEntry<>(subject, filteredPhrases);
})
.filter(entry -> !entry.getValue().isEmpty())
.map(entry -> {
Subject subj = entry.getKey();
List<Phrase> filteredPhrases = entry.getValue();
return new Subject.SubjectBuilder(subj.getId(), subj.getName()).setWeight(subj.getWeight()).setPhrases(filteredPhrases);
})
.map(Subject.SubjectBuilder::build)
.collect(Collectors.toList());
基本上,第一张地图是构建一对原始主题和过滤后的短语,在第二张地图中,这些对映射到单个SubjectBuilder
实例,并初始化所有参数(另请注意,而不是原始短语,过滤后的短语,然后第三张地图,只是新主题的建立。
我不确定这段代码是否会比你的更快(我也没有测试过,所以也没有任何保证!),它只是一个想法,你如何解决你的任务与溪流。
答案 2 :(得分:1)
你必须找到的词越多,执行独特的正则表达式匹配就越少。除了每个不同正则表达式的准备成本之外,您还要为每个单词执行新的线性搜索操作。相反,让引擎仅匹配整个单词并对单词执行快速地图查找。
首先,准备一个查找地图
Map<String,Map.Entry<Phrase,Subject>> lookup = subject.stream()
.flatMap(s->s.getPhrases().stream().map(p->new AbstractMap.SimpleImmutableEntry<>(p,s)))
.collect(Collectors.toMap(e -> e.getKey().getName(), Function.identity()));
然后,使用正则表达式引擎对整个单词进行流式处理,并通过Subject
查找关联的Phrase
/ Subject
组合,组,并将结果组转换为新的{之后{1}}:
Subject
如果List<Subject> result =
Pattern.compile("\\W+").splitAsStream(text)
.map(lookup::get)
.filter(Objects::nonNull)
.collect(Collectors.groupingBy(Map.Entry::getValue,
Collectors.mapping(Map.Entry::getKey, Collectors.toList())))
.entrySet().stream()
.map(e -> {
Subject subject=e.getKey();
return new Subject.SubjectBuilder(subject.getSubjectId(), subject.getName())
.setWeight(subject.getWeight()).setColor(subject.getColor())
.setOntologyId(subject.getOntologyId()).setCreatedBy(subject.getCreatedBy())
.setUpdatedBy(subject.getUpdatedBy()).setPhrases(e.getValue()).build();
})
.collect(Collectors.toList());
支持将现有Subject.SubjectBuilder
指定为模板而不必手动复制每个属性,那会简单得多......
答案 3 :(得分:0)
在我看来,你无法摆脱for循环(这是代码复杂性的绝对杀手),因为你需要检查每个主题(即使你在过滤之前对主题进行了排序)。所以,我认为唯一可能的加速可以通过多线程完成(如果你不关心输出列表中的顺序)。为此,您可以使用java的内置ExecutorService
。它将产生指定数量的线程,您提交所有过滤任务,ExecutorService会自动在线程中调度它们。
修改:您可能还需要确保SubjectBuilder
不会创建p
的副本,因为这也会花费大量时间。< / p>
答案 4 :(得分:0)
我会尝试摆脱正则表达式,因为你正在为每个主题中的每个短语编译这些。我不确定它会更有效率,或者达到完全相同的结果,因为我无法针对您的数据集运行它,但您可以尝试这样的更改:
List<Phrase> p = new ArrayList<Phrase>();
for (Phrase phrase : subject.getPhrases()) {
//String regex = "\\b(" + phrase.getName().toLowerCase() + ")\\b";
//Pattern pattern = Pattern.compile(regex);
//Matcher matcher = pattern.matcher(text);
//
//if (matcher.find()) {
// p.add(phrase);
//}
if (text.contains(phrase.getName().toLowerCase())) {
p.add(phrase);
}
}
我做了一个基本测试,我认为它应该以类似的方式匹配
答案 5 :(得分:0)
解决方案似乎非常简单,使用&#34;包含&#34;而不是使用消耗最多处理时间的Pattern:
private List<Subject> filterSubjects(List<Subject> subjects, String text) {
String SPACE_PATTERN = " ";
List<Subject> result = new ArrayList<Subject>();
for (Subject subject : subjects) {
List<Phrase> p = new ArrayList<Phrase>();
for (Phrase phrase : subject.getPhrases()) {
if (text.contains(SPACE_PATTERN + replaceSpecialChars(phrase.getName()).toLowerCase() + SPACE_PATTERN)) {
p.add(phrase);
}
}
if (!p.isEmpty()) {
result.add(new Subject.SubjectBuilder(subject.getSubjectId(), subject.getName())
.setWeight(subject.getWeight()).setColor(subject.getColor())
.setOntologyId(subject.getOntologyId()).setCreatedBy(subject.getCreatedBy())
.setUpdatedBy(subject.getUpdatedBy()).setPhrases(p).build());
}
}
return result;
}
这给我的表现从大约5分钟开始,现在大约20秒,20K文字处理。我要优化的另一个步骤是从循环中取出replaceSpecialChars
以获取短语名称