Библиотека Java для извлечения ключевых слов из входящего текста

Я ищу библиотеку Java для извлечения ключевых слов из блока текста.

Процесс должен быть следующим:

остановить очистку слов -> определение корней -> поиск ключевых слов на основе статистической информации по английской лингвистике - то есть, если слово встречается в тексте больше раз, чем на английском языке с точки зрения вероятности, чем оно является кандидатом ключевого слова.

Есть ли библиотека, которая выполняет эту задачу?


person Shay    schedule 03.07.2013    source источник


Ответы (3)


Вот возможное решение с использованием Apache Lucene. Я использовал не последнюю версию, а 3.6.2 , так как это тот, который я знаю лучше всего. Помимо /lucene-core-x.x.x.jar, не забудьте добавить в свой проект /contrib/analyzers/common/lucene-analyzers-x.x.x.jar из загруженного архива: он содержит анализаторы для конкретного языка (особенно английский в вашем случае).

Обратите внимание, что это только найдет частоты вводимых текстовых слов на основе их соответствующей основы. Сравнение этих частот со статистикой на английском языке будет выполнено позже (этот ответ, кстати, может помочь).


Модель данных

Одно ключевое слово для одного ствола. У разных слов может быть одна и та же основа, отсюда и набор terms. Частота ключевых слов увеличивается каждый раз, когда обнаруживается новый термин (даже если он уже был найден - набор автоматически удаляет дубликаты).

public class Keyword implements Comparable<Keyword> {

  private final String stem;
  private final Set<String> terms = new HashSet<String>();
  private int frequency = 0;

  public Keyword(String stem) {
    this.stem = stem;
  }

  public void add(String term) {
    terms.add(term);
    frequency++;
  }

  @Override
  public int compareTo(Keyword o) {
    // descending order
    return Integer.valueOf(o.frequency).compareTo(frequency);
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    } else if (!(obj instanceof Keyword)) {
      return false;
    } else {
      return stem.equals(((Keyword) obj).stem);
    }
  }

  @Override
  public int hashCode() {
    return Arrays.hashCode(new Object[] { stem });
  }

  public String getStem() {
    return stem;
  }

  public Set<String> getTerms() {
    return terms;
  }

  public int getFrequency() {
    return frequency;
  }

}

Утилиты

Чтобы остановить слово:

public static String stem(String term) throws IOException {

  TokenStream tokenStream = null;
  try {

    // tokenize
    tokenStream = new ClassicTokenizer(Version.LUCENE_36, new StringReader(term));
    // stem
    tokenStream = new PorterStemFilter(tokenStream);

    // add each token in a set, so that duplicates are removed
    Set<String> stems = new HashSet<String>();
    CharTermAttribute token = tokenStream.getAttribute(CharTermAttribute.class);
    tokenStream.reset();
    while (tokenStream.incrementToken()) {
      stems.add(token.toString());
    }

    // if no stem or 2+ stems have been found, return null
    if (stems.size() != 1) {
      return null;
    }
    String stem = stems.iterator().next();
    // if the stem has non-alphanumerical chars, return null
    if (!stem.matches("[a-zA-Z0-9-]+")) {
      return null;
    }

    return stem;

  } finally {
    if (tokenStream != null) {
      tokenStream.close();
    }
  }

}

Для поиска в коллекции (будет использован список потенциальных ключевых слов):

public static <T> T find(Collection<T> collection, T example) {
  for (T element : collection) {
    if (element.equals(example)) {
      return element;
    }
  }
  collection.add(example);
  return example;
}

Основной

Вот основной метод ввода:

public static List<Keyword> guessFromString(String input) throws IOException {

  TokenStream tokenStream = null;
  try {

    // hack to keep dashed words (e.g. "non-specific" rather than "non" and "specific")
    input = input.replaceAll("-+", "-0");
    // replace any punctuation char but apostrophes and dashes by a space
    input = input.replaceAll("[\\p{Punct}&&[^'-]]+", " ");
    // replace most common english contractions
    input = input.replaceAll("(?:'(?:[tdsm]|[vr]e|ll))+\\b", "");

    // tokenize input
    tokenStream = new ClassicTokenizer(Version.LUCENE_36, new StringReader(input));
    // to lowercase
    tokenStream = new LowerCaseFilter(Version.LUCENE_36, tokenStream);
    // remove dots from acronyms (and "'s" but already done manually above)
    tokenStream = new ClassicFilter(tokenStream);
    // convert any char to ASCII
    tokenStream = new ASCIIFoldingFilter(tokenStream);
    // remove english stop words
    tokenStream = new StopFilter(Version.LUCENE_36, tokenStream, EnglishAnalyzer.getDefaultStopSet());

    List<Keyword> keywords = new LinkedList<Keyword>();
    CharTermAttribute token = tokenStream.getAttribute(CharTermAttribute.class);
    tokenStream.reset();
    while (tokenStream.incrementToken()) {
      String term = token.toString();
      // stem each term
      String stem = stem(term);
      if (stem != null) {
        // create the keyword or get the existing one if any
        Keyword keyword = find(keywords, new Keyword(stem.replaceAll("-0", "-")));
        // add its corresponding initial token
        keyword.add(term.replaceAll("-0", "-"));
      }
    }

    // reverse sort by frequency
    Collections.sort(keywords);

    return keywords;

  } finally {
    if (tokenStream != null) {
      tokenStream.close();
    }
  }

}

