簡體   English   中英

JAVA加速列表過濾

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM