От автора
«Куда только не заведёт любопытство» — именно с этих слов и началась эта история.
Дело обстояло так.
Вернулся я из командировки из США, где провел целый месяц своей жизни. Готовился я Вам скажу я к ней основательно и прилично так налегал на английский, но вот не задача, приехав к заморским друзьям я понял что совершенно их не понимаю. Моему огорчению не было придела. Первым делом по приезду я встретился с другом, который свободно говорит по английски, излил ему душу и услышал в ответ: «… ты просто не те слова учил, нужно учить самые популярные… запас слов, который используется в повседневных разговорах не более 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<String, Integer>): - ключом является * слово - значением частора употребления в тексте * * Поле receivedText - содержет ссылку на dto с текстом и процентом понимания. * */ public class WordStat { /** * Версия */ private static final long serialVersionUID = -1211530860332682161L; // ссылка на dto с исходным текстом и параметрами private ReceivedText receivedText; // кол-во слов в тексте, на который ссылка receivedText private int countOfWords; // статистика по часторе слов текста, на который ссылка receivedText, // отфильтрованная с учётом процента понимания private Map<String, Integer> frequencyWords; /** * Констркутор по умолчанию */ public WordStat() { super(); } /** * Конструктор с параметрами * * @param receivedText * @param countOfWords * @param frequencyWords */ public WordStat(ReceivedText receivedText, int countOfWords, Map<String, Integer> 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<String, Integer>(); } /** * @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<String,Integer>} */ public Map<String, Integer> getFrequencyWords() { return frequencyWords; } /** * Устанавливает параметр * * @param frequencyWords * frequencyWords {@link Map<String,Integer>} */ public void setFrequencyWords(Map<String, Integer> 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<String, Integer> getWordsMap(String text) { Map<String, Integer> wordsMap = new HashMap<String, Integer>(); String newWord = ""; Pattern patternWord = Pattern.compile("(?<word>[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<String, Integer> 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<String, Integer> filterWordsMap(Map<String, Integer> wordsMap, int countOfWords, int percent) { // LinkedHashMap - ассоциативный массив, который запоминает порядок // добавления элементов Map<String, Integer> resultMap = new LinkedHashMap<String, Integer>(); int sumPercentOfWords = 0; // создаёт поток из Map с записями Entry<String, Integer>, // отсортированными по убыванию Stream<Entry<String, Integer>> streamWords = wordsMap.entrySet() .stream().sorted(Map.Entry.comparingByValue( (Integer value1, Integer value2) -> ( value1.equals(value2)) ? 0 : ((value1 < value2) ? 1 : -1) ) ); // создаём итератор для обхода всех записей потока Iterator<Entry<String, Integer>> iterator = streamWords.iterator(); // добавляем в resultMap каждую последующую запись из итератора, пока не // будет тостигнут заданный процент понимания while (iterator.hasNext() && (sumPercentOfWords < percent)) { Entry<String, Integer> 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<String, Integer> 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<String, Integer> wordsMap)
И наконец фильтруем нужные нам слова, для того чтобы покрыть N% текста методом filterWordsMap(Map<String, Integer> 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.: От себя хочу сказать что мои друзья заинтересовались этим экспериментом, и мы будем развивать эту тему. Было принято решение начать создание портала, на котором любой желающий сможет проанализировать интересующий его текст. По результатам таких анализов будет собираться статистика и формироваться ТОРы английских слов.
О всех наших проблемах и достижениях на этом тернистом пути я буду писать в следующих постах. Спасибо за внимание и, надеюсь, до новых встреч.
ссылка на оригинал статьи https://habrahabr.ru/post/282750/
Добавить комментарий