Пример

Используя метод guessFromString в вводной части статьи в Википедии Java, вот первые 10 часто встречающиеся ключевые слова (т. е. основы):

java         x12    [java]
compil       x5     [compiled, compiler, compilers]
sun          x5     [sun]
develop      x4     [developed, developers]
languag      x3     [languages, language]
implement    x3     [implementation, implementations]
applic       x3     [application, applications]
run          x3     [run]
origin       x3     [originally, original]
gnu          x3     [gnu]

Просмотрите список вывода, чтобы узнать, какие были исходные найденные слова для каждой основы, получив terms наборы (отображаются в скобках [...] в приведенном выше примере).


Что дальше

Сравните отношения основной частоты / суммы частот со статистикой по английскому языку и держите меня в курсе, если вам это удалось: меня тоже может заинтересовать :)

person sp00m    schedule 03.07.2013
comment
Насколько я понимаю, код должен работать на сервере apache. Что, если мое программное обеспечение должно быть локальным? - person Shay; 09.07.2013
comment
@Shay Зачем ему нужен сервер Apache? Я просто поместил KeywordsGuesser.guessFromString("input") в метод public static void main(String[] args), чтобы создать пример. - person sp00m; 09.07.2013
comment
Я не знаком с Lucene и вижу, что код сильно зависит от него, поэтому я предположил, что это так. Любая идея о том, где можно найти такой английский словарь основ? - person Shay; 09.07.2013
comment
@Shay Вы можете получить список здесь, но вам придется заплатить, чтобы получить его целиком. Я нашел этот тоже, что кажется довольно интересным, но я могу Не уверяю, что у него достаточно релевантных данных. - person sp00m; 09.07.2013
comment
Похоже, что версия 3.x.x больше не доступна. Пришлось скачать и попробовать 4.4.0. Я не знаю, в чем проблема, но я получаю исключение с нулевым указателем, когда пытаюсь выполнить код. - person Shay; 29.07.2013
comment
@Shay Вы перешли по ссылке, которую я дал (ссылка)? Вроде еще есть в наличии. - person sp00m; 29.07.2013
comment
@ sp00m, спасибо за полный пример кода, все работает, кроме одного - остановки фильтрации слов. Не могли бы вы взглянуть: stackoverflow.com/q/36241051/462347 - person Mike B.; 27.03.2016
comment
невозможно разрешить класс ClassicTokenizer с помощью Lucene 6.1.0, пожалуйста, помогите? - person Utsav Gupta; 02.08.2016
comment
@ sp00m Я получил это исключение java.lang.IllegalStateException: нарушение контракта TokenStream: отсутствует вызов reset () / close (), многократно вызывается reset () или подкласс не вызывает super.reset (). Дополнительные сведения о правильном рабочем процессе потребления см. В документации Javadocs класса TokenStream. - person Duc Tran; 27.11.2016
comment
@ sp00m у меня заработало. Вам нужно 2 вызова reset () в методах guessFromString () и stem (). - person Duc Tran; 27.11.2016

Обновленная и готовая к использованию версия предложенного выше кода.
Этот код совместим с Apache Lucene 5.x… 6.x.

CardKeyword класс:

import java.util.HashSet;
import java.util.Set;

/**
 * Keyword card with stem form, terms dictionary and frequency rank
 */
class CardKeyword implements Comparable<CardKeyword> {

    /**
     * Stem form of the keyword
     */
    private final String stem;

    /**
     * Terms dictionary
     */
    private final Set<String> terms = new HashSet<>();

    /**
     * Frequency rank
     */
    private int frequency;

    /**
     * Build keyword card with stem form
     *
     * @param stem
     */
    public CardKeyword(String stem) {
        this.stem = stem;
    }

    /**
     * Add term to the dictionary and update its frequency rank
     *
     * @param term
     */
    public void add(String term) {
        this.terms.add(term);
        this.frequency++;
    }

