[Из песочницы] Анализ английского текста с чашкой кофе «JavaSE8»
От автора
«Куда только не заведёт любопытство» — именно с этих слов и началась эта история.
Дело обстояло так.
Вернулся я из командировки из США, где провел целый месяц своей жизни. Готовился я Вам скажу я к ней основательно и прилично так налегал на английский, но вот не задача, приехав к заморским друзьям я понял что совершенно их не понимаю. Моему огорчению не было предела. Первым делом по приезду я встретился с другом, который свободно говорит по английски, излил ему душу и услышал в ответ:»… ты просто не те слова учил, нужно учить самые популярные… запас слов, который используется в повседневных разговорах не более 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
И наконец фильтруем нужные нам слова, для того чтобы покрыть N% текста методом filterWordsMap (Map
Ставим эксперимент (выведем в консоль список слов)
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.: От себя хочу сказать что мои друзья заинтересовались этим экспериментом, и мы будем развивать эту тему. Было принято решение начать создание портала, на котором любой желающий сможет проанализировать интересующий его текст. По результатам таких анализов будет собираться статистика и формироваться ТОРы английских слов.
О всех наших проблемах и достижениях на этом тернистом пути я буду писать в следующих постах. Спасибо за внимание и, надеюсь, до новых встреч.