[Из песочницы] Анализ английского текста с чашкой кофе «JavaSE8»

03e4b44046744e64ba78a48b0d31b810.png

От автора


«Куда только не заведёт любопытство» — именно с этих слов и началась эта история.

Дело обстояло так.

Вернулся я из командировки из США, где провел целый месяц своей жизни. Готовился я Вам скажу я к ней основательно и прилично так налегал на английский, но вот не задача, приехав к заморским друзьям я понял что совершенно их не понимаю. Моему огорчению не было предела. Первым делом по приезду я встретился с другом, который свободно говорит по английски, излил ему душу и услышал в ответ:»… ты просто не те слова учил, нужно учить самые популярные… запас слов, который используется в повседневных разговорах не более 1000 слов…»

Хм, так ли это?, возник вопрос в моей голове… И пришла мне в голову идея проанализировать разговорный текст, так сказать, определить те самые употребляемые слова.

Исходные данные


В качестве разговорного текста я решил взять сценарий одной из серий сериала друзья, заодно и проверим гипотезу — »… если смотреть сериалы на английском, то хорошо подтянешь язык …» (сценарий без особого труда можно найти в интернете)

Используемые технологии


  • Java SE 8
  • Eclipse Mars 2


Ожидаемый результат


Результатом нашего творчества станет jar библиотека, которая будет составлять лексический минимум для текста с заданным процентом понимания. То есть мы например хотим понять 80% всего текста и библиотека, проанализировав текст выдаёт нам набор слов, которые необходимо для этого выучить.
И так, поехали.

Объекты DTO (боевые единицы)


ReceivedText.java

package ru.lexmin.lexm_core.dto;

/**
 * Класс для получения от пользователя введённой информации а виде текста (text)
 * и процента понимания (percent)
 *
 */
public class ReceivedText {

        /**
         * Версия
         */
        private static final long serialVersionUID = 5716001583591230233L;

        // текст, который ввёл пользователь
        private String text;

        // желаемый процент понимания текста пользователем
        private int percent;

        /**
         * Пустой конструктор
         */
        public ReceivedText() {
                super();
        }

        /**
         * Конструктор с параметрами
         * 
         * @param text
         *            {@link String}
         * @param percent
         *            int
         */
        public ReceivedText(String text, int percent) {
                super();
                this.text = text;
                this.percent = percent;
        }

        /**
         * @return text {@link String}
         */
        public String getText() {
                return text;
        }

        /**
         * Устанавливает параметр
         * 
         * @param text
         *            text {@link String}
         */
        public void setText(String text) {
                this.text = text;
        }

        /**
         * @return percent {@link int}
         */
        public int getPercent() {
                return percent;
        }

        /**
         * Устанавливает параметр
         * 
         * @param percent
         *            percent {@link int}
         */
        public void setPercent(int percent) {
                this.percent = percent;
        }

}


WordStat.java

package ru.lexmin.lexm_core.dto;

import java.util.HashMap;
import java.util.Map;

/**
 * Класс для передачи рзультов обработки текста в виде: - количество слов в
 * тексте - честота употребления каждого слова.
 * 
 * Количество слов хранится в поле countOfWords (int) Частота употребления
 * хранится в поле frequencyWords (Map): - ключом является
 * слово - значением частора употребления в тексте
 * 
 * Поле receivedText - содержет ссылку на dto с текстом и процентом понимания.
 *
 */
public class WordStat {

        /**
         * Версия
         */
        private static final long serialVersionUID = -1211530860332682161L;

        // ссылка на dto с исходным текстом и параметрами
        private ReceivedText receivedText;

        // кол-во слов в тексте, на который ссылка receivedText
        private int countOfWords;

        // статистика по часторе слов текста, на который ссылка receivedText,
        // отфильтрованная с учётом процента понимания
        private Map frequencyWords;

        /**
         * Констркутор по умолчанию
         */
        public WordStat() {
                super();
        }

        /**
         * Конструктор с параметрами
         * 
         * @param receivedText
         * @param countOfWords
         * @param frequencyWords
         */
        public WordStat(ReceivedText receivedText, int countOfWords, Map frequencyWords) {
                this.receivedText = receivedText;
                this.countOfWords = countOfWords;
                this.frequencyWords = frequencyWords;
        }

        /**
         * Конструктор задаёт значение поля receivedText из передоваемого объекта.
         * остальнве поля интциализируются значениями по умолчанию
         * 
         * @param receivedText
         */
        public WordStat(ReceivedText receivedText) {
                this.receivedText = receivedText;
                // инициализация остальных полей значениями по умолчинию
                this.countOfWords = 0;
                this.frequencyWords = new HashMap();
        }

        /**
         * @return receivedText {@link ReceivedText}
         */
        public ReceivedText getReceivedText() {
                return receivedText;
        }

        /**
         * Устанавливает параметр
         * 
         * @param receivedText
         *            receivedText {@link ReceivedText}
         */
        public void setReceivedText(ReceivedText receivedText) {
                this.receivedText = receivedText;
        }