    /**
     * Compare two keywords by frequency rank
     *
     * @param keyword
     * @return int, which contains comparison results
     */
    @Override
    public int compareTo(CardKeyword keyword) {
        return Integer.valueOf(keyword.frequency).compareTo(this.frequency);
    }

    /**
     * Get stem's hashcode
     *
     * @return int, which contains stem's hashcode
     */
    @Override
    public int hashCode() {
        return this.getStem().hashCode();
    }

    /**
     * Check if two stems are equal
     *
     * @param o
     * @return boolean, true if two stems are equal
     */
    @Override
    public boolean equals(Object o) {

        if (this == o) return true;

        if (!(o instanceof CardKeyword)) return false;

        CardKeyword that = (CardKeyword) o;

        return this.getStem().equals(that.getStem());
    }

    /**
     * Get stem form of keyword
     *
     * @return String, which contains getStemForm form
     */
    public String getStem() {
        return this.stem;
    }

    /**
     * Get terms dictionary of the stem
     *
     * @return Set<String>, which contains set of terms of the getStemForm
     */
    public Set<String> getTerms() {
        return this.terms;
    }

    /**
     * Get stem frequency rank
     *
     * @return int, which contains getStemForm frequency
     */
    public int getFrequency() {
        return this.frequency;
    }
}

KeywordsExtractor класс:

import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.core.LowerCaseFilter;
import org.apache.lucene.analysis.core.StopFilter;
import org.apache.lucene.analysis.en.EnglishAnalyzer;
import org.apache.lucene.analysis.en.PorterStemFilter;
import org.apache.lucene.analysis.miscellaneous.ASCIIFoldingFilter;
import org.apache.lucene.analysis.standard.ClassicFilter;
import org.apache.lucene.analysis.standard.StandardTokenizer;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;

import java.io.IOException;
import java.io.StringReader;
import java.util.*;

/**
 * Keywords extractor functionality handler
 */
class KeywordsExtractor {

