[英]JAVA speed up List filtering
我有一個初始清單,其中包含主題和每個主題的短語。
public class Subject {
private String subject_name;
private List<Phrase> phrases;
}
public class Phrase {
private String phrase_name;
}
我需要過濾初始主題列表( 應該獲取另一個列表 ),條件是短語名稱應與輸入文本中的單詞匹配。 因此,如果我有輸入列表:
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"
]
}
]
我有作為輸入文本= "one year today some rrr"
,最后我需要得到下面的列表
subjects :
[
{
subject_name : "black",
phrases :
[
phrase_name : "one"
]
},
{
subject_name : "green",
phrases :
[
phrase_name : "rrr"
]
}
]
下面的代碼可以正常工作,並且可以得到理想的結果,但是當我需要過濾例如20000個“文本”作為主題時,它會變慢,這需要我花費大約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.*")));
編輯
這是分析的結果
如評論中所建議,您應該進行概要分析。 如果使用得當,探查器應為您提供比“整個方法所花的時間”更多的詳細信息。 您應該能夠看到在Pattern.compile()
, Matcher.find()
, ArrayList.add()
和所有其他方法(無論是您的方法還是JDK方法Pattern.compile()
花費了多少時間。
這樣做絕對至關重要,否則您會因為盲目工作而浪費精力。 例如,也許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更快地進行搜索:
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
。 也許您已經從外部來源讀取了數據-如果是這樣,請務必在閱讀時構建此地圖(也許代替當前的列表):
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
每個單詞,您可以快速獲取一組包含該短語名稱的主題,即Set<Subjects> subjectsForThisWord = map.get(word)
。
Map<T,Collection<U>>
是一個相當常見的模式,但是第三方集合庫(例如Guava和Apache Commons)提供的MultiMap
使用更簡潔的API可以完成相同的任務。
正如您提到的那樣,嘗試流沒有運氣,這是我嘗試將您的函數轉換為流( 警告 :未經測試!):
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
實例(還要注意,已過濾的不是原始短語,而是原始短語被通過),那么第三張地圖就是新主題的建築。
我不確定這段代碼是否會比您的代碼快(我也沒有對其進行測試,因此沒有任何保證!),這只是一個想法,您可以用流來解決您的任務。
您必須找到的單詞越多,執行獨特的正則表達式匹配所得到的回報就越少。 除了每個不同的正則表達式的准備費用之外,您還針對每個單詞執行新的線性搜索操作。 取而代之的是,讓引擎僅匹配整個單詞,然后對單詞進行快速映射查找。
首先,准備一個查找圖
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
分組,然后將結果組轉換為新的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
支持將現有的Subject
指定為模板,而不必手動復制每個屬性,則會更加簡單。
在我看來,您無法擺脫for循環(這是代碼復雜性的絕對殺手),因為您需要檢查每個主題(即使您在過濾之前對主題進行了排序)。 因此,我認為唯一可能的加速可以通過多線程完成(如果您不關心輸出列表中的順序)。 為此,您可以使用Java的內置ExecutorService
。 它將生成指定數量的線程,您提交所有篩選任務,ExecutorService會自動在線程之間分派它們。
編輯:您可能還想確保您的SubjectBuilder
不創建p
的副本,因為這也會花費大量時間。
我會嘗試擺脫正則表達式,因為您正在為每個主題中的每個短語編譯這些正則表達式。 我不確定這樣做會效率更高還是獲得完全相同的結果,因為我無法針對您的數據集運行它,但是您可以嘗試這樣的更改:
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);
}
}
我做了一個基本測試,我認為它應該以類似的方式匹配
使用“包含”而不是使用消耗最多處理時間的模式,該解決方案似乎非常簡單:
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
以獲取短語名稱
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.