        /**
         * @return countOfWords {@link int}
         */
        public int getCountOfWords() {
                return countOfWords;
        }

        /**
         * Устанавливает параметр
         * 
         * @param countOfWords
         *            countOfWords {@link int}
         */
        public void setCountOfWords(int countOfWords) {
                this.countOfWords = countOfWords;
        }

        /**
         * @return frequencyWords {@link Map}
         */
        public Map getFrequencyWords() {
                return frequencyWords;
        }

        /**
         * Устанавливает параметр
         * 
         * @param frequencyWords
         *            frequencyWords {@link Map}
         */
        public void setFrequencyWords(Map frequencyWords) {
                this.frequencyWords = frequencyWords;
        }

}

Ну тут всё просто и понятно, думаю комментариев в коде достаточно

Интерфейс анализатора текстов (определяем функциональность)


TextAnalyzer.java

package ru.lexmin.lexm_core;

import ru.lexmin.lexm_core.dto.ReceivedText;
import ru.lexmin.lexm_core.dto.WordStat;

/**
 * Данный интерфейс описывает основной функционал анализа получаемого от
 * пользователя текста
 *
 */
public interface TextAnalyzer {

        /**
         * Мемод получает объект класса {@link WordStat}, заполненный данными,
         * актуальными для передаваемого объекта {@link ReceivedText}
         * 
         * @param receivedText
         *            {@link ReceivedText}
         * @return возврашает заполненный {@link WordStat}
         */
        public abstract WordStat getWordStat(ReceivedText receivedText);

}

Нам будет достаточно всего одного внешнего метода, который нам вернёт WordStat (DTO), из которого мы потом и вытащим слова.

Реализация анализатора текстов


TextAnalyzerImp.java

package ru.lexmin.lexm_core;

import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import ru.lexmin.lexm_core.dto.ReceivedText;
import ru.lexmin.lexm_core.dto.WordStat;

/**
 * Этот класс является реализацией интерфейса TextAnalyzer
 *
 */
public class TextAnalyzerImp implements TextAnalyzer {

        /* Константы */
        private final int PERCENT_100 = 100;
        private final int ONE_WORD = 1;
        private final String SPACE = " ";

        // регулярное выражение: все испольуемые апострофы
        private final String ANY_APOSTROPHE = "[’]";

        // применяемый, стандартный апостроф
        private final String AVAILABLE_APOSTROPHE = "'";

        // регулярное выражение: не маленькие латинские буквы, не пробел и не
        // апостроф(')
        private final String ONLY_LATIN_CHARACTERS = "[^a-z\\s']";

        // регулярное выражение: пробелы, более двух подрят
        private final String SPACES_MORE_ONE = "\\s{2,}";

        /**
         * Метод преобразует передаваемый текст в нижнеме регистру, производит
         * фильтрацию текста. В тексте отсаются только латинские буквы, пробельные
         * символы и верхний апостроф. Пробелы два и более подрят заменяются одним.
         * 
         * @param text
         *            {@link String}
         * @return отфильтрованный текст
         */
        private String filterText(String text) {

                String resultText = text.toLowerCase().replaceAll(ANY_APOSTROPHE, AVAILABLE_APOSTROPHE)
                                .replaceAll(ONLY_LATIN_CHARACTERS, SPACE).replaceAll(SPACES_MORE_ONE, SPACE);

                return resultText;
        }

        /**
         * Метод преобразует получаемый текст в Map<{слво}, {количество}>
         * 
         * @param text
         *            {@link String}
         * @return заполненный Map
         */
        private Map getWordsMap(String text) {

                Map wordsMap = new HashMap();

                String newWord = "";

                Pattern patternWord = Pattern.compile("(?[a-z']+)");
                Matcher matcherWord = patternWord.matcher(text);

                // поиск слов в тексте по паттерну
                while (matcherWord.find()) {

                        newWord = matcherWord.group("word");

                        if (wordsMap.containsKey(newWord)) {

                                // если слово уже есть в Map то увеличиваеи его количество на 1
                                wordsMap.replace(newWord, wordsMap.get(newWord) + ONE_WORD);

                        } else {

                                // если слова в Map нет то добавляем его со значением 1
                                wordsMap.put(newWord, ONE_WORD);

                        }
                }

                return wordsMap;
        }

        /**
         * Метод возвращает общее количество слов, суммируя частоту употребления
         * слов в получаемом Map
         * 
         * @param wordsMap
         *            {@link Map}
         * @return общее количество слов в тексте, по которому составлен Map
         */
        private int getCountOfWords(Map wordsMap) {

                int countOfWords = 0;

                // считаем в цикле сумму значений для всех слов в Map
                for (Integer value : wordsMap.values())
                        countOfWords += value;

                return countOfWords;
        }