    /**
     * Get list of keywords with stem form, frequency rank, and terms dictionary
     *
     * @param fullText
     * @return List<CardKeyword>, which contains keywords cards
     * @throws IOException
     */
    static List<CardKeyword> getKeywordsList(String fullText) throws IOException {

        TokenStream tokenStream = null;

        try {
            // treat the dashed words, don't let separate them during the processing
            fullText = fullText.replaceAll("-+", "-0");

            // replace any punctuation char but apostrophes and dashes with a space
            fullText = fullText.replaceAll("[\\p{Punct}&&[^'-]]+", " ");

            // replace most common English contractions
            fullText = fullText.replaceAll("(?:'(?:[tdsm]|[vr]e|ll))+\\b", "");

            StandardTokenizer stdToken = new StandardTokenizer();
            stdToken.setReader(new StringReader(fullText));

            tokenStream = new StopFilter(new ASCIIFoldingFilter(new ClassicFilter(new LowerCaseFilter(stdToken))), EnglishAnalyzer.getDefaultStopSet());
            tokenStream.reset();

            List<CardKeyword> cardKeywords = new LinkedList<>();

            CharTermAttribute token = tokenStream.getAttribute(CharTermAttribute.class);

            while (tokenStream.incrementToken()) {

                String term = token.toString();
                String stem = getStemForm(term);

                if (stem != null) {
                    CardKeyword cardKeyword = find(cardKeywords, new CardKeyword(stem.replaceAll("-0", "-")));
                    // treat the dashed words back, let look them pretty
                    cardKeyword.add(term.replaceAll("-0", "-"));
                }
            }

            // reverse sort by frequency
            Collections.sort(cardKeywords);

            return cardKeywords;
        } finally {
            if (tokenStream != null) {
                try {
                    tokenStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Get stem form of the term
     *
     * @param term
     * @return String, which contains the stemmed form of the term
     * @throws IOException
     */
    private static String getStemForm(String term) throws IOException {

        TokenStream tokenStream = null;

        try {
            StandardTokenizer stdToken = new StandardTokenizer();
            stdToken.setReader(new StringReader(term));

            tokenStream = new PorterStemFilter(stdToken);
            tokenStream.reset();

            // eliminate duplicate tokens by adding them to a set
            Set<String> stems = new HashSet<>();

            CharTermAttribute token = tokenStream.getAttribute(CharTermAttribute.class);

            while (tokenStream.incrementToken()) {
                stems.add(token.toString());
            }

            // if stem form was not found or more than 2 stems have been found, return null
            if (stems.size() != 1) {
                return null;
            }

            String stem = stems.iterator().next();

            // if the stem form has non-alphanumerical chars, return null
            if (!stem.matches("[a-zA-Z0-9-]+")) {
                return null;
            }

            return stem;
        } finally {
            if (tokenStream != null) {
                try {
                    tokenStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Find sample in collection
     *
     * @param collection
     * @param sample
     * @param <T>
     * @return <T> T, which contains the found object within collection if exists, otherwise the initially searched object
     */
    private static <T> T find(Collection<T> collection, T sample) {

        for (T element : collection) {
            if (element.equals(sample)) {
                return element;
            }
        }

        collection.add(sample);

        return sample;
    }
}

Вызов функции:

String text = "…";
List<CardKeyword> keywordsList = KeywordsExtractor.getKeywordsList(text);
person Mike B.    schedule 26.03.2016
comment
Я пробовал этот код, и он не работал должным образом под Lucene 6.x. Мне пришлось добавить несколько вызовов reset () в поток токенов. Кроме того, похоже, что он не обрабатывает пунктирные слова должным образом ... Я заметил, что термин, подобный отраслевому, был заменен на отраслевой-0, признанный в попытке предотвратить разбиение слова токенизатором, но у меня все еще есть токен 0, распознанный так что этот взлом, похоже, не сработал. - person J.D. Corbin; 21.11.2016
comment
Мне удалось решить проблему с StandardTokenizer, используя WhitespaceTokenizer. Похоже, он отлично справлялся с пунктирными словами без необходимости в хитрости. - person J.D. Corbin; 21.11.2016
comment
Мне любопытно, какие банки и версии использовались для этого. Я пробовал lucene-core для версий 4.9.0, 5.5.5, 6.5.5, и в каждом случае было много ошибок компиляции. - person Michael Easter; 03.10.2019
comment
@MichaelEaster, этот код был написан, протестирован и работал на Apache Lucene 6.4.1. Пожалуйста, попробуйте запустить этот код на простом Java-проекте с минимумом зависимостей и Apache Lucene 6.4.1. API может претерпеть некоторые изменения. - person Mike B.; 03.10.2019
comment
@MikeB. Спасибо! Это сработало. Я не понимал, что мне нужна еще одна банка, кроме lucene-core. В эти выходные я опубликую здесь полное рабочее решение. - person Michael Easter; 04.10.2019
comment
@MichaelEaster, «Мне нужен был другой jar кроме lucene-core», это странно, насколько я помню, я использовал стандартные jar-файлы Lucune. - person Mike B.; 04.10.2019
comment
@MikeB Я построил этот пример с помощью Gradle здесь - github.com/codetojoy/sandbox_lucene/ / master / ... В течение следующих нескольких недель я могу попробовать использовать последнюю версию Lucene и некоторые другие идеи (с помощью других примеров в репозитории) - person Michael Easter; 05.10.2019

Относительно простой подход, основанный на алгоритме RAKE и моделях opennlp в оболочке rapidrake-java.

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.apache.commons.io.IOUtils;

import io.github.crew102.rapidrake.model.RakeParams;
import io.github.crew102.rapidrake.model.Result;

public class KeywordExtractor {

    private static String delims = "[-,.?():;\"!/]";
    private static String posUrl = "model-bin/en-pos-maxent.bin";
    private static String sentUrl = "model-bin/en-sent.bin";

    public static void main(String[] args) throws IOException {

        InputStream stream = new FileInputStream("res/stopwords-terrier.txt");
        String[] stopWords = IOUtils.readLines(stream, "UTF-8").stream().toArray(String[]::new);
        String[] stopPOS = {"VBD"};
        RakeParams params = new RakeParams(stopWords, stopPOS, 0, true, delims);
        RakeAlgorithm rakeAlg = new RakeAlgorithm(params, posUrl, sentUrl);
        Result aRes = rakeAlg.rake("I'm looking for a Java library to extract keywords from a block of text.");
        System.out.println(aRes);
        // OUTPUT:
        // [looking (1), java library (4), extract keywords (4), block (1), text (1)]
    }
}

Как вы можете видеть из выходных данных, вы получаете карту ключевых слов с их относительными весами.

Как описано на странице https://github.com/crew102/rapidrake-java, вам необходимо загрузить файлы en-pos-maxent.bin и model-bin/en-sent.bin со страницы opennlp. Поместите их в папку model-bin в корне вашего проекта (при использовании структуры проекта maven он должен быть родственником вашей папки src). Файл игнорируемых слов можно взять, например, из https://github.com/terrier-org/terrier-desktop/blob/master/share/stopword-list.txt.

person ccpizza    schedule 30.06.2020