        /**
         * Метод производит вычисление процентрого соотнашения аргумента
         * numberXPercents от аргумента number100Percents
         * 
         * @param number100Percents
         *            int
         * @param numberXPercents
         *            int
         * @return прочентное соотношение
         */
        private int getPercent(int number100Percents, int numberXPercents) {

                return (numberXPercents * PERCENT_100) / number100Percents;
        }

        /**
         * Метод выполняет фильтрацию слов в массива, чтобы их количество покрывало
         * заданный процент понимания текста
         * 
         * @param wordsMap
         *            {@link Map}
         * @param countOfWords
         *            int
         * @param percent
         *            int
         * @return возвращает отфильтрованный массив, элементы когорого
         *         отсорвированы по убывающей
         */
        private Map filterWordsMap(Map wordsMap, int countOfWords, int percent) {

                // LinkedHashMap - ассоциативный массив, который запоминает порядок
                // добавления элементов
                Map resultMap = new LinkedHashMap();

                int sumPercentOfWords = 0;

                // создаёт поток из Map с записями Entry,
                // отсортированными по убыванию
                Stream> streamWords = wordsMap.entrySet()
                            .stream().sorted(Map.Entry.comparingByValue(
                                       (Integer value1, Integer value2) -> (
                                                 value1.equals(value2)) ? 0 : ((value1 < value2) ? 1 : -1)
                                       )
                             );

                // создаём итератор для обхода всех записей потока
                Iterator> iterator = streamWords.iterator();

                // добавляем в resultMap каждую последующую запись из итератора, пока не
                // будет тостигнут заданный процент понимания
                while (iterator.hasNext() && (sumPercentOfWords < percent)) {

                        Entry wordEntry = iterator.next();

                        resultMap.put(wordEntry.getKey(), wordEntry.getValue());

                        sumPercentOfWords += getPercent(countOfWords, wordEntry.getValue());

                }

                return resultMap;
        }

        /*
         * (non-Javadoc)
         * 
         * @see
         * ru.lexmin.lexm_core.TextAnalyzer#getWordStat(ru.lexmin.lexm_core.dto.
         * ReceivedText)
         */
        @Override
        public WordStat getWordStat(ReceivedText receivedText) {

                WordStat wordStat = new WordStat(receivedText);

                Map wordsMap = getWordsMap(filterText(receivedText.getText()));

                wordStat.setCountOfWords(getCountOfWords(wordsMap));

                wordStat.setFrequencyWords(
                          filterWordsMap(wordsMap, wordStat.getCountOfWords(), receivedText.getPercent())
                );

                return wordStat;
        }

}


Я постарался максимально подробно закомментировать все методы.

Если кратко, то происходит следующее:
Сначала из текста вырезается всё что является латинскими буквами, апострофами или пробелами. Количество пробелов более 2х подряд заменяется одним. Делается это в методе метод filterText (String text).

Далее из подготовленного текста формируется массив слов — Map<слово, количество в тексте>. За это отвечает метод getWordsMap (String text).

Подсчитываем общее количество слов методом getCountOfWords (Map wordsMap)

И наконец фильтруем нужные нам слова, для того чтобы покрыть N% текста методом filterWordsMap (Map wordsMap, int countOfWords, int percent)

Ставим эксперимент (выведем в консоль список слов)

package testText;

import ru.lexmin.lexm_core.TextAnalyzer;
import ru.lexmin.lexm_core.TextAnalyzerImp;
import ru.lexmin.lexm_core.dto.ReceivedText;
import ru.lexmin.lexm_core.dto.WordStat;

public class Main {

 public static void main(String[] args) {

        final int PERCENT = 80;

        TextAnalyzer ta = new TextAnalyzerImp();

        String friends = "There's nothing to tell!  He's .... тут текст двух серий первого сезона";

        ReceivedText receivedText = new ReceivedText(friends, PERCENT);

        WordStat wordStat = ta.getWordStat(receivedText);

        System.out.println("Количество слов в тексте: " + wordStat.getCountOfWords());
        System.out.println("Количество слов, покрывающие 80% текста: " + wordStat.getFrequencyWords().size());
        System.out.println("Список слов, покрывающих 80% текста");
        wordStat.getFrequencyWords().forEach((word, count) -> System.out.println(word));

        }

}

Результат


Количество слов в тексте: 1481
Количество слов, покрывающие 80% текста: 501
Список слов, покрывающих 80% текста: i, a, and, you, the, to, just, this, it, be, is, my, no, of, that, me, don’t, with, it’s, out, paul, you’r, have, her, okay, … и так далее

Заключение


В данном эксперименте мы проанализировали только две серии первого сезона и делать какие-либо выводы рано, но две серии идут около 80–90 мин и для их понимания (остальные 20% оставляем на додумывание, логику и зрительное восприятие) достаточно всего 501 слово.

P.S.: От себя хочу сказать что мои друзья заинтересовались этим экспериментом, и мы будем развивать эту тему. Было принято решение начать создание портала, на котором любой желающий сможет проанализировать интересующий его текст. По результатам таких анализов будет собираться статистика и формироваться ТОРы английских слов.

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

© Habrahabr.ru